不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

测试驱动编程(2)

hzqiuxm阅读(2666)

测试驱动编程-进阶单元测试

单元测试

要打造出出类拔萃的作品,你必须专注于最小的细节

单元测试正确打开方式

  • 单元测试应该是测试流程中占比最重的部分
  • 单元测试通常来说是指方法,但是有些情况下,整个类乃至整个应用程序都可视为单元
  • 单元测试是为了缩小其它(功能,集成)测试的范围,如果一个组织主要依赖于手动测试,这本身就是有问题的
  • 单元测试编写更容易,也能让代码的问题尽早暴露
  • 单元测试为代码重构提供了有力的支持

各类测试比较


下图测试金字塔展示的是UI测试、集成测试、单元测试之间的比重,单元测试要比其它测试多得多

拿最常见的用户注册测试场景举例,最常看到的测试用例就是要考虑用户名为空、密码为空、用户名格式、密码格式、用户名是否存在、密码安全等级等等。

仅为测试这一项功能,就可能需要数十个测试,再加上边界值等情况可能导致上百个测试用例,如果是人工的UI测试,那么代价将是非常大的。

如果使用单元测试来做这些会简单和方便的多,人工只需要填写一个集成测试,检查是否调用了正确的后端方法即可,而无需关注细节。当注册逻辑发生改变了也没关系,因为新单元测试会自动进行测试。

TDD中的单元测试

TDD中的单元测试和传统的单元测试有什么不同呢?答案是:时机
看过我之前写的一篇文章,测试驱动编程快速入门的应该都知道了一个名字:红绿重构

传统先写业务再写单元测试,主要是为了验证业务代码写的对不对,而TDD中的单元测试主要目标是作为驱动开发和设计的动力,定义了我们应该做什么以及做到什么程度,验证只是副产品。
完成的工作量和具体的测试(单元、功能、集成)有关,TDD中的单元测试制订接下来完成尽可能小的任务,要求我们遵循一些设计原则(KISS、SOLID),确保代码的可测试性和解耦性。
TDD迫使我们详细地考虑需求和设计,编写整洁而可行的代码,以及创建可执行的需求并频繁重构。

示例实战

接收同事的需求

你的同事正在开发一个远程控制轮船的程序,他已经完成了一部分工作,但是他休假去了, 现在任务交给了你。你的同事是一个TDD的践行者,你接下来要在他完成的工作基础上继续开发

下图是你同事设计的几个主要的类:

  • Ship:表示轮船,目前没有任何方法,继续要迭代的地方
  • Point:表示二维坐标
  • Direction:代表方向的枚举类以及左右转弯的方法
  • Planet:地球,轮船航行的地方,包含最大的航行坐标点以及分布了哪些障碍物设置
  • Location:轮船当前方位,包含所在的坐标以及当前的行驶方向,可以前进后退、进行航行

下面是你同事已经完成的测试验证方法,所以请你放心的改造下去

方向测试类

轮船行驶范围(地球)测试类

二维坐标测试类

方位(包含方向与二维坐标)测试类

控制方向和坐标参考系如下:

特别要注意的是地球是圆的,起点和终点是重叠的。例如:往x轴最远处前进将回到x=1的坐标点

开始迭代需求

上面看到同事之前写的代码所有测试经过都是绿灯,我们可以放心的进行后面的需求迭代了

故事迭代1

我们需要给定轮船的起始位置以及方向

因为每次控制轮船的提前肯定是要有一艘轮船,所以在每个测试方法前都应该需要一艘船,我们把它放在@BeforeEach注解的方法里

Ship默认只有无参的构造函数所以报错了,这也是正常的红-绿-重构阶段,接下来我们进入绿阶段

写一个测试方法测试下

故事迭代2

轮船可以前进或后退

在编写轮船可以前进后退的时候,我们是不是要编写各个方向的前进和后退测试方法呢?类似下面的

该测试方法测试的是轮船往北移动,Y轴的坐标将加1
实际上我们不应该这么去编写测试,原因有三:1,我们的测试方法的结果依赖了其它方法、类的内部工作原理(否则我们怎么知道y轴需要检测的是8);2,会导致编写多个测试方法,我们肯定要把东南西北各个方向相关的方法都写一遍;3,依赖的其它方法虽然经过测试,但是如果发生变化,我们需要找到调用这个方法的所有地方进行修改

这里我们可以巧妙利用已经存在的方位方法进行测试用例编写:

编写moveForward()方法,使其变绿

轮船前进的完成了,后退的功能非常相似,就不详细说明了

故事迭代3

轮船可以左转或右转

和故事迭代2有点类似,在之前辅助类Location中包含了左右转的方法,我们可以结合实现

故事迭代4

轮船接收一个指令然后完成相应的动作(比如:lrfb表示左转右转前进再后退)

每个命令都用一个字符表示:f表示前进、b表示后退、l表示左转、r表示右转
先编写一个测试用例,接收一个字符串,解析并调用相关的方法

完成 的步骤后进入 绿

测试下结果没问题

故事迭代5

轮船行驶的地球是圆的,所以可以从一边的尽头跨越到另外一边

目前为止,轮船的构造函数只有一个参数location,代表轮船的方位,现在需要引入一个planet参数代表行驶的范围(水域大小)

新增一个两个参数的Ship构造函数,使其变绿

我们发现其实之前有过ship单个参数的测试方法,而且还有一个初始化方位的方法,其实我们可以将其重构成每次测试前都初始化一艘带方位和行驶范围的轮船

刚才的测试方法就可以简化为

Ship的构造参数也只需要保留一个就可以了

接下来编写一个往东边再前进回到x轴坐标1的测试方法,因为我们初始化的时候船就是朝东的,所以不同重新设置方向了

跑下测试用例肯定报错的,我们期望的是1,但是实际是51,超出边界了

度假中的同事之前已经在location辅助类中完成了跨越边界的逻辑,所以我们只要重构下ship中的前后形式方法,带上边界范围即可

运行下ship的所有测试方法,全部绿灯,故事迭代完成

故事迭代6

每次移动需要进行障碍检测,遇到障碍的话停在原处报告遇到的障碍

地球上的水域不是畅通无阻的,有暗礁和陆地,所以我们得在行驶区域中加入障碍物,轮船行驶的时候先检查是否有有障碍物

我们首先在初始化的时候在行驶区域内加入障碍物

重构下初始化的方法

无需添加实现,测试就可以通过
接下来我们测试有障碍物的时候,轮船无法行驶,我们的设计是让接收指令的方法返回一个字符串,代表每条指令的执行情况

根据测试代码,障碍物和船形式的路线如下:(先往蓝色箭头指示方向,后往绿色箭头指示方向)

执行下肯定是报错的,因为我们之前逻辑不支持障碍物,接下来要重构下我们的业务代码:
第一个地方:调用位置帮助类的时候,前进后退的方法要加上障碍物信息

第二个地方:指令接收方法要判断每次执行的结果,拼接成字符串返回

运行下我们的测试类

至此我们的遥控轮船开发完成,同事度假回来了,后面的需求交给他吧,毕竟我们也成为了TDD的践行者,让他放心的迭代吧

PS:验收代码仓库 https://gitee.com/hzqiuxm/tdd-demo-lessons.git 的ship模块

测试驱动编程(1)

hzqiuxm阅读(2200)

java测试驱动编程简明教程

本篇教程基于junit5,不熟悉的人可以先去官网了解下:https://junit.org/junit5/ 当然比较推荐我个人整理的junit5的简明教程,可以帮你省下不少时间

测试驱动编程实战

我们的原始需求

开发一个“井”字游戏,双方在3X3的网格中画X和O,最先在水平、垂直、对角线上将自己3个标记连起来的玩家获胜

  • 初始用户故事:作为玩家,可以将棋子放在3X3棋盘上任何没有棋子的地方

故事验收:

  • 1 如果棋子放在超出X轴边界地方,引发RuntimeException;
  • 2 如果棋子放在超出Y轴边界地方,引发RuntimeException;
  • 3 如果棋子放在已经有棋子的地方,引发RuntimeException

开始红-绿-重构

  • 首先我们在业务模块创建一个井字游戏的实现类 JingGameService
  • 然后利用Idea工具创建对应的测试类(手动也可以,这里只是图个方便)

选择创建一个在每个测试方法之前都执行的方法

  • 添加一个初始化井字游戏实例的方法

  • 编写X轴越界的测试方法

注意,我们创建的井字游戏类还没有任何的方法,所以肯定会报错的,这个过程就是红-绿-重构过程中的阶段

  • 为了进入绿阶段我们实现play方法

  • 执行下该测试方法

我们看到测试通过了,这个过程就是红-绿-重构过程中的绿阶段

  • 我们还可以查看下Junit5自动生成的测试报告

  • 此时我们的代码比较简单也没有重构的必要,继续完成其他的验收测试

  • Y轴的异常逻辑和X的如同一辙,我们马上就可以实现


执行下整个类的测试,发现都是绿的,此时我们又经过了一次红-绿-重构的过程(重构没做?因为目前还不用嘛)

  • 好了,只剩下最后一个验收测试了:确定棋子在棋盘内,并且该位置是没有棋子的
  • 先完成位置冲突的测试用例

执行下,不出意外的报错了:

  • 接下来进入绿阶段,进行业务代码的编写

执行下整个测试类,全部通过:

  • 此时的代码其实已经可以重构了,因为我们的判断语句比较多,不能很好的表达业务含义,并且这些判断可能其它地方也会用到,应该抽取为不同的方法,用明确的含义表示出来该步骤的意图

下面是一个代码重构的参考:

重构完记得重新执行下测试类哦

PS:使用Idea重构很方便,只要二步
第一步:选中要重构的代码

第二步:起个方法名和选择需要的参数

  • 好了到目前为止,需求1的用户故事结束了

进入需求2

我们得加入处理玩家落子的规则,验收测试如下:

  • 白子先下
  • 如果上一次是白子,那么这次是黑子
  • 如果上一次是黑子,那么这次是白子

我们开始红-绿-重构的过程:
先编写测试方法,当白子先下,下一个玩家应该是白,我们用W表示白子,B表示黑子

的过程完成了,接下来是绿的过程,编写业务方法

我们只要编写一行代码就能实现了,然后继续完成白方黑方交替下的过程
编写白方完成后轮到黑方测试方法:

修改刚才的nextPalyer方法,将它变绿:


因为我们修改了nextPalyer方法,之前的用例也要重新跑一次,保证一直是绿的
那么我们需要编写黑方下完后应该是白方的逻辑吗?如果你写完测试方法,发现业务代码啥也不用动就能正确通过了,那么说明这个测试是多余的,我们应该删除掉
OK,我们又完成了第二个需求

进入需求3

终于进入该游戏的关键规则环节了:如何获胜?最先在水平、垂直、对角线上将自己3个标记连起来的玩家获胜
我们需要检查三个方向:水平,垂直,对角线,当没有分出胜负时,我们也要返回没有赢家,先来完成这个

看来我们又要重构/修改play方法了

之前我们是使用0或1来表示是否有棋子的,现在不仅要知道某个位置上是否有棋子,还要知道棋子是谁下的,所以保存的时候直接保存是谁下的



继续编写垂直方向的

写了水平的,垂直的也就简单了

最后是对角线三连,注意对角线有二种可能

因为都只有唯一的一条线,判断起来也更加简单了

最后所有的用例跑一次看看

完美,所有的用例都通过了,需求3也完成了

进入需求4

最后我们要处理下平局,毕竟这个游戏的大多数结局可能是平局
平局只有一种可能,就是棋盘满了,所以最后的验收测试是,当棋盘满了,游戏显示为平局

进入阶段

进入绿阶段


最后看下测试结果:

好了,一个超级简单的井字游戏就告一个段落了,代码层面还有一些优化的地方,比如获胜的判断这块。当然这不是本教程的主题,我主要演示的是如何通过测试驱动来进行业务需求/用户故事的开发

我们可以清晰的体会到,我们在编写的时候没有一开始就写业务代码,而是分析需求的验收测试,然后将验收测试编程Junit5的测试代码,当然这个过程中,代码肯定是会报错的,报错的原因有二类:

  • 编译错误:缺少必要的方法和参数
  • 运行错误:逻辑上的不满足

我们要做的事情就是把不通过的测试用例都变绿,如此反复进行。当我们发现代码臃肿难以阅读时,就应该及时的进行重构,我们可以放心的进行重构,因为有之前的测试为我们检验重构后的结果,让我们不用过于担心代码重构的不可控。

测试覆盖率

最后我们看下Junit5提供的测试覆盖率功能:

在Idea的右上角可以看到下图所示,类、方法、代码行的测试覆盖率情况,让我们做到心里有数,有多少代码或方法是测试过的

一些忠告

  • 测试方法的名称:使用given(前置条件,一般如果有公共的前置,方法中可省略)/when(描述操作)/then(描述期望的结果)
  • 红-绿-重构的过程:保证短节奏式的交替进行

最后的话

本文讲述的只是一个非常简单的测试驱动需求例子,现实情况中需求比较复杂。我们需要转变的是我们开发系统的思想,一开始可能你还不是很熟练,但是随着练习次数,你将会喜欢上这种方法。就像当初学习DDD的时候,上手比较困难,各种概念难以理解,但是一旦你上手后,你再也不想回到以前那种贫血失血模型的开发中去了。

如果你立志成为一个优秀的工程师,那么测试驱动编程是你一定要掌握好的技能。

PS:本教程的源码分享在了 https://gitee.com/hzqiuxm/tdd-demo-lessons.git 的jingame模块

OKR思考与实战

hzqiuxm阅读(3517)

OKR思考与实战笔记

本文主要来自对黄勇老师课程相关学习的总结和思考,以及自己在OKR实践过程中的一些经验与复盘,很早时候就拜读过黄勇老师架构探险系列书籍以及技术博文,一直十分推崇。

快速入门

为什么要学习OKR?

OKR主要有以下四个优势:

  • 1)专注:OKR使方向更加聚焦,专注解决核心问题,提倡要事第一原则;
  • 2)透明:OKR让沟通更加透明,增强彼此间的信任,建立共识,达成共赢;
  • 3)激励:OKR设定的目标挑战越大,成就感越大,对人的激励效果就越好;
  • 4)简单:OKR理解容易且用法简答,用数据和事实说话,结果容易度量;
    OKR不只是优秀的目标管理工具,也是高效的沟通框架,更是有效的激励手段,还是自我成长思维

看看OKR的流行趋势:

OKR除了推动战略落地、优化绩效管理,还有其它几个场景:管理变革项目、激发组织创新、强化组织文化、提升管理水平

什么是OKR?

OKR是一套科学的目标管理工作法,它由O和KR两部分组成,O表示Object(目标),KR表示Key Result

好OKR标准

遵循SMART基本原则:

O和KR分别遵循的标准:

KPI和OKR相爱相杀

下图是二者的区别:

OKR 以过程论成败,KPI 以结果论英雄
OKR 重视过程、关注成长、勇于挑战,而 KPI 关注结果、被动执行、利益挂钩,所以从员工可接受程度来看,似乎 OKR 更有“人情味”,更能体现人的价值

OKR不能做的事

目标是可以包含绩效的,但是千万不能把绩效和考核挂钩,更不要和奖金或薪资挂钩,否则OKR就会被你彻底玩坏了

比如,研发团队在交付项目时总是延期,你希望将“无延期上线”这项绩效指标放入 OKR,这样做是可以的,但你一定不要告诉团队“如果项目发生延期需扣工资,如果提前或准时上线可发奖金”,否则 OKR 就会变成一种利益驱动的工具,以后一旦缺乏利益,团队也就缺乏动力了,最终结果是 OKR 被“玩坏”了

适用场景

  • 推算型、机械化的工作,更加适合用KPI,这类工作一般有现成的指令、规定和方法可以遵循
  • 探索型、创造性的工作,更加适合OKR

相互结合

KPI 中包括的绩效指标一定是只看结果,而不看过程的,只要结果达到了就行,而 OKR 需要更多地关心过程,从结果中判断目标的具体达成情况。因此,我们需要打造一个“以 OKR 为主并以 KPI 为辅”的研发团队。

企业中的快速落地

使用四层目标金字塔:

我们在制定目标的时候,尽可能与上级目标保持对齐,而不是完全对齐
千万不要让HR去主导,否则很容易变成KPI2.0
企业和部门或小队实施起来难度是不一样的,请遵循下面三个步骤:

  1. 一定不要强制性地去推广 OKR,更不要在大家概念不一致的情况下就大规模推广 OKR。
  2. 当大家对 OKR 理解不一致时,一定要请外部 OKR 专家到企业内部做 OKR 培训,才能快速建立共识。
  3. 要充分发挥你的领导力去影响身边的人,让高管们对 OKR 表示认同,让团队成员们尝到 OKR 的甜头,这才是我们作为领导者需要去做的事情。

研发团队如何实施OKR?

首先要判断研发团队的内驱力如何?主要是个人成长和团队贡献两个方面

内驱力不够的话,建立先培养或激发出一定内驱力,或者先在内驱力强的几个人身上先试实行
先决条件1:部门应该对产品的设计、开发和交付的整个过程负责
先决条件2:公司的成败很大程度上取决于产品本身的特性和表现,对销售市场环节的依赖度有限

产品型互联网公司发展路径模型:

产品型公司的发展规律告诉我们,企业往往不是依赖销售目标去牵引,而是通过产品技术竞争力去推动的

  • 初创阶段:技术产品负责人要对企业希望达成的商业目标、目标客户选择,以及要解决的客户问题上,心知肚明;

    该阶段有意义的OKR建议:识别标杆客户、提升销售转化率、完善关键产品特性、消除量产缺陷、偿还技术债务

  • PMF后阶段:首要目标就是思考同倍的资源增加能否带来同倍的业绩增长

    该阶段OKR建议:打造中台系统、提供数据分析能力、赋能业务团队、提升运营效率

操作指南

打造OKR土壤

团队文化是否达标
  • 面对沟通时,是否公开透明?
  • 面对交付时,是否信守承诺?
  • 面对压力时,是否用于挑战?

在目标“制定”过程中,通过跟自己的上级、平级、下级沟通,从而实现目标对齐。在目标“执行”过程中,我们需要不断跟踪目标的进度,一旦有调整就需要及时沟通。在目标“评估”过程中,我们需要对目标进行评估和复盘,需要跟团队一起开会讨论,这又会产生更进一步的沟通。

既然制定了目标,那就一定要为之努力并争取最大化地实现目标,而不是一遇到困难就轻易放弃。这绝不是 OKR 所推崇的价值观。如果你发现你的团队在承诺上做得还不够,那么你现在首先要做的就是,建立起遵守诺言的团队文化。

团队内驱力是否达标?

内驱力不强的团队其实更加需要OKR来打造团队文化,OKR是领导和员工建立信任的纽带
好好利用团队1V1沟通方式,慢慢走近员工的内心
打造内驱力团队文化的三点实战心得:

  • 先管理好自己:自己先按照你要打造的团队文化标准去做
  • 给团队更多的空间:通过聊天和分享为团队搭建施展能力的舞台
  • 文化由团队说了算:你是教练,文化不需要贴在墙上,写在纸片上,它应该在每个团队成员的身上
正确与认清OKR使用
  • 不需要一上来就全员推行,要找到符合OKR的土壤才行
  • 作为领导者,只需要影响自己身边值得被影响的人

  • 制订OKR不是拿团队的KR作为自己的O,而是用自己的O去对其团队的O
  • 乐观的说一个公司的成功取决于大家的努力,悲观的说其实是少数人的努力

制定基本原则

有效使用O的三点
  • 方向明确:目标不能太抽象,也不能含混不清,要让每个人都能看得懂
  • 目标对齐:下级要对齐上级的目标,即向上对齐,平级还要相互对齐,即水平对齐
  • 时间限制:目标需要有时间边界,不能遥遥无期,而且时间边界要恰到好处
有效使用KR的三点
  • 有挑战性:关键结果要有挑战性,拒绝平庸,拒绝不切实际,要做到“跳一跳,就能够得着”。
  • 容易度量:无法度量的关键结果是没有任何意义的,宁可抛弃。
  • 不是任务:关键结果需要对目标产生直接影响,它不是日常任务,也不是行动计划
OKR三板斧
  • 制定 OKR:自顶向下地制定目标,自底向上对齐目标
  • 执行 OKR:唯有做到持续追踪、持续更新,才能做到拥抱变化
  • 评估 OKR:周期性地评估和复盘,持续改进、持续迭代

轻松定制OKR

在OKR制订的过程中一定要沟通和评审

如何制定O
  • O其实是做什么,尽量让O看起来有使命感
  • O的制订要注意满足三条核心原则:1 有实际价值,所以你要深度思考;2 能激励人心,所以你要善于表达;3 能短期实现,所以要脚踏实地
  • O尽量以动词开头,带上制订O的动机
如何制定KR
  • KR其实是怎么做层面的
  • KR是定性的,它可能是一个数字,一个范围,一个里程碑事件
  • KR要对当事人或部门有一定的挑战性,不能太低也不能太高
  • 用信心指数来表达KR的挑战性,初始值为5/10,表示完成和不能完成的概率是50%

推动OKR的技巧

OKR推行过程中常见问题
  • 很长时间大家都没有去更新
  • 没有完成OKR的理由:工作太忙,根本没时间
  • 完成的不好理由:制订时候对其的O,最后因为同级的O变了,导致自己的O失去意义或价值
  • 不能及时同步大家OKR的执行情况
OKR推行技巧
  • 每天更新,每周回顾,每月优化,每季复盘
  • 将OKR和日常工作关联起来,通过任务去驱动
  • OKR要有固定的周期

OKR的评估

  • 最好的OKR评估方式就是自评,而OKR评估也是自我管理的有效手段
  • 评价结果就分为四档:0、0.3、0.7、1
  • 一般 OKR 平均得分在 0.6~0.7 范围内,就会被认为完成得很好。如果得到 1,可能就需要反思是否目标设定得太低
    信心指数与评分的关系图:

OKR的复盘

  • 1 审视目标:当初定目标原因,完成的结果如何?为什么是这个结果?
  • 2 回顾过程:你是如何执行的?
  • 3 分析得失:哪些做得好?哪些做得不好?分别是什么原因?
  • 4 总结规律 :如果再次做相同的事情,你会怎么做?收获了哪些规律、方法论?

OKR实施需要一位教练角色

在OKR布道推广的时候,如果你还不是OKR专家时,你需要借力请一位OKR专家来帮助落地,善于借力也是领导力的表现之一
当你成为OKR教练时,你要注意在扮演教练角色时:1 学会引导;2 保持学习;3 不做权威

如何将OKR和"个人利益"挂钩

原则:OKR 不要与个人利益直接挂钩,但可以与个人利益间接关联

【OKR定位】
OKR解耦公司:1 将绩效作为目标,放到OKR中进行管理;2 通过OKR来实现个人成长与团队贡献;3 衡量员工的成长和贡献速度,来提升员工岗位级别;4 当岗位级别提升时,决定薪资的具体涨幅

上面的链条关系: 绩效--目标--成长/贡献--岗位级别--薪资
本职工作完成可以拿到工资,OKR完成的好进行升级加薪

【本职工作和OKR】
那么问题来了:本职过于饱和的情况下,哪有时间去完成OKR呢?升级加薪迎娶白富美岂不是东方夜谭?
OKR的分类:

  • 承诺型:是我们必须努力完成的,例子:项目及时上线
  • 愿景型:是我们需要挑战才能实现的,例子:提升开发效率,优化系统架构

建议:将本职工作放入承诺型OKR,然后将个人成长与团队愿景放入愿景型OKR,二者互相集合
在一个OKR周期中,必须要有一个愿景型OKR
优先完成承诺型OKR(重要且紧急的事),再去完成愿景型OKR(重要非紧急的事)
技术人员不要陷入生产代码陷阱中,否则生产的只是代码而不是价值

管理心经

改变研发是"成本中心"思想定位

有句话对技术的定位很准确:在一个公司中,创业初期技术的重要性往往被高估,后面稳定长期发展时技术的重要性又往往被低估
很多不懂技术的人都会有:技术部门属于成本中心,销售部门才是利润中心,我们想要改变这个认知,可以做以下几个方面的努力:

  • 让其他部门的核心人员了解一个工程开发的基本流程
  • 让其他人了解到软件开发是一个很复杂的体系,是一个工程性要求极强的工作
  • 支撑技术部门各岗位之间的协作,不仅要求大家有较强的逻辑思维能力,还要有一系列的技术规范和研发流程
  • 利用向大家同步OKR来体现技术团队的价值,同时工程师也要懂得自己代码是如何变现的过程

向上管理的套路

向上管理的本质就是上级对你的期望,所以一定要弄清楚这一点,请看下面这个常见的例子

  • 场景一

  • 场景二:

  • 场景三:

如果选择回答一,那么可能会给团队埋下工作效率低,士气下降的祸根
如果选择回答二,将影响老板对你的信任,他可能会说:如何提高团队效率,是你们团队负责人的事
如果选择回答三,那么老板一般都会说,其它部门我会去问下,你这边先开始

那么如何利用OKR的思维来进行向上管理呢?

面对老板996强制加班要求,一般人内心都是拒绝和不认同的,我们希望带领的是一支能“打仗”的队伍,该拼的时候一定要能拼,而不是为了加班而去加班,做样子给别人看,而且阿里等企业的成功,那绝不是单纯靠长期加班而取得的,而且人家有完善的加班体系和福利制度作保障,如果只要求马儿一样跑,但却没有相同的草,那就是耍流氓

所以我们要先弄清楚老板的O:我明白您的意思,那么您期望达到的目标是什么呢?
老板答案无外乎:项目早点上线、提升大家工作效率、有耕更多的产出,肯定不会有老板说:我就是想大家工作时间长一点

那么此时我们归纳出老板的O了,例如:成为一家工作效率高的公司

同时我们表态:是的,我们应该不断提高我们的工作效率,我有个想法,希望你能给我些建议;既然要关注工作效率,那么我们就应该用某个指标科学合理的去衡量工作效率,一般称之为 人效,我们每个部门可以先根据自己人员情况,计算下当前的人效,然后定一个目标,逐步来提高对大家的要求。既然是一家互联网公司,利用数据驱动的方式也更能体现我们文化和价值

OKR和项目开发结合

  • OKR比较适合迭代式的项目开发
  • 每次迭代的O尽量少,迭代周期一般为二周
  • 技术、产品、业务达成OKR共识
  • 迭代的时候开发将产品需求或用户故事拆分为多个任务

活学活用

招聘与考核

  • 与HR一起制定招聘的OKR,设置明确的时间期限和关键路径

  • 和候选人一起制订试用期OKR,让新人明确自己的目标是什么

人才激励

作者朋友圈有效激励小调查:

  • 第一:金钱激励最有效,但时效短
  • 第二:个人成长
  • 第三:认同感
  • 第四:其他福利

我们来了解下激励四象限:

  • 物质激励:金钱、福利
  • 精神激励:成长、认同、期权、荣誉、参与、特权、贡献、工作
    仅用物质激励显然不够持久,只用精神激励显然也不够刺激,物质激励与精神激励相结合,才能产生更强烈、更持久的激励效果

激励的三要素:

  • 1 及时性:激励越及时,激励效果越好
  • 2 多样性:激励方式越多,使用上灵活性也更强
  • 3 差异性:激励要有差异,也要有个性

CRF敏捷绩效管理

  • C:Conversation(对话):员工找管理者1V1的聊天(员工找管理者吃饭,管理者买单,反之亦然)

    对话环节注意点:1 在整个对话过程中,要围绕员工的“个人 OKR”进行引导,并有针对性地进行辅导;2 自始至终,你都在扮演一位“教练”的角色,你的目的是让“队员”产出高绩效;3 需要打造出“双向辅导”的交流氛围,你帮他成为更好的员工,他帮你成为更好的管理者。

  • R:Feedback(反馈):双向反馈,可以根据实际情况,选择实名或匿名

    反馈环节注意点: 1 当每季度 OKR 自评完毕后,即可进入到 CFR 反馈环节;2 反馈要做到尽可能具体,要能聚焦在某件事情上;3 不要依据反馈的结果,调整奖金多少与加薪幅度

  • F:Recognition(认可):一种激励方式,多鼓励员工之间进行相互认可

    认可环节注意点:1 认可要及时;2 认可要全面,阳光普照;3 认可要真实,认可具体的事情

两种OKR类型,评分要求:

OKR易踩的坑

  • 将OKR用于绩效考核
  • 用KPI思维做OKR
  • O数量太多,不够聚焦
  • KR不够量化,无法评估
  • 将KR当成任务或待办清单
  • KR挑战不够或不切实际
  • 忽略OKR的上下平级对齐
  • 忽略OKR的更新于复盘
  • 忽略OKR的评分环节
  • 用OKR替代了KPI

OKR十诫

  • 1 OKR 不与绩效考核相关,不与奖金或薪资直接挂钩。
  • 2 OKR 必须向上对齐,一起努力实现组织目标。
  • 3 OKR 是动态的,需要根据实际情况来灵活调整。
  • 4 OKR 是公开透明的,每个人都能看到所有人的 OKR。
  • 5 OKR 需要具备一定挑战,是通过努力才能达成的。
  • 6 OKR 要有评分,评分不是越高越好,越高说明挑战越不够。
  • 7 OKR 目的在于让事情变得聚焦,在正确的轨道上做出努力。
  • 8 OKR 重在沟通,通过沟通达到目标的理解一致性。
  • 9 OKR 是工作法,也是管理方法,它能激励团队取得成功。
  • 10 OKR 需做出承诺并付出行动,它是思想,也是文化

Docker简明教程(13)

hzqiuxm阅读(2183)

Docker网络管理

网络基础基础

  • 科普下常见的两种网络分层结构类型

一个是7层一个是4层,4层是对7的一个归纳和抽象,标准的模型是7层的

  • 常用查看网卡命令: ls /sys/class/net / 或者 ip a

  • 关于docker的几个网卡的简单介绍
    执行命令ip a

默认创建的容器都是在docker0这个network中,可以创建自己的network,指定容器运行在特定的network中

namespaceh和veth-pair技术

veth pair :Virtual Ethernet Pair,是一个成对的端口(veth-pair的link),可以实现两个namespace之间通信

观察容器中veth-pair对

  • 创建两个tomcat容器
docker run -d --name tomcat01 -p 8081:8080 tomcat
docker run -d --name tomcat02 -p 8082:8080 tomcat

查看宿主机网络情况:ip a

从上图中我们发现生成了两个veth-pair的link,但是它们其实不是一对,应为二者明显数字不是连续的

我们再看下两个tomcat容器的网路情况:docker exec -it tomcat01 ip a

其中eth0@if6和eth0@if7是一对veth-pair,eth0@if8和eth0@if9是一对veth-pair;它们都通过docker0来进行通信
我们使用类似命令 docker exec -it tomcat1 ping 172.17.0.3可以在tomcat01和tomcat02之间互相ping通

其原理示意图如下:

自定义网络配置

  • docker默认的网络配置方式 docker network ls

桥接 bridge(默认):虚拟机和宿主机通过桥接方式直连物理网络
主机host:通过共享宿主机ip通信去通信,和宿主机的网络配置显示是一模一样的
none:和外界完全不同的服务,特殊环境下使用

  • 创建自己的network: docker network create my-network

查看docker链接的各容器网络情况:docker network inspect bridge

可以把其它容器加入到多个network中实现之间的通信
自定义的 network中容器间可以通过名字直接访问(自动添加了DNS记录),默认的docker0是只能通过ip地址访问
如果是默认的docker0的network想通过容器名字直接访问,就要通过--link显性的指定要链接的容器
但是--link在生产环境中很少去使用,更加提倡自定义自己的bridge式network去管理容器

所以单机docker的通信最佳方案就是自己创建network,通过容器名称访问

跨network通信

当我们把类似的应用加入到不同的network中时,它们之属于不同的网段,肯定是不能互相通信的
比如新增一个tomcat3,指定在我们刚才创建的network下:docker run -d --name tomcat03 --network my-network -p 8083:8080 tomcat

观察发现tomcat3的网段是在172.18.0.2,而之前的是172.17.0.*,二者是不同的, 通过 docker exec -it tomcat03 ping 172.17.0.3测试是ping不通的

我们观察my-network的情况,也发现了该network中也只有一个容器

要想让tomcat3和tomcat1能互通该怎么办呢?一个想法就是把tomcat1加入到my-network中来
执行命令 docker network connect my-network tomcat01

omcat01已经被加入到了my-network当中,分到的IP是172.18.0.3,这个是时候我们不仅可以通过IP进行ping通,甚至可以根据容器名称来进行,大大提升了开发的灵活性

跨主机通信

  • 简单的方案就是通过端口映射来解决,通过外网,依赖DNS
  • 另外一个是通过vxlan技术实现,组建成内网,内部地址不能冲突,底层技术是overlay
    大致示意图如下:


这块内容涉及到比较专业的网络知识,详情待到后续补充新的文章再细说了

参考文献

docker官方文档: https://docs.docker.com/

DDD领域驱动战略篇(6)

hzqiuxm阅读(12956)

领域驱动架构篇—菱形对称架构

领域驱动设计中,对于架构风格有一个指导思想:不同的限界上下文,根据其领域模型和业务特征,可以选用不同的架构风格。
在传统的分层架构与领域驱动理念相结合的过程中,产生了多种架构风格:六边型架构、整洁架构、微服务架构等。本文是根据对对IT文艺工作者张逸老师的相关Chat阅读和思考整理而得

前言

菱形对称架构(Rhombic Symmetric Architecture) 主要针对领域层次的架构,借鉴了六边形架构、分层架构、整洁架构的知识,并结合了领域驱动设计的元模型,使其能够更好地运用到限界上下文的架构设计中。

架构演进之路

六边型架构

六边型架构的特点就是:在满足整洁架构思想的同时,更加关注内层与外层自己与外部资源之间的通信本质
对于外界每种类型都有一个适配器与之对应:消息适配器,REST适配器,SOAP适配器,持久化适配器
通过内外两个六边形的不同边界清晰地展现了了领域与技术的边界(蓝色是领域,灰色是技术)

  • 端口:我们可以把端口看成是具体的协议
  • 适配器:看成是具体的通信垂直技术,如:Servlet、REST、JPA
  • 应用:用例级别的业务逻辑,体现了一个业务场景
  • 领域模型:系统级别业务逻辑,多个服务和聚合构成一个用例

六边型架构中,端口是解耦的关键:入口隔离了外部请求和技术实现细节;出口隔离了数据持久化和外部访问设备

六边型架构的业务(预定机票)示例:

  • ReservationResource:订票请求发送给以 RESTful 契约定义的资源服务,它作为入口适配器,介于应用六边形与领域六边形的边界之内,在接收到以 JSON 格式传递的前端请求后,将其转换(反序列化)为入口端口ReservationAppService需要的请求对象
  • ReservationAppService:入口端口为应用服务,位于领域六边形的边界之上。当它在接收到入口适配器转换后的请求对象后,调用位于领域六边形边界内的领域服务TicketReservation
  • TicketReservation:订票的领域逻辑
  • ReservationRepository:出口端口为资源库,位于领域六边形的边界之上,定义为接口
  • ReservationRepositoryAdapter:真正访问数据库的逻辑则由介于应用六边形与领域六边形边界内的出口适配器,该实现访问了数据库,将端口发送过来的插入订票记录的请求转换为数据库能够接收的消息,执行插入操作

整洁架构

整洁架构的模型是一个类似内核模式的内外层结构,它具有以下特点:

  • 1 越靠近中心,层次越高
  • 2 由外到内:框架与驱动程序 --》接口适配器 --》应用级业务逻辑--》系统级业务逻辑
  • 3 源码中依赖关系必须指向同心圆内层,从外到内

我们可以在整洁架构上得到很多值得回味的设计思想:

  • 不同层次的组件其变化频率也不相同,引起变化的原因也不相同(符合高内聚低耦合设计思想)
  • 层次越靠内组件依赖越少,处于核心业务实体没有任何依赖(领域模型和技术完美解耦)
  • 层次越靠内组件与业务关系越密切,专属于特定领域的内容,很难形成通用的框架(领域构建出了壁垒)

在这个架构中,外层圆代表的是机制,内层圆代表的是策略;机制和具体的技术实现有关,容易受到外部环境变化;策略与业务有关,封装了最核心领域模型,最不容易受到外界环境变化的影响

六边形架构仅仅区分了内外边界,提炼了端口与适配器角色,并没有规划限界上下文内部各个层次与各个对象之间的关系;而整洁架构又过于通用,提炼的是企业系统架构设计的基本规则与主题。因此,当我们将六边形架构与整洁架构思想引入到领域驱动设计的限界上下文时,还需要引入分层架构给出更为细致的设计指导,即确定层、模块与角色构造型之间的关系。

经典分层架构

分层架构是运用最为广泛的架构模式,几乎每个软件系统都需要通过层(Layer)来隔离不同的关注点(Concern Point),以此应对不同需求的变化,使得这种变化可以独立进行。

  • 用户界面层:负责向用户展现信息和解释用户命令。包含web端UI界面、移动端UI界面、第三方服务等。
  • 应用层:很薄的一层,用来协调应用的活动,它不包含业务逻辑,它不保留业务对象的状态。在领域设计中,它其实是一个外观(Facade),一般是供其他界限上下文基础设置层中controller调用。
  • 领域层:本层包含领域的信息,是业务软件的核心所在。在这里保留业务对象的状态,对业务对象状态的持久化被委托给基础设置层。它包含领域设计中的:聚合、值对象、实体、服务、资源接口等。
  • 基础设置层:不要简单的理解为物理上的一层,它作为其他层的支撑库而存在。它提供了层间的通讯,实现对业务对象的持久化。一般包含:网络通讯、资源实现(数据库持久化实现)、异步消息服务、网关、controller控制等。

菱形对称架构

菱形对称架构融合了分层架构和六边型架构的思想

对六边型进行分层映射

  • 入口适配器:响应边界外客户端的请求,需要实现进程间通信以及消息的序列化和反序列化,这些功能皆与具体的通信技术有关,故而映射到基础设施层
  • 入口端口:负责协调外部客户端请求与内部应用程序之间的交互,恰好与应用层的协调能力相配,故而映射到应用层
  • 应用程序:承担了整个限界上下文的领域逻辑,包含了当前限界上下文的领域模型,毫无疑问,应该映射到领域层
  • 出口端口:作为一个抽象的接口,封装了对外部设备和数据库的访问,由于它会被应用程序调用,遵循整洁架构思想,也应该映射到领域层
  • 出口适配器:访问外部设备和数据库的真正实现,与具体的技术实现有关,映射到基础设施层

突破分层架构

分层架构仅仅是对限界上下文的逻辑划分,在编码实现时,逻辑层或许会以模块的形式表现,但是也可能将整个限界上下文作为一个模块,每个层不过是命名空间的差异,定义为模块内的一个包。不管是物理分离的模块,还是逻辑分离的包,只要能保证限界上下文在六边形边界的保护下能够维持内部结构的清晰,就能降低架构腐蚀的风险。

依据整洁架构遵循的“稳定依赖原则”,领域层不能依赖于外层。因此,出口端口只能放在领域层。事实上,领域驱动设计也是如此要求的,它在领域模型中定义了资源库(Repository),用于管理聚合的生命周期,同时,它也将作为抽象的访问外部数据库的出口端口。

将资源库放在领域层确有论据佐证,毕竟,在抹掉数据库技术的实现细节后,资源库的接口方法就是对聚合领域模型对象的管理,包括查询、修改、增加与删除行为,这些行为也可视为领域逻辑的一部分。

然而,限界上下文可能不仅限于访问数据库,还可能访问同样属于外部设备的文件、网络与消息队列。为了隔离领域模型与外部设备,同样需要为它们定义抽象的出口端口,这些出口端口该放在哪里呢?如果依然放在领域层,就很难自圆其说。例如,出口端口EventPublisher支持将事件消息发布到消息队列,要将这样的接口放在领域层,就显得不伦不类了。倘若不放在位于内部核心的领域层,就只能放在领域层外部,这又违背了整洁架构思想。

既然出口端口的位置如此尴尬,而且很明显出和入不太对称,所以我们干脆就把出和入对称下,将端口和适配器统一掉,组合成“网关”

上面的对称架构虽脱胎于六边形架构与领域驱动设计分层架构,却又有别于二者。
对称架构北向网关定义的远程网关与本地网关同时承担了端口与适配器的职责,这实际上改变了六边形架构端口-适配器的风格;领域层与南北网关层的内外分层结构,以及南向网关规定的端口与适配器的分离,又与领域驱动设计的分层架构渐行渐远。

既然已经改变,就根据思想,重新抽象下架构图

就得到了菱形对称架构,主要体现了南北网关的对称关系

菱形对称架构的组成

菱形架构其上下文的组成:

  • 北向网关的远程网关:进程间通信
  • 北向网关的本地网关:进程内通信
  • 领域层的领域模型:领域逻辑
  • 南向网关的端口抽象:各种资源库操作的抽象借接口,可以被领域层依赖,
  • 南向网关的适配器实现:网关端口的实现,运行时通过依赖注入将适配器实现注入到领域层

以六边型中的业务示例为例,改成菱形架构的话,其架构如下:

引入上下文映射

北向网关演变

北向网关和开放式主机服务很类似,但是职能更多,相当于开放式主机层,包含了远程服务和本地服务

  • 远程服务:跨进程通信;包含资源(Resource)服务、供应者(Provider)服务、控制器(Controller)服务与事件订阅者(Event Subscriber)服务
  • 本地服务:进程内通信;对应于应用层的应用服务

当外部请求从远程服务进入时,如果需要调用领域层的领域逻辑,则必须经由本地服务发起对领域层的请求。此时的本地服务又扮演了端口的作用,可认为远程服务是本地服务的客户端。

南向网关演变

南向网关引入了抽象的端口来隔离内部领域模型对外部环境的访问,这一价值很等同于上下文映射中的防腐层,同样它也扩大了防腐层的功能
南向网关的端口分为:

  • 资源库(repository)端口:隔离对外部数据库的访问,对应的适配器提供聚合的持久化能力
  • 客户端(client)端口:隔离对上游限界上下文或第三方服务的访问,对应的适配器提供对服务的调用能力
  • 事件发布者(event publisher)端口:隔离对外部消息队列的访问,对应的适配器提供发布事件消息的能力

改进后的菱形对称架构

菱形对称架构去掉了应用层和基础设施层的概念,以统一的网关层进行概括,并以北向与南向分别体现了来自不同方向的请求。如此形成的对称结构突出了领域模型的核心作用,更加清晰地体现了业务逻辑、技术功能与外部环境之间的边界。

资源库视为防腐层的端口与适配器,作为领域建模时的角色构造型,与场景驱动设计更好地结合,增强了领域模型的稳定性。应用层被去掉之后,被弱化为开放主机服务层的本地服务,相当于从设计层面回归到服务外观的本质,也有助于解决应用服务与领域服务之间的概念之争。

代码模型示例:

ohs 为开放主机服务模式的缩写,acl 是防腐层模式的缩写,pl 代表了发布语言;也可以使用北向(northbound)与南向(sourthbound)取代 ohs 与 acl 作为包名,使用消息(messages)契约取代 pl 的包名。

菱形对称架构价值体现

当我们为限界上下文引入菱形对称架构之后,一方面可以更加清晰地展现上下文映射模式之间的差异,并凸显了防腐层与开放主机服务的重要性;另一方面,遵循菱形对称架构的领域驱动架构亦具有更好的响应变化的能力。

展现上下文映射

以“查询订单”为例,若需求要求查询返回的订单需要包含商品的信息,这时可能产生订单上下文的订单与商品上下文的商品之间的“模型依赖”。

遵奉者或共享内核模式

一种方式是让订单直接重用商品上下文的领域模型,即采用遵奉者(Conformist) 模式或共享内核(Shared Kernel) 模式

重用领域模型的方式会突破菱形对称架构北向网关修筑的堡垒,让商品上下文的领域模型直接暴露在外

防腐层和开放式主机结合

如果订单上下文与商品上下文处于同一进程,根据菱形对称架构的定义,位于下游的订单上下文可以通过其南向网关发起对商品上下文北向网关中本地服务的调用。
为了保护领域模型,商品上下文在北向网关中还定义了消息契约模型

此时的菱形对称架构体现了防腐层模式与开放主机服务模式共同协作,位于商品上下文北向网关的本地服务还定义了消息契约,从而产生了消息契约模型之间的依赖。

如果订单上下文与商品上下文位于不同的进程,两个限界上下文之间的模型依赖就不存在了,我们需要各自为其定义属于自己的模型对象。

上图所示的订单上下文与商品上下文仅存在上下游调用关系,通过南向网关的防腐层与北向网关的开放主机服务亦降低了彼此的耦合;然而,在各自边界内定义的ProductResponse之间,却隐隐包含了逻辑概念的映射关系,它会带来变化的级联反应(一边改动后另一边也要改动)。

分离方式

若真正体会了限界上下文作为知识语境的业务边界特征,就可以将订单包含的商品信息视为订单上下文的领域模型,隐含的统一语言为“已购商品(Purchased Product)”,它与商品上下文的商品属于不同的领域概念,处于不同的业务边界,然后共享同一个productId

当用户只看订单及商品基本信息时,无需求助商品上下文,若要获取更多的商品信息时,可以通过productId向商品上下文发起请求,此时也和订单上下文无关,做到很好的隔离

这种方式真正展现了限界上下文的价值,即对领域模型的控制力。当限界上下文针对业务关注点进行垂直切割时,不仅要从语义相关性划分领域概念,还要考虑这些概念之间存在的功能相关性。

例如,商品与商品上下文是语义强相关的,但在查询订单这一领域场景中,获得订单时随之返回对应的商品信息,却是功能相关性发挥着作用。它也充分体现了“最小完备”的自治特性,因为在查询订单时,如果不为订单项提供对应的商品信息,该限界上下文就是不完备的。

当我们在定义领域模型时,如果一些领域概念出现矛盾或冲突时,就是引入限界上下文维护概念一致性的时机,也是统一语言发挥作用的时候。

响应变化的能力

限界上下文之间产生协作时,通过菱形对称架构可以更好地响应协作关系的变化。

客户方-供应方模式

通常说来,这种协作模式就是典型的客户方-供应方(Customer-Supplier) 模式,参与协作的角色包括

  • 下游限界上下文:防腐层的客户端端口(acl.ports.Client)作为适配上游服务的接口,客户端适配器(acl.adapters.ClientAdapter)封装对上游服务的调用逻辑
  • 上游限界上下文:开放主机服务的远程服务(ohs.remote.Resource)与本地服务(ohs.local.AppService)为下游限界上下文提供具有业务价值的服务

客户端适配器到底该调用上游限界上下文的远程服务还是本地服务,取决于这两个限界上下文的通信边界
如果上下游的限界上下文位于同一个进程边界内,客户端适配器可以直接调用本地服务。

如果上下游的限界上下文处于不同的进程边界,就由远程服务来响应下游客户端适配器发起的调用。

发布者和订阅者模式

采用这种模式时,限界上下文之间的耦合主要来自对事件的定义。作为事件发布者的限界上下文可以不用知道有哪些事件的订阅者,反之亦然,彼此之间的解耦往往通过引入事件总线(可以是本地的事件总线,也可以是单独进程的事件总线)来保证

在限界上下文内部,同样需要隔离领域模型与事件通信机制,这一工作由菱形对称架构网关层中的设计元素来完成。事件的发布者位于防腐层,发布者端口(acl.ports.EventPublisher)提供抽象定义,发布者适配器(acl.adapters.EventPublisherAdapter)负责将事件发布给事件总线;事件的订阅者(ohs.remote.EventSubscriber)属于开放主机服务层的远程服务,在订阅到事件之后,交由本地服务(ohs.local.ApplicationService)来处理事件。

应用事件:

领域事件:

一个订单提交的示例

业务场景如下:

  • 客户提交订单,向订单上下文发送提交订单的客户端请求
  • 订单上下文向库存上下文发送检查库存量的客户端请求
  • 库存上下文查询库存数据库,返回库存信息
  • 若库存量符合订单需求,则订单上下文访问订单数据库,插入订单数据
  • 订单上下文调用库存上下文的锁定库存量服务,对库存量进行锁定
  • 提交订单成功后,发布应用事件,并发布到事件总线
  • 通知上下文订阅应用事件,调用客户上下文获得该订单的客户信息,组装通知内容
  • 通知上下文调用短信服务,发送短信通知客户

识别的上下文如下:

菱形调用关系如下:

订单上下文内部

客户提交订单通过前端 UI 向订单上下文远程服务OrderController提交请求,然后将请求委派给本地服务OrderAppService

远程服务与本地服务使用的消息契约模型定义在ohs.local.pl包中,如此即可同时支持两种开放主机服务,这些消息契约模型都定义了如to()和from之类的转换方法,用于消息契约模型与领域模型之间的互相转换。

订单上下文与库存上下文协作

本地服务OrderAppService收到PlacingOrderRequest请求后,会将该请求对象转换为Order领域对象,然后通过领域服务OrderService提交订单。

提交订单时,首先需要验证订单有效,然后再检查库存量。验证订单的有效性由领域模型对象Order聚合根承担,库存量的检查则需要通过端口InventoryClient,并由注入的适配器InventoryClientAdapter发起向库存上下文远程服务InventoryResource的调用

位于南向网关的客户端端口InventoryClient及其适配器实现需要调用与之对应的消息契约模型,如CheckingInventoryRequest和InventoryReviewResponse。

根据前面的分析,如果上下游的限界上下文都处于同一个进程中,则下游上下文的南向网关可以重用上游上下文的消息契约。既然这里假定各个限界上下文都处于不同进程,就需要自行定义消息契约了

库存上下文协作

InventoryResource又会通过库存上下文的本地服务InventoryAppService调用领域服务InventoryService,然后经由端口InventoryRepository与适配器InventoryRepositoryAdapter访问库存数据库,获得库存量的检查结果

订单上下文发布应用事件

领域服务OrderService在确认了库存量满足订单需求后,通过端口OrderRepository以及适配器OrderRepositoryAdapter访问订单数据库,插入订单数据。

一旦订单插入成功,订单上下文的领域服务OrderService还要调用库存上下文的远程服务InventoryResource锁定库存量。订单上下文的本地服务OrderAppService会在OrderService成功提交订单之后,组装OrderPlacedEvent应用事件,并调用端口EventPublisher,由适配器EventPulbisherAdapter将该事件发布到事件总线

通知上下文订阅事件

通知上下文的远程服务EventSubsriber订阅了OrderPlacedEvent事件。一旦接收到该事件,就会通过本地服务NotificationAppService调用其领域层中的NotificationService领域服务

通知上下文和客户上下文协作

NotificationService领域服务会调用端口CustomerClient,经由适配器CustomerClientAdapter向客户上下文的远程服务CustomerResource发送调用请求。

在客户上下文内部,由北向南,依次通过远程服务CustomerResource、本地服务CustomerAppService、领域服务CustomerService和南向网关的端口CustomerRepository与适配器CustomerRepositoryClient完成对客户信息的查询,返回调用者需要的信息。

通知上下文的领域服务NotificationService在收到该响应消息后,组装领域对象Notification,再通过本地的端口SmsClient与适配器SmsClientAdapter,调用短信服务发送通知短信

小结

显然,若每个限界上下文都采用菱形对称架构,则代码结构是非常清晰的,各个层各个模块各个类都有着各自的职责,泾渭分明,共同协作。同时,网关层对领域层的保护是不遗余力的,没有让任何领域模型对象泄露在限界上下文边界之外,唯一带来的成本就是可能存在重复定义的消息契约对象,以及对应的转换逻辑实现。

DDD领域驱动战略篇(4)

hzqiuxm阅读(11999)

DDD领域驱动战略篇(4)

架构与代码模型

认识分层架构

分层架构是运用最为广泛的架构模式,几乎每个软件系统都需要通过层来隔离不同的关注点,以此应对不同需求的变化,使得这种变化可以独立进行

传统经典三层架构

DDD经典分层结构

  • 用户界面/展现层:负责向用户展现信息以及解释用户命令
  • 应用层:很薄的一层,用来协调应用的活动,它不包含业务逻辑,它不保留业务对象的状态,但它保有应用任务的进度状态
  • 领域层:本层包含关于领域的信息,这是业务软件的核心所在。在这里保留业务对象的状态,对业务对象和它们状态的持久化被委托给了基础设施层
  • 基础设施层 :本层作为其他层的支撑库存在。它提供了层间的通信,实现对业务对象的持久化,包含对用户界面层的支撑库等作用
分层的依据和原则
  • 基于关注点为不同的调用目的划分层次:以领域驱动设计的四层架构为例,之所以引入应用层就是为了给调用者提供完整的业务用例
  • 面对变化:针对不同的变化原因确定层次的边界,严禁层次之间互相干扰
  • 保证同一层的组件处于同一个抽象层次
层与层之间协作
  • 依赖倒置原则:我们要依赖不变或稳定的元素(类、模块或层),抽象不应该依赖于细节,细节应该依赖于抽象
  • 低层模块的细节实现可以独立变化,避免变化对高层模块产生污染
  • 在编译时,高层模块可以独立于低层模块单独存在
  • 对于高层模块而言,低层模块的实现是可替换的
  • 为了更好地解除高层对低层的依赖,我们往往需要将依赖倒置原则与依赖注入结合起来

分层架构的演化

整洁架构


该架构思想提出的模型并非传统的分层架构,而是类似于一个内核模式的内外层架构,由内及外分为四层

  • 层次越靠内的组件依赖的内容越少,处于核心的 Entities 没有任何依赖
  • 层次越靠内的组件与业务的关系越紧密,因而越不可能形成通用的框架
  • Entities 层封装了企业业务规则,准确地讲,它应该是一个面向业务的领域模型
  • Use Cases 层是打通内部业务与外部资源的一个通道,因而提供了输出端口(Output Port)与输入端口(Input Port),但它对外的接口展现的其实是应用逻辑,或者说是一个用例
  • Gateways、Controllers 与 Presenters 其本质都是适配器(Adapter),用于打通应用业务逻辑与外层的框架和驱动器,实现逻辑的适配以访问外部资源
  • 系统最外层包括框架和驱动器,负责对接外部资源,不属于系统(仅指限界上下文而言)开发的范畴,但选择这些框架和驱动器,是属于设计决策要考虑的内容。这一层的一些组件甚至与要设计的系统不处于同一个进程边界
六边型架构

六边形架构在满足整洁架构思想的同时,更关注于内层与外层以及与外部资源之间通信的本质:六边形架构通过内外两个六边形为系统建立了不同层次的边界。核心的内部六边形对应于领域驱动设计的应用层与领域层,外部六边形之外则为系统的外部资源,至于两个六边形之间的区域,均被 Cockburn 视为适配器(Adapter),并通过端口(Port)完成内外区域之间的通信与协作,故而六边形架构又被称为端口-适配器模式

下面是对六边型架构的一个落地实践示例:

六边形架构中,Gateways、Controllers 与 Presenters 统一看做是适配器
不同的命名代表的是对其职责认识上的不同,如果认为是“网关”,则将该组件的实现视为一种门面,内部负责多个对象之间的协作以及职责的委派;如果认为是“适配器”,则是为了解决内外协议(数据协议与服务接口)之间的不一致而进行的适配。

微服务架构

下图的逻辑边界代表了一个微服务

该架构图并未严格按照分层架构模式来约定各个组件的位置与职责,这是完全合理的设计
整幅图的架构其实蕴含了两个方向:自顶向下和由内至外

  • Resource(REST服务中资源层的意思) 组件就是我们通常定义的 Controller,对应于上下文映射中的开放主机服务
  • 当微服务需要调用外部服务,且外部服务由 HTTP 协议通信,就需要提供一个 HTTP Client 组件完成对外部服务的调用
  • 为了避免当前微服务对外部服务的强依赖,又或者对客户端的强依赖,需要引入 Gateways 来隔离,充当防腐层

领域驱动架构的演进

改良的DDD经典分层

将基础设施层奇怪地放在了整个架构的最上面

整个架构模型清晰地表达了领域层别无依赖的特质,但整个架构却容易给人以一种错乱感
这个架构模型仍然没有解决人们对分层架构的认知错误,例如它并没有很好地表达依赖倒置原则与依赖注入
这个架构模型将基础设施层放在了整个分层架构的最顶端,导致它依赖了用户展现层
那如何继续演进呢?避免领域模型出现贫血模型,保证领域模型的纯粹性

避免贫血,保证纯粹

经典分层架构图:

将数据访问层中的Java Beans这种没有任何业务行为的对象称之为“贫血对象”,基于这样的贫血对象进行领域建模,得到的模型则被称之为“贫血模型”
贫血模型开发起来比较简单,但不具备丰富表达能力,无法有效应对重用与变化,无法展现业务特性

改进后的架构图:

目前,前后端分离已经是流行趋势,逐渐的用户表现层将彻底分离出去,形成一个完全松耦合的前端层
不管前端的展现方式如何,它的设计思想是面向调用者,而非面向领域。因此,我们在讨论领域驱动设计时,通常不会将前端设计纳入到领域驱动设计的范围

前端演化后架构:

这个分层架构展现了“离经叛道”的一面,因为基础设施层在这里出现了两次,但同时也充分说明了基础设施层的命名存在不足。当我们提及基础设施(Infrastructure)时,总还是会想当然地将其视为最基础的层。同时,这个架构也凸显了分层架构在表现力方面的缺陷。

引入应用层架构:

领域驱动分层架构中的应用层其实是一个外观,不包含业务逻辑,但对外它却提供了一个一致的体现业务用例的接口。一般它和用户故事一一对应。

基础设施层本质

出现的二个基础设施层是不是有点奇怪?从依赖关系看,处于领域层下端的基础设施层是通过实现抽象 Repository 接口导致的。二个基础设施层其实体现出的是南北网关的概念,我们结合整洁架构与六边型架构,可以得到下面的表达公式:

Controllers + Gateways + Presenters = Adapters = Infrastructure Layer

分层架构是一种架构模式,遵循了“关注点分离”原则。因此,在针对不同限界上下文进行分层架构设计时,还需要结合当前限界上下文的特点进行设计,合理分层,保证结构的清晰和简单。

层次职责与协作案例

案例业务描述

电商系统的下订单场景,在买家提交订单时,除了与订单直接有关的业务之外,还需要执行以下操作:

  • 订单数据的持久化:OrderRepository 提供插入订单功能。它属于支撑订单提交业务的基础功能,但将订单持久化到数据库的实现 OrderMapper 并不属于该业务范畴
  • 发送通知邮件:NotificationService 提供通知服务。它属于支撑通知业务的基础功能,但邮件发送的实现 EmailSender 却不属于该业务范畴
  • 异步发送消息给仓储系统:提交订单成功后需要异步发送消息 OrderConfirmed 给仓储系统,这一通信模式是通过消息队列来完成的。EventBus 发送 OrderConfirmed 事件属于支撑订单提交成功的基础功能,但发送该事件到 RabbitMQ 消息队列的 RabbitEventBus 则不属于该业务范畴

基本的架构如下:

  • 北向网关:OrderController提供RESTAPI服务,属于基础设施层
  • 南向网关:OrderMapper、EmailSender 和 RabbitEventBus分别提供了订单持久化,邮件发送,事件总线具体实现
  • 持久化的抽象方法属于领域逻辑,放在领域层;事件和消息通知抽象不属于领域逻辑,放在应用层
  • 领域层:包含 PlaceOrderService、Order、Notification、OrderConfirmed 与抽象的 OrderRepository,封装了纯粹的业务逻辑,不掺杂任何与业务无关的技术实现
  • 应用层:包含 OrderAppService 以及抽象的 EventBus 与 NotificationService,提供对外体现业务价值的统一接口,同时还包含了基础设施功能的抽象接口
  • 基础设施层:包含 OrderMapper、RabbitEventBus 与 EmailSender,为业务实现提供对应的技术功能支撑,但真正的基础设施访问则委派给系统边界之外的外部框架或驱动器

问题:在实现过程,我们会碰到组装邮件和组装订单确认事件的内容,假设这两个功能在该业务场景下是属于领域层的,那应用层怎么获取领域层生成的对象呢?

方案一

应用层直接调用领域层的方法获取
类目录结构:

类图关系:

方案二

将“上层对下层的调用”改为“下层对上层的通知”,领域层通过事件通知的方式告知应用层,相当于应用层是订阅者,领域层是发布者
事件发布接口定义在领域层,具体实现在应用层(这里的前提是:发送邮件与异步发送通知属于应用逻辑的一部分)
领域服务在下订单完成后,需要分别发布 NotificationComposed 与 OrderConfirmed 事件,应用层在调用业务逻辑时要先注册订阅事件

类目录结构:

类图关系:

方案三

重新分配 NotificationService 与 EventBus,将这两个抽象接口放到单独的一个名为 interfaces 的包中,这个 interfaces 包既不属于应用层,又不属于领域层
通过这样的职责分配后,业务逻辑发生了转移,发送邮件与异步发送通知的调用不再放到应用服务 OrderAppService 中,而是封装到了 PlaceOrderService 领域服务。
这里的前提是:发送邮件与异步发送通知属于业务逻辑的一部分

类目录结构:

类图关系:

限界上下文与架构

  • 架构范围:限界上下文体现的是一个垂直的架构边界,主要针对后端架构层次的垂直切分;不仅仅作用于领域层和应用层,它是架构设计而非仅仅是领域设计的关键因素
限界上下文的通信边界

划分时,限界上下文之间是否为进程边界隔离,直接影响架构设计,进程内与进程间在如下方面存在迥然不同的处理方式:通信、消息的序列化、资源管理、事务与一致性处理、部署。除此之外,通信边界的不同还影响了系统对各个组件(服务)的重用方式与共享方式

  • 进程内通信边界:通过模块和命名空间来划分设计,即使处于同一个进程的边界,我们仍需重视代码模型的边界划分

越容易重用,越容易耦合,进程间通信我们依然可以适当采取防腐层来进行设计,如下图所示:

项目上下文中定义通知服务的接口 NotificationService,并由 NotificationClient 去实现这个接口,它们扮演的就是防腐层的作用
倘若在未来需要将通知上下文分离为进程间的通信边界,这种变动将只会影响到防腐层的实现,作为 NotificationService 服务的调用者,并不会受到这一变化的影响

  • 进程间通信:一个限界上下文通过分布式的通信方式调用另一个限界上下文的方法,有两种风格:1 数据共享架构;2 零共享架构

数据库共享架构:即多个限界上下文共享同一个数据库
数据库共享架构也可能是一种“反模式”。当两个分处不同限界上下文的服务需要操作同一张数据表(这张表被称之为“共享表”)时,就传递了一个信号,即我们的设计可能出现了错误

零共享架构:将两个限界上下文共享的外部资源彻底斩断
彻底分离的限界上下文变得小而专,使得我们可以很好地安排遵循 2PTs 规则的小团队去治理它
然而,这种架构的复杂度也不可低估,数据一致性是棘手的问题,运维与监控的复杂度也随之而剧增

限界上下文对架构的影响

在进行架构设计时,其中最主要的视图就是逻辑视图和物理视图,这两种视图都需要考虑限界上下文以及它们之间的协作关系
在考虑逻辑视图时,我们会为限界上下文履行的职责所吸引,同时又需得关注它们之间的协作,此时,就该物理视图粉墨登场了

对于跨进程边界进行协作的限界上下文,建议为其绘制上下文映射,并通过六边形架构来确定二者之间的通信端口与通信协议
分布式调用需要考虑各种方案原则选择:CAP 原则的约束,最终一致性(BASE),TCC(Try-Confirm-Cancel)模式等

限界上下文与六边型及微服务
  • 一个限界上下文就是一个六边形,限界上下文之间的通信通过六边形的端口进行
  • 一个微服务就是一个六边形,微服务之间的协作就是限界上下文之间的协作

限界上下文即微服务:我们可以利用领域驱动设计对限界上下文的定义,以及根据前述识别限界上下文的方法来设计微服务
微服务即限界上下文:运用微服务设计原则,可以进一步甄别限界上下文的边界是否合理,对限界上下文进行进一步的演化
微服务即六边形:深刻体会微服务的“零共享架构”,并通过六边形架构来表达微服务
限界上下文即六边形:运用上下文映射来进一步探索六边形架构的端口与适配器角色
六边形即限界上下文:通过六边形架构的端口确定限界上下文之间的集成关系

电商购物示例

用例如下:

根据用例可以会的六个限界上下文:Product Context,Basket Context,Order Context,Inventory Context,Payment Context,Notification Context
如果这六个限界上下文之间采用跨进程通信,实际上就是六个微服务
采用客户方供应商开发模式,得到如下上下文映射图:

命令:是一个动作,是一个要求其他服务完成某些操作的请求,它会改变系统的状态,命令会要求响应
查询:是一个请求,查看是否发生了什么事。重要的是,查询操作没有副作用,它们不会改变系统的状态
事件:既是事实又是触发器,用通知的方式向外部表明发生了某些事

如果采用事件协作机制,协作过程如下:

1 Basket Context 发布 OrderRequested 事件,Order Context 订阅该事件,然后执行提交订单的流程
2 Order Context 验证订单,并发布 InventoryRequested 事件,要求验证订单中购买商品的数量是否满足库存要求
3 Inventory Context 订阅此事件并对商品库存进行检查,倘若检查通过,则发布 AvailabilityValidated 事件
4 Order Context 侦听到 AvailabilityValidated 事件后,验证通过,发布 OrderValidated 事件从而发起支付流程
5 Payment Context 响应 OrderValidated 事件,在支付成功后发布 PaymentProcessed 事件
6 Order Context 订阅 PaymentProcessed 事件,确认支付完成进而发布 OrderConfirmed 事件
7 Basket Context、Notification Context 与 Shipment Context 上下文都将订阅该事件。Basket Context 会清除购物篮,Notification Context 会发起对买家和卖家的通知,而 Shipment Context 会发起配送流程,在交付商品给买家后,发布 ShipmentDelivered 事件并被 Order Context 订阅

事件皆以“过去时态”命名,味着它是过去发生的且不可变更的数据,代表了某种动作的发生,并以事件的形式留下了足迹

DDD的代码模型

理解了限界上下文和分层架构的本质,要确认系统的代码模型自然也就水到渠成,不同项目可以使用不同的代码模型,同一个项目中代码模型应作为架构规范共同遵守

遵循DDD代码模型

  • application:对应了领域驱动设计的应用层,主要内容为该限界上下文中所有的应用服务
  • interfaces:对 gateways 中除 persistence 之外的抽象,包括访问除数据库之外其他外部资源的抽象接口,以及访问第三方服务或其他限界上下文服务的抽象接口。从分层架构的角度讲,interfaces 应该属于应用层,但在实践时,往往会遭遇领域层需要访问这些抽象接口的情形,单独分离 出 interfaces,非常有必要
  • domain:对应了领域驱动设计的领域层,可以将 repositories 单独分出来,目的是为了更好地体现它在基础设施层扮演的与外部资源打交道的网关语义
  • repositories:代表了领域驱动设计中战术设计阶段的资源库,皆为抽象类型。如果该限界上下文的资源库并不复杂,可以将 repositories 合并到 domain 中
  • infrastructure:对应了领域驱动设计的基础设施层,其下可以视外部资源的集成需求划分不同的包。其中,controllers 相对特殊,它属于对客户端提供接口的北向网关,等同于上下文映射中“开放主机服务(OHS)”的概念。如果为了凸显它的重要性,可以将 controllers 提升到与 application、domain、gateways 同等层次。我之所以将其放在 gateways 之下,还是想体现它的网关本质。persistence 对应了 repositories 抽象,至于其余网关,对应的则是 interfaces 下的抽象,包括消息队列以及与其他限界上下文交互的客户端。例如,通过 http 通信的客户端。其中,client 包下的实现类与 interfaces 下的对应接口组合起来,等同于上下文映射中“防腐层(ACL)”的概念

每个模块或包都是单一职责的设计,扮演着不同的角色,有的对应了分层架构的层,有的代表了领域驱动设计的设计要素,有的则是为了保证架构的松散耦合

进程间通讯代码模型

限界上下文的边界是进程间通信,则意味着每个限界上下文就是一个单独的部署单元,此即微服务的意义
架构的设计需要“恰如其分”,在不同的微服务中,各自的领域逻辑复杂程度亦不尽相同,故而不必严格遵循领域驱动设计的规范

有三种不同的领域建模模式,包括事务脚本(Transaction Script)、表模块(Table Module)或领域模型(Domain Model)
不同建模模式代码结构肯定有所不同,DDD主要关注领域模型模式(选择了事务脚本,领域模型就不一定要规避贫血模型,依赖注入也就未必成为必选项)

订单上下文和通知上下文例子:

由于限界上下文之间采用进程间通信,因此在 Notification Context 中,提供开放主机服务是必须的。倘若 NotificationController 以 RESTful 服务实现,则在 Order Context 发起对 RESTful 服务的调用属于基础设施的内容,因而必须定义 NotificationService 接口来隔离这种实现机制,使其符合整洁架构思想

进程内通讯代码模型

注意如何在代码模型中体现限界上下文的边界,更关键的则是要考虑两个处于相同进程中的限界上下文彼此之间该如何协作
下面是一些主要的考量因素:

  • 简单:在下游限界上下文的领域层直接实例化上游限界上下文的领域类
  • 解耦:在下游限界上下文的领域层通过上游限界上下文的接口和依赖注入进行调用
  • 迁移:在下游限界上下文中定义一个防腐层,而非直接调用
  • 清晰:要保证领域层代码的纯粹性,应该避免在当前限界上下文中依赖不属于自己的代码模型

如果确有迁移可能,且架构师需要追求一种纯粹的清晰架构,可以考虑在 interface 中定义自己的服务接口,然后在 gateway/client 中提供一个适配器,在实现该接口的同时,调用上游限界上下文的服务,无论这个服务是领域服务还是应用服务,甚至也可以是领域层的领域对象

订单上下文和通知上下文例子:

与进程间通信的唯一区别在于:NotificationClient 不再通过跨进程调用的方式发起对 RESTful 服务的调用,即使在 Notification Context 中定义了这样的开放主机服务。如上图所示,NotificationClient 直接通过实例化的方式调用了 Notification Context 应用层的 NotificationAppService。这是在 Order Context 中,唯一与 Notification Context 产生了依赖的地方

如果两个或多个限界上下文还存在共同代码,只能说明一点:那就是我们之前识别的限界上下文有问题!一旦提炼或发现了这个隐藏的限界上下文,就应该将它单列出来,与其他限界上下文享受相同的待遇,即处于代码模型的相同层次,然后再通过 interfaces 与 gateways/client 下的相关类配合完成限界上下文之间的协作即可

代码模型的架构决策

如果我们将限界上下文视为微服务,则该系统的架构风格就是微服务架构风格;果我们将上下文协作模式抽象为发布/订阅事件,则该系统的架构风格就是事件驱动架构风格;如果在限界上下文层面将查询与命令分为两种不同的实现模式,则该系统的架构风格就是命令查询职责分离(CQRS)架构风格

帮助设计元素的粒度和层次的C4模型:

  • 系统上下文:是最高的抽象层次,代表了能够提供价值的东西。一个系统由多个独立的容器构成。
  • 容器:是指一个在其内部可以执行组件或驻留数据的东西。作为整个系统的一部分,容器通常是可执行文件,但未必是各自独立的进程。从容器的角度理解一个软件系统的关键在于,任何容器间的通信可能都需要一个远程接口。(等同微服务概念)
  • 组件:可以想象成一个或多个类组成的逻辑群组。组件通常由多个类在更高层次的约束下组合而成。
  • 类:在一个面向对象的世界里,类是软件系统的最小结构单元

代码模型中公共组件分为两种:1 具有业务价值的作为独立的限界上下文(如规则引擎、消息验证、分析算法);2 非领域一部分,作为公共的基础设施功能,以独立项目依赖包方式进行设计(文件读写、FTP 传输、Telnet 通信等)

一个典型微服务架构:

优先考虑设计成微服务的前提条件:

  • 实现经常变更,导致功能需要频繁升级
  • 采用了完全不一样的技术栈
  • 对高并发与低延迟敏感,需要单独进行水平扩展
  • 是一种端对端的垂直业务体现(即需要与外部环境或资源协作)

单体架构提供一些与业务无关的 RESTful 服务,如健康检查、监控等时,可以采用一个统一RESTful组个多个限界上下文,如下图所示:

支持多终端展示的用户前端方案:

图中为浏览器 UI 调用提供的 UI Layer,即 BFF,它实则是在服务器与浏览器之间增加了一个 Node.js 中间层。各层的职责如下表所示

BFF(Backends For Frontends,为前端提供的后端) 的设计并不在领域驱动战略设计的考虑范围之内

系统架构层面需要的基础设施层方案思考,例如对 Excel 或 CSV 文件的导入导出,消息的发布与订阅、Telnet 通信等。理想状态下,这些公共组件的调用应由属于限界上下文自己的基础设施实现调用。倘若它们被限界上下文的领域对象或应用服务直接调用(即绕开自身的基础设施层),则应该遵循整洁架构思想,在系统架构层引入 interfaces 包,为这些具体实现定义抽象接口。

一个单体架构代码模型示意图:

如果选择微服务架构风格,通常不需要建立一个大一统的代码模型,而是按照内聚的职责将这些职责分别封装到各自的限界上下文中,又或者定义为公共组件以二进制依赖的方式被微服务调用。这些公共组件应该各自构建为单独的包,保证重用和变化的粒度。

如果选择 CQRS 架构风格,就可以在限界上下文的代码模型中为 command 和 query 分别建立 module(领域驱动设计中的设计要素),使得它们的代码模型可以独自演化,毕竟命令和查询的领域模型是完全不同的。

基于质量因素的考虑,我们甚至可以为同一个领域的 command 和 query 各自建立专有的限界上下文。在 command 上下文中,除了增加了 command 类和 event 类以及对应的 handler 之外,遵循前面讲述的限界上下文代码模型,而 query 上下文的领域模型就可以简化,例如直接运用事务脚本或表模块模式。

DDD领域驱动战略篇(3)

hzqiuxm阅读(9044)

DDD领域驱动战略篇(3)

上下文映射

理解上下文映射

上下文映射是为了用来描述限界上下文之间的协作问题,上下文映射是一种设计手段包含:共享内核、防腐层、开放式主机等多种协作模式
上下文映射是用于将限界上下文边界变得更清晰的重要工具。所以当我们正在为一些限界上下文的边界划分而左右为难时,不妨先放一放,在定下初步的限界上下文后,通过绘制上下文映射来检验,或许会有意外收获。

限界上下文之间关系是有方向的:

U 代表上游,D 代表下游,上游限界上下文作为下游限界上下文的功能支撑

用例图中的包含用例或扩展用例或许是一个不错的判断上下文协作关系的切入点,选择从包含或扩展关系切入,既可能确定了职责分离的逻辑边界,又可以确定协作关系的方向,这就是用例对领域驱动设计的价值所在了
上下文映射可以分为两大类:团队协作模式和通信集成模式

上下文映射的团队协作

领域驱动设计根据团队协作的方式与紧密程度,定义了五种团队协作模式

合作关系

合作关系:两个限界上下文同生共死的关系,合作越多依赖越多,耦合越严重,甚至出现双向依赖
不好的示例:图中出现了双向依赖,循环依赖,可谓是貌合神离,并没有真正的分开

解决之道:1 干脆合在一起;2 尽量减少一个方向上多余依赖;3 抽取双向依赖循环依赖原因,建立单独限界上下文(共享内核)

新引入的 Metadata 成为了其余限界上下文的上游,解除了 DataEngine 对 ReportEngine 的依赖,同样解除了 EntityEngine 以及 ReportDesigner 对 ReportEngine 的依赖

共享内核

共享内核是解除不必要依赖实现重用的重要手段,共享内核仍然属于领域的一部分,分离出来的共享内核属于上游团队的职责,因而需要处理好它与下游团队的协作
共享内核往往被用来解决合作关系引入的问题
要特别注意:共享内核可能是多个限界上下文共同的上游,每次修改都可能牵一发而动全身

客户方与供应方

正常情况下,这是团队合作中最为常见的合作模式,体现的是上游(供应方)与下游(客户方)的合作关系
面对多个下游时,如何排定不同领域需求的优先级,如何针对不同的领域需求建立统一的抽象,都是上游团队需要考虑的问题
采用持续集成为上下游建立集成测试和API测试等自动化测试的构建与发布,可以更好的规避风险

遵奉者

由上游团队来决定是响应还是拒绝下游团队提出的请求时,所谓的“遵奉者”模式就产生了
领域驱动设计提出的“限界上下文”实践,影响的不仅仅是设计决策与技术实现,还与企业文化、组织结构直接有关
遵奉者还有一层意思是下游限界上下文对上游限界上下文模型的追随
做出遵奉模型决策的前提是需要明确这两个上下文的统一语言是否存在一致性

分离方式

分离方式的合作模式就是指两个限界上下文之间没有哪怕一丁点儿的丝毫关系,就是无关系
这种“无关系”仍然是一种关系,而且是一种最好的关系
这种没有关系的关系似乎无足轻重,其实不然,它对改进设计质量以及团队组织都有较大帮助。两个毫无交流与协作关系的团队看似冷漠无情,然而,正是这种“无情”才能促进它们独立发展,彼此不受影响

上下文映射的通讯集成

防腐层ACL

防腐层其实是设计思想“间接”的一种体现,引入一个中间层,有效隔离限界上下文之间耦合
防腐层经常扮演:适配器、调停者、外观等角色(设计模式中常见几种结构型模式)
防腐层往往属于下游限界上下文,用以隔绝上游限界上下文可能发生的变化

对付遗留系统时,防腐层可谓首选利刃

开放主机服务OHS

开放主机服务就是上游服务用来吸引更多下游调用者的诱饵
设计开放主机服务,就是定义公开服务的协议,包括通信的方式、传递消息的格式(协议)。同时,也可视为是一种承诺,保证开放的服务不会轻易做出变化
因为开放主机服务一般位于上游,对应多个下游,所以不建议为每个下游都做一个防腐层,实践时候防腐层一般在下游

发布订阅事件

采用发布/订阅事件的方式可以在解耦合方面走得更远
当确定了消息中间件后,发布方与订阅方唯一存在的耦合点就是事件,准确地说,是事件持有的数据
一个电商购物流程发布订阅事件流程:


事件的发布和订阅并不一定是异步的或跨进程的

辨别限界上下文协作关系

首先我们需要确定是否存在关系,然后再确定是何种关系,最后再基于变化导致的影响来确定是否需要引入防腐层、开放主机服务等模式

通信边界对协作的影响

限界上下文的通信边界分为进程内边界与进程间边界,这种通信边界会直接影响到我们对上下文映射模式的选择

这里存在一个设计决策,即引入开放主机服务与防腐层是否必要?这就需要设计者权衡变化、代码重用、架构简单性的优先级

协作即依赖

从依赖的角度看,这种协作关系是因为一方需要“知道”另一方的知识(领域行为,领域模型,数据)
不要在领域建模过程中过多纠缠建模的细节,选择一个恰好合理的模型即可
从建模到设计,再从设计到编码开发,其实是一个迭代的过程,倘若在实现时确实发现模型存在瑕疵,再回过头来修改即可

领域行为产生的依赖

针对领域行为产生的依赖,我们可以通过抽象接口来解耦,通过引入防腐层(ACL)解除了对促销上下文的直接依赖
示例:根据促销策略计算订单的总价(上下文协作设计)

  • 订单上下文分别去调用客户上下文和促销上下文,客户侧调用1次,促销侧调用2次


  • 订单只调用促销上下文,由促销上下文再调用客户上下文,促销侧调用了2次

  • 可以对促销上下文的调用进一步进行封装,只调用1次,上下文协作图不变

我们应尽可能遵循“最小知识法则”,在保证职责合理分配的前提下,产生协作的限界上下文越少越好

领域模型产生的依赖

我们可以使用延迟加载来判断领域对象是否合理
不好的设计示例:在客户领域对象中添加了订单属性,正确姿势应该通过调用订单上下文的服务来获取

public class Customer { public List<SaleOrder> saleOrders() { // ... } }

在跨限界上下文消费领域模型的场景有二种解决方式(订单查询商品信息):

  • 第一种:采用遵奉者模式,重用(订单重用商品的领域模型)
  • 第二种:定义符合自己的领域模型,分离(订单定义自己和商品有关的领域模型)

在两个不同的限界上下文中为相同或相似的领域概念分别建立独立的领域模型为常见做法

销售和售后对商品的关注点其实是不同的,销售可能需要了解客户的性别、年龄与职业,以便于他更好地制定推销策略,售后支持则不必关心这些信息,只需要客户的住址与联系方式

推荐采用分离的方式进行领域模型设计,但需要注意以下几点:

  • 数据可以仍然存(持久化)在一处,领域模型仅仅是内存中的对象
  • 数据按照不同的业务边界分散存储,但它们之间用相同的 Identity 来保持关联
  • 数据虽然出现了冗余,但是导致它们产生变化的原因却不相同
数据产生的依赖

倘若严格遵循领域驱动设计,通常不会产生这种数据库层面的依赖
出于性能或其他原因的考虑,CQRS 模式就存在一个限界上下文去访问属于另外一个限界上下文边界的数据
没有绝对的理由,我们不要轻易做出这种数据依赖的妥协
数据分析用的数据库应该和业务数据库分离,通过对业务数据库的采集提供给数据分析使用

DDD领域驱动战略篇(2)

hzqiuxm阅读(7086)

DDD领域驱动战略篇(2)

领域知识

软件开发团队的沟通与协作

  • 组建好项目团队的第一件事:先识别问题域,进而为团队提炼达成共识的领域知识
  • 我们需要把需求看成一颗种子,技术人员要和领域专家一起共用培育
  • 在先启阶段,与提炼领域知识相关的活动有:

  • 每个活动是有顺序关系的,从上到下
  • 迭代开发阶段,针对迭代生命周期和用户故事生命周期可以开展不同形式的沟通与协作

  • 领域专家:介绍和解释该迭代需要完成的用户故事,包括用户故事的业务逻辑与验收标准
  • 开发人员:使用工时卡对用户故事进行评估,预估故事耗时
  • 项目经理:每天了解当前的迭代进度,并与产品负责人一起基于当前进度和迭代目标确定是否需要调整需求的优先级
  • 测试人员:迭代完成后需要召开演示会议进行功能演示,可以邀请实际客户参与
  • 用户故事指导着开发人员的开发、测试人员的测试,它是构成领域知识的最基本单元
  • 敏捷开发实践强调业务分析人员与测试人员共同编写验收测试的自动化测试脚本
  • 时刻牢记未经过测试用户故事价值为0

运用场景分析提炼领域知识

领域场景分析方法6W模型
  • 一种生动的方式是通过“场景”来展现领域逻辑
  • 组成场景的要素常常被称之为 6W 模型,即描写场景的过程必须包含 Who、What、Why、Where、When 与 How 这六个要素

首先需要识别参与该场景的用户角色(Who),通过分析该用户的特征与属性来辨别该角色在整个场景中参与的活动
这意味着我们需要明确业务功能(What),思考这一功能给该角色能够带来什么样的业务价值(Why)

  • 领域功能划分为三个层次,即业务价值(Why)、业务功能(What)和业务实现(How)
领域场景分析方法
  • 用例:每个用例都是系统中一个完整序列的事件(下图是一个下订单的用例示例)

转换成图形:

  • 用户故事:敏捷开发中的需求功能点,每次迭代由不同的用户故事组成,一个用户故事就是一个独立的业务场景
    用户故事的典型模板:

    不合格的用户故事:作为一名用户,我希望可以提供查询功能,以便于了解分配我的任务情况
    合格的用户故事:作为一名普通项目成员,我希望获取分配给自己的未完成任务,以便于跟踪自己的工作进度

  • 需求分析人员与测试人员结对编写用户故事,一个完整的用户故事必须是可测试的,有验收标准的
    注意:用户故事应该只受到业务规则与业务流程变化的影响,不要考虑任何UI操作

  • 测试驱动开发:测试先行,先写测试用例,然后将一个个测试用例跑通

建立统一语言

  • 获得统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程
  • 统一语言的体现:统一的领域术语,统一的领域行为描述
  • 在维护领域术语表时,一定需要给出对应的英文术语,否则可能直接影响到代码实现
  • 领域行为是对业务过程的描述,相对于领域术语而言,它体现了更加完整的业务需求以及复杂的业务规则
  • 领域行为注意点:强调动词的精确性,符合业务动作在该领域的合理性;要突出与领域行为有关的领域概念

磨刀不误砍柴工,多花一些时间去打磨统一语言,并非时间的浪费,相反还能改进领域模型乃至编码实现的质量,反过来,领域模型与实现的代码又能避免统一语言的“腐化”,保持语言的常新。重视统一语言,就能促成彼此正面影响的良性循环;否则领域模型与代码会因为沟通不明而泥足深陷,就真是得不偿失了。

限界上下文

理解限界上下文

限界上下文定义

什么是限界上下文(Bounded Context)?让我们来读一个句子:wǒ yǒu kuài dì
到底是:我有快递还是我有块地?如果没有说话的语境上下文,我们确定不了!当我们在理解系统的领域需求时,同样需要借助这样的上下文,而限界上下文的含义就是用一个清晰可见的边界(Bounded)将这个上下文勾勒出来,如此就能在自己的边界内维持领域模型的一致性与完整性。

在实际的业务场景中,不同的上下文,同一个人扮演的角色可能是不同的,如下图所示:

理解限界上下文时,要注意的几个关键点:

  • 知识:不同上下文领域知识不同,如果在一个上下文里某活动不具备对应知识,则活动分配不合理
  • 角色:深入思考参与到一个上下文中对象到底是什么角色,以及角色之间是如何协作的
  • 边界:按照不同关注点进行划分,越是关系弱,越是要划定边界
限界上下文价值
  • 领域逻辑层面:限界上下文确定了领域模型的业务边界,维护了模型的完整性与一致性,从而降低系统的业务复杂度
  • 团队合作层面:限界上下文确定了开发团队的工作边界,建立了团队之间的合作模式,避免团队之间的沟通变得混乱,从而降低系统的管理复杂度
  • 技术实现层面:限界上下文确定了系统架构的应用边界,保证了系统层和上下文领域层各自的一致性,建立了上下文之间的集成方式,从而降低系统的技术复杂度
  • 限界上下文本质:并不是像大多数程序员理解的那样,是模块、服务、组件或者子系统,而是你对领域模型、团队合作以及技术风险的控制
  • 醍醐灌顶:限界上下文目的不仅仅是为了划分边界,更是为了如何控制边界

限界上下文是“分而治之”架构原则的体现,我们引入它的目的其实为了控制(应对)软件的复杂度,它并非某种固定的设计单元,它可以成为系统、模块、服务或组件
理解限界上下文的三种境界:

  • 1 参悟之初:模块、服务或组件就是限界上下文(看山是山,看水是水)
  • 2 当有悟时:模块、服务或组件不是限界上下文(看山不是山,看水不是水)
  • 3 彻底悟透:模块、服务或组件仍然是限界上下文(看山仍然山,看水仍然是水)
限界上下文自治特点

  • 最小完备:根据业务价值的完整性进行设计,无需针对自己的信息去求助别的自治单元,这就避免了不必要的依赖关系
  • 稳定空间:减少外界变化对限界上下文内部的影响,符合OCP原则
  • 自我履行:自治单元自身决定要做什么,履行的职责一定是你掌握的知识范畴之内
  • 独立进化:减少限界上下文的变化对外界的影响,需要接口设计良好,符合标准规范,并在版本上考虑了兼容与演化

这四个要素又是高内聚低耦合思想的体现。我们需要根据业务关注点和技术关注点,尽可能将强相关性的内容放到同一个限界上下文中,同时降低限界上下文之间的耦合。对于整个系统架构而言,不同的限界上下文可以采用不同的架构风格与技术决策,而在每个限界上下文内部保持自己的技术独立性与一致性。

限界上下文控制力

限界上下文分离了业务边界

一个不好的示例:产品领域对象被多个子领域使用,但是每个子领域关心产品领域中的属性和行为却不相同,这将导致领域模型和数据模型耦合,违背了SRP原则,产品类变成了一个上帝类

正确的示例:虽然不同的限界上下文都存在相同的 Product 领域模型,但由于有了限界上下文作为边界,使得我们在理解领域模型时,是基于当前所在的上下文作为概念语境的。这样的设计既保证了限界上下文之间的松散耦合,又能够维持限界上下文各自领域模型的一致性,此时的限界上下文成为了保障领域模型不受污染的边界屏障。

限界上下文明确了工作边界

根据亚马逊公司提出的2PTs 规则,团队成员人数控制在 7~10 人左右比较合适

2PTs 规则自有其科学依据。如果我们将人与人之间的沟通视为一个“联结(link)”,则联结的数量遵守如下公式,其中 n 为团队的人数:

N(link) = n*(n-1)/2

联结的数量直接决定了沟通的成本,以 6 人团队来计算,联结的数量为 15;12人团队,则联结数陡增至 66;50人团队,联结数竟然达到了惊人的 1225

矩阵式组织结构,特性(项目)团队和组件(职能)团队

我们按照领域特性来组建团队,使得团队成员之间的沟通更加顺畅,至少针对一个领域而言,知识在整个特性团队都是共享的。二者的结合可以取长补短。

限界上下文封装了应用边界

从控制技术复杂度的角度来考虑技术实现,从而做出对系统质量属性的响应与承诺,通常体现在如下几个方面:

  • 高并发(临时性流量高峰的场景)
  • 功能重用(用户权限管理之类)
  • 实时性(大数据下的更新)
  • 第三方服务集成(支付,安全)
  • 遗留系统(整体作为一个上下文)

识别限界上下文

限界上下文的识别并不是一蹴而就的,需要演化和迭代。通过从业务边界到工作边界再到应用边界这三个层次抽丝剥茧,分别以不同的视角、不同的角色协作来运用对应的设计原则,会是一个可行的识别限界上下文的过程方法。

从业务边界识别
  • 对业务流程进行梳理,划分出业务场景
  • 从业务场景中识别业务活动(动宾结构)
  • 将业务活动根据语义相关性(包含同一个领域对象)和功能相关性(依赖关联关系)进行归类
  • 将识别出来的归类进行业务边界命名,命名的难易也是有效判断划分准确与否的一个标准
从工作边界识别
  • 根据团队人数,一般一个团队至多负责一个上下文
  • 根据预估工作量,当一个限界上下文工作量过大,就要考虑拆分了
  • “任劳任怨”的好团队也不是真正的好团队,边界内的要积极,边界外的要“抱有成见”
从应用边界识别
  • 关注系统的质量属性,不同质量属性要求的可以考虑划分成不同的限界上下文
  • 考虑重用和变化,可重用的部分作为独立的上下文,不同变化维度的划分为不同限界上下文

DDD领域驱动战略篇(1)

hzqiuxm阅读(8069)

DDD领域驱动战略篇(1)

前言

  • DDD可不是什么新玩意,它已经诞生十几年了,只是因为微服务流行的契机,焕发了第二春
  • DDD是一套软件工程方法(一种设计思想、一种开放的设计方法体系),微服务只是一种架构风格,二者关系主要体现在限界上下文
  • 推行DDD难的原因:1 技能门槛高;2 不经过时间推移无法彰显价值
  • DDD发展可能趋势:1 以 DDD 设计方法为基础的框架的出现,让微服务设计与领域建模变得更加容易,降低领域驱动设计的门槛;2 以函数式编程思想为基础的领域建模理念与事件驱动架构和响应式编程的结合,可能在低延迟高并发的项目中发挥作用
  • 更好进行领域驱动设计的前提就是解决团队成员协作与沟通
  • 树立属于自己的技术标签(DDD纳入个人标签规划)
  • 领域驱动设计能够带来的收获:1 使得你的设计思路能够更加清晰,设计过程更加规范;2 为你的产品建立一个核心而稳定的领域模型内核,有利于领域知识的传递与传承;3 与领域专家的合作,能够帮助团队建立一个沟通良好的团队组织,构建一致的架构体系; 4 善于处理系统架构的演进设计; 5 有助于提高团队成员的面向对象设计能力与架构设计能力; 领域驱动设计与微服务架构天生匹配,微服务可以遵循领域驱动设计的架构原则
  • DDD学习之路会很崎岖,但是怕什么真理无穷,进一寸就有一寸的欢喜

软件复杂度

领域驱动设计概览

  • 领域驱动设计是一套方法论,具有一定的开放性,可以使用用例(Use Case)、测试驱动开发(TDD)、用户故事(User Story)来帮助我们对领域建立模型;可以引入整洁架构思想及六边形架构,以帮助我们建立一个层次分明、结构清晰的系统架构;还可以引入函数式编程思想,利用纯函数与抽象代数结构的不变性以及函数的组合性来表达领域模型。
  • 领域驱动设计贯穿了整个软件开发的生命周期,包括对需求的分析、建模、架构、设计,甚至最终的编码实现,乃至对编码的测试与重构。

  • 战略设计阶段:1 确认问题域(限界上下文,核心域,子域,通用域);2 通过多种分层架构(六边型、微服务、整洁、CQRS等)隔离关注点(多种架构的原因是因为限界上下文之间物理边界分开后,架构就是针对具体某个限界上下文系统了)
  • 战术设计阶段:应对领域复杂性,识别出主要要素:值对象、实体、领域服务、领域事件、资源库、工厂、聚合、应用服务

  • 领域驱动演进的设计过程:战略设计会控制和分解战术设计的边界与粒度,战术设计则以实证角度验证领域模型的有效性、完整性与一致性,进而以演进的方式对之前的战略设计阶段进行迭代,从而形成一种螺旋式上升的迭代设计过程

深入分析软件复杂度

  • 复杂没有一个公认明确的定义,我们先从理解力(分为 Simple 与 Comlicated 两个层次)和预测能力(分为 Ordered、Complex 与 Chaotic混沌 三个层次)两个维度来分析

  • 软件系统的所谓“复杂”其实覆盖了 Complicated 与 Complex 两个方面
  • 影响理解力的因素:1 规模;2 结构;
  • 影响预测能力的因素:变化
  • 优雅的设计和拙劣的设计都会使结构变得复杂,前者是主动控制,后者是不可控制错误滋生,是技术债
  • 变化可能来自业务需求(业务和用户活动本身复杂),也可能来自质量属性(非功能性需求)
  • 软件设计者们就像走在高空钢缆的技巧挑战者,惊险地调整重心以维持行动的平衡。故而,变化之难,在于如何平衡

控制软件复杂度原则

  • 当然也是从模块、结构、变化三个主要因素去控制
  • 手段一:分而治之,控制规模

    Kiss原则,单一职责原则

  • 手段二:保持结构的清晰和一致

    整洁架构

  • 手段三:拥抱变化

    敏捷,快速迭代,可进化、可扩展、可定制

软件需求复杂性

  • 需求复杂度可以分为:技术复杂度(安全、高性能、高并发、高可用性等)和业务复杂度
  • 技术复杂度与业务复杂度并非完全独立,二者混合在一起产生的化合作用更让系统的复杂度变得不可预期,难以掌控
  • 二者变化维度、周期也不同,再加上团队规模和人员流动等因素,加剧了架构腐化和系统复杂性

  • 面临的典型问题:1 问题域庞大复杂,难以寻求解决方案(规模上问题);2 开发人员将业务逻辑的复杂度与技术实现的复杂度混淆在一起(结构上问题);3 随着需求的增长和变化,无法控制业务复杂度和技术复杂度(变化上问题)

DDD如何应对软件复杂性

隔离业务复杂度和技术复杂度
  • 确认业务逻辑和技术实现的边界,从而隔离各自的复杂度
  • 理想情况下,业务规则和技术实现应该是正交的
  • DDD通过分层架构和六边型架构来确保业务逻辑和技术实现的分离

  • 分层架构关注点分离原则:将业务逻辑关注点放在领域层,技术实现放在基础设施层

蓝色区域的内容与业务逻辑有关,灰色区域的内容与技术实现有关,二者泾渭分明,然后汇合在应用层。应用层确定了业务逻辑与技术实现的边界,通过直接依赖或者依赖注入(DI,Dependency Injection)的方式将二者结合起来

  • 六边型架构的内外分离:内部领域层为核心,技术实现以周边适配器方式出现

如果我们在领域层或应用层抽象了技术实现的接口,再通过依赖注入将控制的方向倒转,业务内核就会变得更加的稳定,不会因为技术选型或其他决策的变化而导致领域代码的修改。

数据库和缓存隔离访问例子:

注意:缓存抽象是放在应用层,实现放在基础设施层

限界上下文分而治之

上一小节中,缓存接口放在了应用层,从层次的职责来看,这样的设计是合理的,但它却使得系统的应用层变得更加臃肿,职责也变得不够单一了。这是分层架构与六边形架构的局限所在,因为这两种架构模式仅仅体现了软件系统的逻辑划分。

如果我们将缓存作为一个独立的上下文,它拥有自己的应用层、领域层、基础设置层,那么我们就可以将庞大的问题域划分为松散的耦合的多个小系统,即不同的限界上下文

XXXX外贸系统的研发中心系统问题域划分示例:

领域模型对领域知识的抽象
  • 领域模型是对业务需求的一种抽象,其表达了领域概念、领域规则以及领域概念之间的关系
  • 模型是封装,实现了对业务细节的隐藏;模型是抽象,提取了领域知识的共同特征,保留了面对变化时能够良好扩展的可能性

某项目管理系统领域模型建模示例:

Docker简明教程(12)

hzqiuxm阅读(2411)

基于vagrant和virtualbox虚拟机搭建docker环境

环境部分

vagrant下载和安装

  • 01 访问Vagrant官网:https://www.vagrantup.com/
  • 02 点击Download:Windows,MacOS,Linux等
  • 03 选择对应的版本
  • 04 傻瓜式安装
  • 05 命令行输入vagrant,测试是否安装成功

virtual box下载安装

  • 01 访问VirtualBox官网:https://www.virtualbox.org/
  • 02 选择左侧的“Downloads”
  • 03 选择对应的操作系统版本
  • 04 傻瓜式安装
  • 05 [win10中若出现]安装virtualbox快完成时立即回滚,并提示安装出现严重错误
    (1)打开服务
    (2)找到Device Install Service和Device Setup Manager,然后启动
    (3)再次尝试安装

CentOS安装

  • 01 创建centos7文件夹,并进入其中[目录全路径不要有中文字符]
  • 02 在此目录下打开cmd,运行vagrant init centos/7
    此时会在当前目录下生成Vagrantfile,同时指定使用的镜像为centos/7,关键是这个镜像在哪里,我已经提前准备好了,名称是virtualbox.box文件。大家可以前往百度网盘https://pan.baidu.com/s/1cIlUnjleWSDxVLx9cjsgxA 进行下载

  • 03 将virtualbox.box文件添加到vagrant管理的镜像中
    (1)下载网盘中的virtualbox.box文件
    (2)保存到磁盘的某个目录,比如D:\vm\virtualbox.box
    (3)添加镜像并起名叫centos/7(唯一):vagrant box add centos/7 D:\vm\virtualbox.box
    (4)vagrant box list 查看本地的box[这时候可以看到centos/7]

  • 04 centos/7镜像有了,根据Vagrantfile文件启动创建虚拟机
    来到centos7文件夹,在此目录打开cmd窗口,执行vagrant up[打开virtual box观察,可以发现centos7创建成功]

  • 05 以后大家操作虚拟机,还是要在centos文件夹打开cmd窗口操作
    vagrant halt 优雅关闭
    vagrant up 正常启动

  • 06 vagrant常用命令
    (1)vagrant ssh
    进入刚才创建的centos7中
    (2)vagrant status
    查看centos7的状态
    (3)vagrant halt
    停止/关闭centos7
    (4)vagrant destroy
    删除centos7
    (5)vagrant status
    查看当前vagrant创建的虚拟机
    (6)Vagrantfile中也可以写脚本命令,使得centos7更加丰富
    但是要注意,修改了Vagrantfile,要想使正常运行的centos7生效,必须使用vagrant reload

至此,使用vagrant+virtualbox搭建centos7完成,后面可以修改Vagrantfile对虚拟机进行相应配置

Vagrantfile 配置示例

# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
  # The most common configuration options are documented and commented below.
  # For a complete reference, please see the online documentation at
  # https://docs.vagrantup.com.

  # Every Vagrant development environment requires a box. You can search for
  # boxes at https://vagrantcloud.com/search.
  config.vm.box = "centos/7"

  # Disable automatic box update checking. If you disable this, then
  # boxes will only be checked for updates when the user runs
  # `vagrant box outdated`. This is not recommended.
  # config.vm.box_check_update = false

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine. In the example below,
  # accessing "localhost:8080" will access port 80 on the guest machine.
  # NOTE: This will enable public access to the opened port
  # config.vm.network "forwarded_port", guest: 80, host: 8080

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine and only allow access
  # via 127.0.0.1 to disable public access
  # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

  # Create a private network, which allows host-only access to the machine
  # using a specific IP.
  # config.vm.network "private_network", ip: "192.168.33.10"

  # Create a public network, which generally matched to bridged network.
  # Bridged networks make the machine appear as another physical device on
  # your network.
  config.vm.network "public_network"

  # Share an additional folder to the guest VM. The first argument is
  # the path on the host to the actual folder. The second argument is
  # the path on the guest to mount the folder. And the optional third
  # argument is a set of non-required options.
  # config.vm.synced_folder "../data", "/vagrant_data"

  # Provider-specific configuration so you can fine-tune various
  # backing providers for Vagrant. These expose provider-specific options.
  # Example for VirtualBox:
  #
  # config.vm.provider "virtualbox" do |vb|
  # # Display the VirtualBox GUI when booting the machine
  # vb.gui = true
  #
  # # Customize the amount of memory on the VM:
  # vb.memory = "1024"
  # end
     config.vm.provider "virtualbox" do |vb|
        vb.memory = "4000"
        vb.name= "first-centos7"
        vb.cpus= 2
    end
  #
  # View the documentation for the provider you are using for more
  # information on available options.

  # Enable provisioning with a shell script. Additional provisioners such as
  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
  # documentation for more information about their specific syntax and use.
  # config.vm.provision "shell", inline: <<-SHELL
  # apt-get update
  # apt-get install -y apache2
  # SHELL
end

虚拟机访问配置

  • 01 使用centos7的默认账号连接
    在centos文件夹下执行vagrant ssh-config
    关注:Hostname Port IdentityFile
    IP:127.0.0.1
    port:2222
    用户名:vagrant
    密码:vagrant
    文件:Identityfile指向的文件private-key

  • 02 使用root账户登录(推荐)
    vagrant ssh 进入到虚拟机中
    sudo -i
    vi /etc/ssh/sshd_config
    修改PasswordAuthentication yes
    passwd修改密码,比如abc123
    systemctl restart sshd
    使用账号root,密码abc123进行登录

至此,可以使用你熟悉的ssh工具来进行访问,推荐几款常用的工具:xshell,Terminus,securtCRT

box的打包分发()

  • 01 退出虚拟机:vagrant halt

  • 02 打包:vagrant package --output first-docker-centos7.box

  • 03 得到first-docker-centos7.box

  • 04 将first-docker-centos7.box添加到其他的vagrant环境中
    vagrant box add first-docker-centos7 first-docker-centos7.box

  • 05 得到Vagrantfile:vagrant init first-docker-centos7

  • 06 根据Vagrantfile启动虚拟机
    vagrant up [此时可以得到和之前一模一样的环境,但是网络要重新配置]

docker部分

docker基本安装

  • 01 用你喜欢的方式,连接进入centos7
  • 02 卸载之前的docker
sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
  • 03 安装必要的依赖
sudo yum install -y yum-utils \
    device-mapper-persistent-data \
    lvm2
  • 04 设置docker仓库
 sudo yum-config-manager \
      --add-repo \
      https://download.docker.com/linux/centos/docker-ce.repo

建议添加一个加速器:访问这个地址,使用自己的阿里云账号登录,查看菜单栏左下角,发现有一个镜像加速器:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

例如我的如下:

sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://yzcs6yua.mirror.aliyuncs.com"] } EOF

  • 05 安装docker
 sudo yum install -y docker-ce docker-ce-cli containerd.io
  • 06 启动docker : sudo systemctl start docker
  • 07 设置开机启动: sudo systemctl enable docker
  • 08 测试docker安装是否成功 sudo docker run hello-world

能看到打印出hello from Docker!就表示成功了

具体详情可以参考官网介绍:https://docs.docker.com/install/linux/docker-ce/centos/

欢迎加入极客江湖

进入江湖关于作者