码农戏码


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 在线工具

  • 搜索

架构与架构师2

发表于 2021-02-24
字数统计: 1.6k | 阅读时长 ≈ 5

最近闲了,看了几次李运华关于架构的视频,不尽再次反问架构是什么?架构师的职责是什么?

对于这两个问题,之前也总结过一篇《架构和架构师》,再结合他的专栏文章和视频,补充一下

架构

李运华给架构的定义:软件架构指软件系统的顶层结构,缩句成架构指结构,而结构的修饰语蕴含了太多东西,抽象不够直白

这个定义里面蕴含了作者介绍的系统和子系统、模块与组件、框架与架构三组常见的概念

系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。它的意思是“总体”“整体”或“联盟”

软件模块(Module)是一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。模块的接口表达了由该模块提供的功能和调用它时所需的元素。模块是可能分开被编写的单位。这使它们可再用和允许人员同时协作、编写及研究不同的模块。

软件组件定义为自包含的、可编程的、可重用的、与语言无关的软件单元,软件组件可以很容易被用于组装应用程序中。

从逻辑的角度来拆分系统后,得到的单元就是“模块”;从物理的角度来拆分系统后,得到的单元就是“组件”。

划分模块的主要目的是职责分离;划分组件的主要目的是单元复用。其实,“组件”的英文 component 也可翻译成中文的“零件”一词,“零件”更容易理解一些,“零件”是一个物理的概念,并且具备“独立且可替换”的特点。

软件框架(Software framework)通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品

软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述

这么多的概念,在不理解的情况下,最多能记忆一小时吧,其实就算是理解了,最多也就记忆一天。因为这些概念比较虚,离我们具体coding有点远,但学习新知识又都是从定义起始,定义不理解时,就实践,再回看定义,可架构太大了,短时间没能力也没条件去架构设计

而且这定义可是一位阿里P9级别多年经验总结归纳出来的,得行多少路,抽象了多少回,才有的认知,所以我也不打算靠记忆了,不过对于模块和组件的认知很独到

虽然架构定义众家纷说,但对于如何描述架构还是有共识的,那就是“4+1视图”,在《架构和架构师》也描述了,也就是说架构的确需要从各角度观察和考虑

想来还是喜欢ISO/IEC 42010:20072 中对架构有如下定义

The fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution. 

这里定义了架构的三要素:

  • 职责明确的模块或者组件
  • 组件间明确的关联关系
  • 约束和指导原则

即架构是一种结构,是由组件(Components)+ 组件之间的关系 + 指导原则组成的

在《code review》中也提了,万事要以降低代码复杂度为大计,先Review设计实现思路,然后Review设计模式,接着Review成形的骨干代码,最后Review完成的代码;到了架构设计更是得如此,落地到代码层面,也就是要解决代码要如何被组织的问题,以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度


架构师

架构说清楚了,那架构师呢?在《架构和架构师》中,也说了

【优秀程序员的价值,不在于其所掌握的几招屠龙之术,而是在细节中见真著】,那么架构师则不仅要有屠龙刀,还得有绣花针

但如何拥有屠龙刀和绣花针呢?架构师在国内,大多时候可能不是个岗位,而是个角色。大厂还有架构师一说,小厂难得有专职架构师,所以架构师职能还得多多取经大牛,学习一下大牛

架构师能力模型

这三部分好似是任何一个职业的三条腿,要像走向人生巅峰迎娶白富美,这三条腿都得硬,越往上走,尤其管理和业务

技术只是技术人的最基本敲门砖,初级阶段以技术为重,往上走时,重点就得向后偏移,不能一直安静地码代码

架构设计过程

这个过程,回顾最近几个系统设计的确是这样的

  1. 业务方提出一个业务,刚开始可能只是个目标,轮廓
  2. 与业务方、产品不停的交流,交流得越深入,需求就越明确
  3. 理解业务并明确需求后,划分模块,不管是传统画ER图,还是4色建模,找出实体以及他们的关系
  4. 模块确定后,就是再深入细节,模块内部的业务流程,模块之间的交互
  5. 最后整理,确定技术选型,输出设计方案

之后,在架构落地过程中,随着业务进化,不停地演化架构,这些像上面说的宏观面的屠龙刀,绣花针就体现在细节,有时细节决定成败,架构师需要去识别哪些细节会影响到架构,以防后面不停地打补丁

code review

发表于 2021-02-19
字数统计: 2.1k | 阅读时长 ≈ 7

也不知code review是从哪年开始流行的,我的职场经历从刚开始完全没有到1对1,再到团队式review

一、Review Meeting

优点:

  1. 团队内新技术/新观点的交流Meeting、项目开发思路、解决方案讨论,偏头脑风暴式;
  2. 各类项目都适合进行;

缺点:

  1. 依赖于主持者(项目owner)的素质、时间成本高(为会议上所有人时间的总和);
  2. 集体评审的代码行数有限;

二、Single Review

优点:

  1. 更偏重与具体的代码评审,人员分散参与,评审代码行数有保证;
  2. 时间自由,reviewer什么时候进行评审时间可自控;

缺点:

  1. 依赖reviewer的技术水平,代码提交合并前强审核,只适用于重要项目或核心模块;

why

为什么需要code review,其实在任何行业,基本都是大厂带给整个行为最佳实践,code review就是其中一种实践

The biggest thing that makes Google’s code so good is simple: Code Review.

At Google, no code, for any product, for any project, gets checked in until it gets a positive review.

code review的好处可以罗列出很多很多,设计、结构、编码方方面面

代码有这几种级别:1)可编译,2)可运行,3)可测试,4)可读,5)可维护,6)可重用。
通过自动化测试的代码只能达到第3)级,而通过Code Review的代码少会在第4)级甚至更高

Code Review主要是让你的代码可以更好的组织起来,有更易读,有更高的维护性,同时可以达到知识共享,找到bug只是其中的副产品

以我个人经验看,code review更多是技术及业务知识的分享,甚至可以相互结合,理论分享与code的结合

比如check list与最佳实践结合

how

code review有点类似TDD,但强于TDD,这儿的强于不是说功能性,而在于落地层面,只要大家坐一起,指点江山,就可以完成了,当然效果另说

怎么更好地落地code review呢?或者说code review需要review些什么?code?

每个团队都有各自的情况,所以并不是随便拿一份review check list对照就做好,至少侧重点不同

比如人家团队人员素质普遍高一些,那人家的checklist可能就少了些基础知识点;团队职责不同,checklist也可能会相应不同,基础架构的checklist肯定跟业务线的不一样,各个不同业务线的也不同,需要根据团队情况制定合适的checklist

极端情况,团队中无人能识别好代码,每次都是流水帐式看代码,那团队人员得流动一下了

如何code review,结合why谈谈一些点

不要挑毛病

这就是上面的图中显示的,尤其团队式review,一群坐在下面的人

  1. 命名不太好啦
  2. 空格太多了
  3. 方法太长了
  4. 编码没格式化啊
  5. 这循环用lambda一行解决问题
  6. 实现的不是产品要的
  7. 这设计有问题啊

对1~4点,这些纯粹是浪费时间,一个团队的时间是宝贵的,来review这些,极大的浪费

因此需要明确两点:

  • Code review 不应该承担发现代码错误的职责
  • Code review 不应该成为保证代码风格和编码标准的手段

【管理工具化、工具流程化】指导方针,这儿可以引入checkstyle工具,让团队统一code sytle,新人加入团队时的培训指南中,并加入到CI中,检查失败直接构建失败

再引入sonar识别常见质量问题和安全问题,这样提高code review的质量

第5点:这也很典型,从code review层面讲,这也不应该是code review的职责,但从知识分享角度讲,这的确是,怎么办呢?使用流还是经典的for循环最好,如果团队成员对同一段代码有不同的意见,那么开发人员应该如何进行修改,结束审阅,并将代码推送到生产中?

解决这个问题最好能有一套最佳实践标准,明确什么情况使用流式,什么情况使用传统方式,其实这很难,真这样搞最佳实践会成为一本谁也学不完的手册,那只能说“这要看情况”,未尝不可,但需要有前提,团队中需要有一名裁决者来决定最终方案,而不能陷入长时间的争论

好比service能不能跨业务调用dao,这也是无对错,需要是的团队的一致性和最初的决策方案,不必每次code review时无休争论

6~7两点,这是最坑的,浪费了开发时间,也对代码作者造成极大打击,为什么到此时才发现,所以需要在开始前就得对功能设计和架构设计进行review,不能只看结果,得看起始与过程

保证正确性

这是code review的前提条件,如上述的6、7两点,不应该出现,一个优秀的工程师需要能够独当一面,能够在系统角度实现局部的良好设计,通过合理的测试方法论验证结果。能够用合理的数据结构、算法实现功能

在技术驱动的团队里,即使需求很紧急,对于关键的功能,核心成员也会耐心地审视架构设计和实现方案,如何控制熵,如何用更合理的方式实现,如何考虑到未来的变化。
技术驱动的团队里,应该持续进行对设计的调整和代码的微小重构与改良,时刻在整个系统的设计和表现(performance)角度审视自己的工作。这也是“系统思考”的核心。
大部分的代码的熵增大难以控制,并不是因为没有好的架构,而是因为迭代中忽略了系统性的思考和审视,而是用局部的解决方案解决问题,在反复迭代后,复杂度过高导致控制熵变得异常困难。这是code review比较难解决的

分享

从上面所述,code review虽然能发现代码中的一些错误,但不应该是他的核心价值。正好在《DDD总结》中所述,“降低代码复杂度”是所有方法实践论的终极目标。降低复杂度、易于扩展是我们的目标。那么code review也应该是为实现这个目标的手段,因此code review需要去review设计的合理性(如实现方法,数据结构,设计模式,扩展性考虑等),是否存在大量重复代码等

如何达到这些呢?需要发挥团队力量,三个臭皮匠顶过一个诸葛亮,代码终究是需要人去看的,通过与他人的交流,去寻求最佳实践,交流前提就是去分享自我,包括设计思想和实现路径

小到与一个人分享,也就是一对一code reivew,这样让review的开发人员了解代码的设计和实现,即能得到别人的指导,又能传递自我,并且能互为backup,方便后期维护,减少项目风险

大到与团队分享,产生技术氛围,让好的知识、设计在团队中分享,实现整体团队的成长和整体效益最大化

也鉴于要去把代码与人分享,就更容易让大家写出更具可读性的代码,提高可维护性,随便也让别人发现除功能逻辑外的一些技术逻辑:比如数据库连接是否忘记关闭,线程池是否正确使用等等,也加强了checklist的广度和深度

when

什么时候code review,大多数时候都是在上线前才做这件事,但理论最佳时间应该在提测前,以防测试完成后,又要对代码做变动

在实践时,可以拿出专门时间进行,以错开迭代发布的紧张期


除了上述的方法论,team leader还要在如何更好地code review,让团队更有意愿地参与上花心思,让团队成为一个学习型组织,有工程师文化的组织

面向对象是什么

发表于 2021-02-08
字数统计: 3k | 阅读时长 ≈ 11

近两年设计了几个系统,不管是直接使用传统设计ER图,还是使用4C建模,但在做架构评审时,ER却都是重中之重,让人不得不深思,编程思想经过了一代代发展,为什么还在围绕ER,在远古时代,没有OO,没有DDD,但为什么延续至今的伟大软件也比比皆是

带着这个问题,需要回头看看,结构化编程为什么不行?面向对象因何而起,到底解决了什么问题?

《架构整洁之道》也特别介绍了面向对象编程,面向对象究竟是什么,大多从三大特性:封装、继承、抽象说起,但其实这三种特性并不是面向对象语言特有

结构化编程

提到结构化编程就自然想到其中的顺序结构:代码按照编写的顺序执行,选择结构: if/else,而循环结构: do/while

虽然这些对每个程序员都很熟悉,但其实在结构化编程之间还有非结构化编程,也就是goto语句时代,没有if else、while,一切都通过goto语句对程序控制,它可以让程序跑到任何地方执行,这样当代码规模变大之后,就几乎难以维护

编程是一项难度很大的活动。因为一个程序会包含非常多的细节,远超一个人的认知能力范围,任何一个细微的错误都会导致整个程序出现问题。因此需要将大问题拆分成小问题,逐步递归下去,这样,一个大问题就会被拆解成一系列高级函数的组合,而这些高级函数各自再拆分成一系列低一级函数,一步步拆分下去,每一个函数都需要按照结构化编程方式进行开发,这也是现在常被使用的模块功能分解开发方式

结构化编程中,各模块的依赖关系太强,不能有效隔离开来,一旦需求变动,就会牵一发而动全身,关联的模块由于依赖关系都得变动,那么组织大规模程序就不是它的强项

面向对象

正因为结构化编程的弊端,所以有了面向对象编程,可以更好的组织程序,相对结构局部性思维,我们有了更宏观视角:对象

封装

把一组相关联的数据和函数圈起来,使圈外的代码只能看见部分函数,数据则完全不可见;如类中的公共函数和私有成员变量

提取一下关键字:

  1. 数据,完全不可见
  2. 函数,只能看见
  3. 相关联

这些似乎就是我们追求的高内聚,也是常提的充血模型,如此看,在实践中最基本的封装都没有达成

到处是贫血模型,一个整体却分成两部分:满是大方法的上帝类service与只有getter和setter的model

service对外提供接口,model传输数据,数据库固化数据,哪有封装性,行为与数据割裂了

怎么才能做到一个高内聚的封装特性呢?

设计一个类,先要考虑其对象应该提供哪些行为。然后,我们根据这些行为提供对应的方法,最后才是考虑实现这些方法要有哪些字段

并且对于这些字段尽可能不提供getter 和 setter,尤其是 setter

暴露getter和setter,一是把实现细节暴露出来了;二是把数据当成了设计核心

方法的命名,体现的是你的意图,而不是具体怎么做

1
2
3
4
5
6
7
8
9
// 修改密码 
public void setPassword(final String password) {
this.password = password;
}

// 修改密码
public void changePassword(final String password) {
this.password = password;
}

把setter改成具体的业务方法名,把意图体现出来,将意图与实现分离开来,这是一个优秀设计必须要考虑的问题

构建一个内聚的单元,我们要减少这个单元对外的暴露,也就是定义中的【只能看到的函数】

这句话的第一层含义是减少内部实现细节的暴露,它还有第二层含义,减少对外暴露的接口

最小化接口暴露。也就是,每增加一个接口,你都要找到一个合适的理由。

总结:
基于行为进行封装,不要暴露实现细节,最小化接口暴露

继承

先看继承定义:

继承(英语:inheritance)是面向对象软件技术当中的一个概念。这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为

从定义看,继承就是为了复用,把一些公共代码放到父类,之后在实现子类时,可以少写一些代码,消除重复,代码复用

继承分为两类:实现继承与接口继承

1
2
3
Child object = new Child();

Parent object = new Child();

但有个设计原则:组合优于继承Composition-over-inheritance

为什么不推荐使用继承呢?

继承意味着强耦合,而高内聚低耦合才符合我们的道,但其实并不是说不能使用继承,对于行为需要使用组合,而数据还得使用继承

这样解释似乎不够形象,再进一步讲,继承也违背了《SOLID》中的OCP,继承虽然可以通过子类扩展新的行为,但因为子类可能直接依赖父类实现,导致一个变更可能会影响所有子类。也就是讲继承虽然能Open for extension,但很难做到Closed for modification

借用阿里大牛的示例:

有个游戏,基本规则就是玩家装备武器去攻击怪物

  • 玩家(Player)可以是战士(Fighter)、法师(Mage)、龙骑(Dragoon)
  • 怪物(Monster)可以是兽人(Orc)、精灵(Elf)、龙(Dragon),怪物有血量
  • 武器(Weapon)可以是剑(Sword)、法杖(Staff),武器有攻击力
  • 玩家可以装备一个武器,武器攻击可以是物理类型(0),火(1),冰(2)等,武器类型决定伤害类型
1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Player {
Weapon weapon
}
public class Fighter extends Player {}
public class Mage extends Player {}
public class Dragoon extends Player {}

public abstract class Weapon {
int damage;
int damageType; // 0 - physical, 1 - fire, 2 - ice etc.
}
public Sword extends Weapon {}
public Staff extends Weapon {}

攻击规则如下:

  • 兽人对物理攻击伤害减半
  • 精灵对魔法攻击伤害减半
  • 龙对物理和魔法攻击免疫,除非玩家是龙骑,则伤害加倍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Player {
public void attack(Monster monster) {
monster.receiveDamageBy(weapon, this);
}
}

public class Monster {
public void receiveDamageBy(Weapon weapon, Player player) {
this.health -= weapon.getDamage(); // 基础规则
}
}

public class Orc extends Monster {
@Override
public void receiveDamageBy(Weapon weapon, Player player) {
if (weapon.getDamageType() == 0) {
this.setHealth(this.getHealth() - weapon.getDamage() / 2); // Orc的物理防御规则
} else {
super.receiveDamageBy(weapon, player);
}
}
}

public class Dragon extends Monster {
@Override
public void receiveDamageBy(Weapon weapon, Player player) {
if (player instanceof Dragoon) {
this.setHealth(this.getHealth() - weapon.getDamage() * 2); // 龙骑伤害规则
}
// else no damage, 龙免疫力规则
}
}

如果此时,要增加一个武器类型:狙击枪,能够无视一切防御,此时需要修改

  1. Weapon,扩展狙击枪Gun
  2. Player和所有子类(是否能装备某个武器)
  3. Monster和所有子类(伤害计算逻辑)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Monster {
public void receiveDamageBy(Weapon weapon, Player player) {
this.health -= weapon.getDamage(); // 老的基础规则
if (Weapon instanceof Gun) { // 新的逻辑
this.setHealth(0);
}
}
}

public class Dragon extends Monster {
public void receiveDamageBy(Weapon weapon, Player player) {
if (Weapon instanceof Gun) { // 新的逻辑
super.receiveDamageBy(weapon, player);
}
// 老的逻辑省略
}
}

由此可见,增加一个规则,几乎链路上的所有类都得修改一遍,越往后业务越复杂,每一次业务需求变更基本要重写一次,这也是为什么建议尽量不要违背OCP,最核心的原因就是现有逻辑的变更可能会影响一些原有代码,导致一些无法预见的影响。这个风险只能通过完整的单元测试覆盖来保障,但在实际开发中很难保障UT的覆盖率

也由此可见继承的确不是代码复用的好方式

从设计原则角度看,继承不是好的复用方式;从语言特性看,也不是鼓励的做法。一是像Java,只能单继承,一旦被继承就再也无法被其他继承,而且java中有Variable Hiding的局限性

比如现在添加一个业务规则:

  • 战士只能装备剑
  • 法师只能装备法杖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class Fighter extends Player {
private Sword weapon;
}

@Test
public void testEquip() {
Fighter fighter = new Fighter("Hero");

Sword sword = new Sword("Sword", 10);
fighter.setWeapon(sword);

Staff staff = new Staff("Staff", 10);
fighter.setWeapon(staff);

assertThat(fighter.getWeapon()).isInstanceOf(Staff.class); // 错误了
}

其实只是修改了父类的weapon,并没有修改子类的;由此编程语言的强类型无法承载业务规则。

继承并不是复用的唯一方法,如ruby中有mixin机制

多态

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态

在上一讲,接口继承更多是多态特性

只使用封装和继承的编程方式,称之为基于对象编程,而只有把多态加进来,才能称之为面向对象编程,有了多态,才将基于对象与面向对象区分开;有了多态,软件设计才有了更大的弹性

多态虽好,但想要运用多态,需要构建出一个抽象,构建抽象需要找出不同事物的共同点,这也是最有挑战地方。在构建抽象上,接口扮演着重要角色:一接口将变的部分和不变部分隔离开来,接口是约定,约定是不变的,变化的是各自的实现;二接口是一个边界,系统模块间通信重要的就是通信协议,而接口就是通信协议的表达

1
2
3
ArrayList<> list = new ArrayList();

List<> list = new ArrayList();

二者之间的差别就在于变量的类型,是面向一个接口,还是面向一个具体的实现类;看似没什么意义,但在《SOLID》中可以发现,几乎所有原则都需要基于接口编程,才能达到目的

而这也就是多态的威力

就java这门语言,继承与多态相互依存,但对于其他语言并不是如此

总结

除了结构化编程和面向对象编程,现在还有函数式编程,然通过上面的阐述,回到开篇的问题,我应该是把编程语言与编程范式搞混了,像结构化编程、面向对象编程是一种编程范式,而具体的C、Java其实是编程语言,对于编程语言是年轻的,的确在很多伟大软件之后才诞生,但编程范式是一直存在的,面向对象范式并不是java之后才有

更不是C语言不能创造伟大软件,语言不过是工具,而最最重要的是思维方式,最近思考为什么TDD,DDD这些驱动式开发都很难,关键还是思维方式的转变

为什么都要看ER图呢,这里面又常被混淆的概念:数据模型与领域模型,下一篇再分解

Reference

《架构整洁之道》

《软件之美》

再议DDD分层

发表于 2021-01-22
字数统计: 520 | 阅读时长 ≈ 1

之前整理过《DDD分层》 以及《分层架构》

最近看网友讨论,整理一些有亮点的地方

现在分层架构+整洁架构似乎是个万金油组合了

之前DDD的标准分层结构:

右边传统分层,左边经过DIP改进型,两者有什么区别呢?

眼尖的人可以看出来,两者确实差了不少

线条1:application到infrastructure被反转了

线条2:这条线没有了,在MVC里面这线是常见的,applicaton与domain没分开,但DDD中这条线是不推荐的,就算在松散分层架构中也一般不使用,除非简单的CRUD项目

线条3:也被反转了,这其实类似CQRS中的Q部分


以上来源于群友的讨论,真的是世上无难事,只怕有心人;这点区别真没留意过

这图来源于阿里大牛殷浩之手,《阿里DDD四弹》中进行过总结,DTOAssembler放在了application层,有些不太合理

在《分层架构》中thrift的TService,为了不与controller重复,所以需要一个application service,此时thrift与controller可以有相同的业务请求

也就是说controller对外有多种输入,但对应用层来说都是同一个user case,如果放在应用层内转化,是不是应该层为了同一个use case要爆露多过方法

适配层做三件事:

  1. 协议适配(如采用controller,通过 @RequestMapping 注解和 JSON 序列化)
  2. 参数规则验证(如,不能为空、手机是数字并且11位、邮箱要有@之类简单验证)
  3. 为调用下层(应用层)转化输入(assembler)

如果说分4层的话:

  1. controller (assembler、转化)
  2. appliction
  3. domain
  4. repository(convertor、转化)

应用层是真正的业务入口,是很披的一层:

  1. 用来协调领域操作 这里一般看系统架构不一样会有所不同,主要再分为同步调用的方式,和步骤事件的方式。
  2. 关注点分离的操作(如日志、通知等)

application service编排业务,domain service编排领域

阿里DDD四弹拜读

发表于 2021-01-11
字数统计: 5.5k | 阅读时长 ≈ 19

阿里殷浩大牛写了DDD系统文章,现在已经更新到每四篇,有很多异于常规的地方,收获良多,总结一下

Domain primitive

对于DDD第一讲,作者介绍的Domain primitive,开始有些反感的,对DDD理论也深知一二,但从没听过有这概念,所以觉得作者是挂羊头卖狗肉的,但读完发现原来是对Value object的升华,牛人就是不一样

Domain Primitive的概念和命名来自于Dan Bergh Johnsson & Daniel Deogun的书 Secure by Design,特意找到了这本书的章节,https://livebook.manning.com/book/secure-by-design/chapter-5/1,有兴趣可以看看

对于DP,在《代码大全》中指出过类似概念:ADT

抽象数据类型ADT是指一些数据以及对这些数据所进行的操作的集合

关于使用ADT的建议:

  1. 把常见的底层数据类型创建为ADT并且使用这些ADT,而不再使用底层数据类型
  2. 把像文件这样的常用对象当成ADT
  3. 简单的事物也可以当做ADT:这样可以提高代码的自我说明能力,让代码更容易修改。
  4. 不要让ADT依赖于其存储介质

ADT就是业务上的最小类型,不要去使用编程语言提供的基础类型

在之前的DDD文章中,也指出很多时候的重构不过是大方法拆分成小方法,更SRP一些,其实也什么意义,DDD带来的好处是业务语义显现化,而DP就是一种手段

使用DP后代码遵循了 DRY 原则和单一性原则,作者从四个维度提出DP带来的好处:接口的清晰度(可阅读性)、数据验证和错误处理、业务逻辑代码的清晰度、和可测试性

在实际项目中碰到一个有意义的问题,我们通过OCR接受识别的增值税发票信息

之前的接口是receiveInvoice(String invoiceCode,String invoiceNo,String checkCode,…),接受OCR给的发票结构化信息

发票号码和发票代码是有业务语义,是业务的最小类型,invoiceCode可以从String升级为InvoiceCode

接口变成:receiveInvoice(InvoiceCode invoiceCode,InvoiceNo invoiceNo,CheckCode checkCode),把业务最小单元提取出来了,接口清晰度,业务语义也显现了。可有个有意思的地方,OCR会出错的,一个正常的发票号码是8位,但会被识别成9位,业务上不能是InvoiceNo,可得固化存储这个识别结果,因此这个入口不能过于语义,总不能来一个WrongInvoiceNo

这儿我可能有个误区,把DP作为有语义的数据验证工具类使用了,可DP应该是Value object的升华,得在domain层使用,参数校验还得用Validate

定义

Domain Primitive 是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object 。

  1. DP是一个传统意义上的Value Object,拥有Immutable的特性
  2. DP是一个完整的概念整体,拥有精准定义
  3. DP使用业务域中的原生语言
  4. DP可以是业务域的最小组成部分、也可以构建复杂组合

原则

  1. 将隐性的概念显性化(Make Implicit Concepts Explicit)
  2. 将隐性的上下文显性化(Make Implicit Context Explicit)
  3. 封装多对象行为(Encapsulate Multi-Object Behavior)

VS Value Object

在 Evans 的 DDD 蓝皮书中,Value Object 更多的是一个非 Entity 的值对象

在Vernon 的 DDD红皮书中,作者更多的关注了Value Object的Immutability、Equals方法、Factory方法等

Domain Primitive 是 Value Object 的进阶版,在原始 VO 的基础上要求每个 DP 拥有概念的整体,而不仅仅是值对象。在 VO 的 Immutable 基础上增加了 Validity 和行为。当然同样的要求无副作用(side-effect free)

VS DTO


应用架构

这一篇算是正常篇,很多文章都是这样的,也是以银行转账为例,但分析得更特彻

应用架构,意指软件系统中固定不变的代码结构、设计模式、规范和组件间的通信方式

一个好的架构应该需要实现以下几个目标:

  1. 独立于框架:架构不应该依赖某个外部的库或框架,不应该被框架的结构所束缚
  2. 独立于UI:前台展示的样式可能会随时发生变化
  3. 独立于底层数据源:无论使用什么数据库,软件架构不应该因不同的底层数据储存方式而产生巨大改变
  4. 独立于外部依赖:无论外部依赖如何变更、升级,业务的核心逻辑不应该随之而大幅变化
  5. 可测试:无论外部依赖什么样的数据库、硬件、UI或服务,业务的逻辑应该都能够快速被验证正确性

这是很多架构的目标,但想想,一个架构是这样,那还剩下什么,框架没了,数据库没了,对于习惯了CRUD的程序员,什么都没了,我们的架构为了什么。真的就是那个模型,一个软件之所以是这个软件的核心模型,也就是domain

DDD是一种设计范式,主张以领域模型为中心驱动整个软件的设计。在DDD中,业务分析和领域建模是软件开发的关键活动。它不关心软件的架构是怎样的。随着技术的发展,我们可能在新版本中更换软件的架构,但是只要业务没有变更,领域模型就是稳定的,无需改动。

事务脚本弊端

一、可维护性能差

可维护性 = 当依赖变化时,有多少代码需要随之改变

二、可拓展性差

可扩展性 = 做新需求或改逻辑时,需要新增/修改多少代码

三、可测试性能差

可测试性 = 运行每个测试用例所花费的时间 * 每个需求所需要增加的测试用例数量

破坏原则

一、单一原则

二、依赖反转原则

三、开放封闭原则

DDD

如果今天能够重新写这段代码,考虑到最终的依赖关系,我们可能先写Domain层的业务逻辑,然后再写Application层的组件编排,最后才写每个外部依赖的具体实现。这种架构思路和代码组织结构就叫做Domain-Driven Design(领域驱动设计,或DDD)。所以DDD不是一个特殊的架构设计,而是所有Transction Script代码经过合理重构后一定会抵达的终点

这段话一针见血,很多时候在讨论DDD时,只是学习战术,什么实体、值对象、聚合,可在做项目时,引入了这些器,其他还不是CRUD,谁真正想过domain,并且以此驱动开发

DDD架构能有效解决传统架构中问题:

  1. 高可维护性:当外部依赖变更时,内部代码只用变更跟外部对接的模块,其他业务逻辑不变
  2. 高可扩展性:做新功能时,绝大部分代码都能利用,仅需要增加核心业务逻辑即可
  3. 高可测试性:每个拆分出来的模块都符合单一性原则,绝大部分不依赖框架,可以快速的单元测试,做到100%覆盖
  4. 代码结构清晰:通过POM module可以解决模块间的依赖关系,所有外接模块都可以单独独立成jar包被复用。当团队形成规范后,可以快速的定位到相关代码

Repository模式

这一篇对repository有了更深的了解,之前对repository的认知和实践都太浮浅

  1. repository只能操作聚合根
  2. repository类似dao,当作dao使用
  3. repository是领域层,但从菱形架构中得知,保持领域层的纯洁,放到南向网关

对repository的认知和实践也就这些了,在实践时基本当成dao使用,当然也碰到了想不通的问题

第一点:数据加载与性能平衡问题

repository操作的对象是聚合根,因此加载一个聚合根,就得是一个完整的聚合根,可是有时我们只想加载部分数据,怎么办?
很多人指出依赖懒加载方式来解决,但也有人指出这是通过技术解决设计问题,我也迷茫,到底怎么办呢?写两个方法吧

1
2
3
findOrder(OrderId id);//获取完整的order聚合

findOrderWithoutItems(OrderId id);//只取order不取明细

特别的别扭吧

第二点:更新数据时,只从聚合根操作,那到了repository怎么知道具体操作哪个对象

可能又需要类似第一点,写多个方法了

这些都是很麻烦很现实的问题,为了domain纯洁性,为了DIP而特意加上不合格的repository,是不是更麻烦了呢?

很多时候,其实技术、框架、依赖三方都不怎么变,变得恰恰是domain,产品需求一日三变,难道我们努力的方向有问题?

repository价值

对于这个问题,就是为了DIP吧,作者又重新对比了DAO

DAO的核心价值是封装了拼接SQL、维护数据库连接、事务等琐碎的底层逻辑,让业务开发可以专注于写代码,但是在本质上,DAO的操作还是数据库操作,DAO的某个方法还是在直接操作数据库和数据模型,只是少写了部分代码

在Uncle Bob的《代码整洁之道》一书里,作者用了一个非常形象的描述:

  • 硬件(Hardware):指创造了之后不可(或者很难)变更的东西。数据库对于开发来说,就属于”硬件“,数据库选型后基本上后面不会再变,比如:用了MySQL就很难再改为MongoDB,改造成本过高。
  • 软件(Software):指创造了之后可以随时修改的东西。对于开发来说,业务代码应该追求做”软件“,因为业务流程、规则在不停的变化,我们的代码也应该能随时变化。
  • 固件(Firmware):即那些强烈依赖了硬件的软件。我们常见的是路由器里的固件或安卓的固件等等。固件的特点是对硬件做了抽象,但仅能适配某款硬件,不能通用。所以今天不存在所谓的通用安卓固件,而是每个手机都需要有自己的固件

从上面的描述我们能看出来,数据库在本质上属于“硬件”,DAO 在本质上属于“固件”,而我们自己的代码希望是属于“软件”。但是,固件有个非常不好的特性,那就是会传播,也就是说当一个软件强依赖了固件时,由于固件的限制,会导致软件也变得难以变更,最终让软件变得跟固件一样难以变更

比如我们使用的mybaties,有各种mapper,原来放在dao中,现在放在repository,换汤不换药

我们需要一个模式,能够隔离我们的软件(业务逻辑)和固件/硬件(DAO、DB),让我们的软件变得更加健壮,而这个就是Repository的核心价值

此刻,对菱形架构有点反思,是不是repository就应该是领域层,就是与外界数据来源的隔离,不关心具体是不是数据库,io,都是repository

作者把DTOAssembler放在了application层,是不是有点不太合理,至少不太符合分层架构,应该放在controller中呢?

从使用复杂度角度来看,区分了DO、Entity、DTO带来了代码量的膨胀(从1个变成了3+2+N个)。但是在实际复杂业务场景下,通过功能来区分模型带来的价值是功能性的单一和可测试、可预期,最终反而是逻辑复杂性的降低。

repository规范

传统Data Mapper(DAO)属于“固件”,和底层实现(DB、Cache、文件系统等)强绑定,如果直接使用会导致代码“固化”。所以为了在Repository的设计上体现出“软件”的特性,主要需要注意以下三点:

  1. 接口名称不应该使用底层实现的语法,insert,select,update,delete是sql语法
  2. 出入参不应该使用底层数据格式,操作的是Aggregate Root,避免底层实现逻辑渗透到业务代码中的强保障
  3. 应该避免所谓的“通用”repository模式

Change-Tracking 变更追踪

这是很多文章没提过的,这也解决了上面的第二点问题

对于第一点问题,也特意请教了作者,作者这样回复:

在业务系统里,最核心的目标就是要确保数据的一致性,而性能(包括2次数据库查询、序列化的成本)通常不是大问题。如果为了性能而牺牲一致性,就是捡了芝麻漏了西瓜,未来基本上必然会触发bug。

如果性能实在是瓶颈,说明你的设计出了问题,说明你的查询目标(主订单信息)和写入目标(主子订单集合)是不一致的。这个时候一个通常的建议是用CQRS的方式,Read侧读取的可能是另一个存储(可能是搜索、缓存等),然后写侧是用完整的Aggregate来做变更操作,然后通过消息或binlog同步的方式做读写数据同步。


领域层设计规范

这一讲,对领域层讲解得很充分,大牛就是大牛,DDD只是一个外衣,遮挡不了牛人内涵的韵美,这个系统如果不与DDD联系,就取名牛人叫你怎么写代码,也是相当优秀

一直以来我也认为DDD的基础是面向对象思想,我相对认为DDD与面向对象两者交集很大,重合度很高,结果作者这篇让我认知更深刻了,尤其以游戏为示例,让我更加佩服了,毕竟我在游戏业混了很久,一直自认写的代码还不错,也碰到文章指出的一些问题,可没再深入思考更优解决方案

继承

为什么Composition-over-inheritance?以前只知道继承是强耦合,我们的道是高内聚低耦合,所以不要多过使用继承。之前同事讲过行为要多Composition,但数据层面还得inheritance

作者从OCP角度再次解释这个问题

继承虽然可以通过子类扩展新的行为,但因为子类可能直接依赖父类的实现,导致一个变更可能会影响所有对象

继承虽然能Open for extension,但很难做到Closed for modification。所以今天解决OCP的主要方法是通过Composition-over-inheritance,即通过组合来做到扩展性,而不是通过继承

领域服务

作者讲了三种场景的领域服务

单对象策略型

这个示例,很有新意,实在的,没见过这样写的,有点不理解,特地请教了作者

为什么通过Double Dispatch来反转调用领域服务的方法

这里的问题就是你作为服务提供方,没办法保证Weapon入参一定是合法的。在这里依赖了另一个服务的提前校验,就说明Player没有做校验,那如果因为忘记或者bug没有提前校验呢?
在这里Entity的设计理念是要强保证一致性,这也是为什么要让服务通过参数注入

可能这是事务脚本思维的原因,先判断再执行,而作者的意思是执行本身就应该包含判断,是个整体,不能分两步

跨对象事务型

这个常见,领域服务就是这样来的

Player.attack(monster) 还是 Monster.receiveDamage(Weapon, Player)?放在领域服务就行了

通用组件型

这个平常大多被Utils类给取代了

但如果再增加一个跳跃能力Jumpable呢?一个跑步能力Runnable呢?如果Player可以Move和Jump,Monster可以Move和Run,怎么处理继承关系?要知道Java(以及绝大部分语言)是不支持多父类继承的,所以只能通过重复代码来实现

这个问题,最近在项目正好又碰到了,java继承没法处理,只能接口化处理

但在实体上都得实现相应接口,有些重复,对此也特地请教了作者:

MovementSystem可以共用,但这些实体类都实现Movable,也得重复实现,这个情况是不是没法避免

这个是很正常的,Movable本来就应该是说“我能够Move”,然后要Move就必须要有Position和Velocity。所以在Entity层面重复实现是必须的。只要是接口编程就必然需要这样(除非走mixin,但那个Java不支持)。没办法走Base类就是因为要避免继承,同时base类也没办法实现多父继承。
在有Mixin的语言里理论上是可以避免这种,但是Mixin有自己的问题,同时我们主流编程语言都没有mixin

OOP、ECS、DDD三者的比较:

  • 基于继承关系的OOP代码:OOP的代码最好写,也最容易理解,所有的规则代码都写在对象里,但是当领域规则变得越来越复杂时,其结构会限制它的发展。新的规则有可能会导致代码的整体重构。
  • 基于组件化的ECS代码:ECS代码有最高的灵活性、可复用性、及性能,但极具弱化了实体类的内聚,所有的业务逻辑都写在了服务里,会导致业务的一致性无法保障,对商业系统会有较大的影响。
  • 基于领域对象 + 领域服务的DDD架构:DDD的规则其实最复杂,同时要考虑到实体类的内聚和保证不变性(Invariants),也要考虑跨对象规则代码的归属,甚至要考虑到具体领域服务的调用方式,理解成本比较高。

实体类

在这个过程中,作者也随带说了些实体的规范

因为 Weapon 是实体类,但是Weapon能独立存在,Player不是聚合根,所以Player只能保存WeaponId,而不能直接指向Weapon;
这是对象之间关系的处理,都是这样推荐的,不要对象对联,使用ID关联。但聚合根可以直接对象关联

Entity只能保留自己的状态(或非聚合根的对象)。任何其他的对象,无论是否通过依赖注入的方式弄进来,都会破坏Entity的Invariance,并且还难以单测

对于为什么实体都特地加一个业务实体ID,之前学习有介绍:

身份标识(Identity,或简称为 ID)是实体对象的必要标志,换言之,没有身份标识的领域对象就不是实体。实体的身份标识就好像每个公民的身份证号,用以判断相同类型的不同对象是否代表同一个实体。身份标识除了帮助我们识别实体的同一性之外,主要的目的还是为了管理实体的生命周期。实体的状态是可以变更的,这意味着我们不能根据实体的属性值进行判断,如果没有唯一的身份标识,就无法跟踪实体的状态变更,也就无法正确地保证实体从创建、更改到消亡的生命过程。

一些实体只要求身份标识具有唯一性即可,如评论实体、博客实体或文章实体的身份标识,都可以使用自动增长的 Long 类型或者随机数与 UUID、GUID,这样的身份标识并没有任何业务含义。有些实体的身份标识则规定了一定的组合规则,例如公民实体、员工实体与订单实体的身份标识就不是随意生成的。遵循业务规则生成的身份标识体现了领域概念,例如公民实体的身份标识其实就是“身份证号”这一领域概念。定义规则的好处在于我们可以通过解析身份标识获取有用的领域信息,例如解析身份证号,可以直接获得该公民的部分基础信息,如籍贯、出生日期、性别等,解析订单号即可获知该订单的下单渠道、支付渠道、业务类型与下单日期等。

在设计实体的身份标识时,通常可以将身份标识的类型分为两个层次:通用类型与领域类型。通用类型提供了系统所需的各种生成唯一标识的类型,如基于规则的标识、基于随机数的标识、支持分布式环境唯一性的标识等。这些类型都将放在系统层代码模型的 domain 包中,可以作为整个系统的共享内核

总结

大牛写的文章就是深入浅出,丰富饱满,还是多拜读,多温故,不是因为DDD他们变得这么优秀,而是他们本身就很优秀,也许他们心中早有DDD了,只是没有DDD这个名字而已

希望有一天我也能写出这么好的文章

DDD战略战术

发表于 2020-11-15
字数统计: 1.7k | 阅读时长 ≈ 5

《DDD开篇总结》的前三篇已经阐述了几个内容

  1. DDD是什么
  2. 复杂系统的特征
  3. DDD如何应对复杂系统
  4. 模型概念
  5. 软件开发流程

但一般DDD资料中都会分为两部分讲述:战略和战术,所以按这两种分类,重新归纳整合一下

道

在讨论战略和战术前,先表述一下“道”

DDD是一种软件开发的方法论,任何方法论,都必须落实到“减少代码复杂度”

那么“道”是什么呢?

一直认为DDD的战略就是道,结果搞错了

软件开发的终极“道”就是“高内聚、低耦合”,它是任何有价值思想和方法的具象

如何才能达到这个终极道呢?

    1. DRY
    1. 分离关注点
      • 2.1. 业务和技术分离
      • 2.2. 业务和部署分离
      • 2.3. 变与不变分离
    1. 缩小依赖范围
    1. 向稳定方向依赖

战略

DDD战略主要包含统一语言和限界上下文

统一语言

在以往OO开发过程中,会经过OOA,再到OOD,复杂系统中,没有人能全方位了解系统并实现系统,术业有专工,专业人士干伟业事,这是正确的

但分工后,团队合作时沟通至关重要,,在整个系统开发过程中,是有一根主线的,那就是业务知识,业务知识在系统落地前经过层层传递,走样变形是常有的事,从开发人员经过多少次的返工情况就很清楚

如果解决这个问题呢?DDD引入了统一语言,把业务名词含义事先确定好,减少不必要的翻译过程,车同轨,书同文,行同伦

这也消除了业务与技术之间的重复,共同使用业务原语对话

代码就是文档,代码就是领域知识

1
2
userService.love(Jack, Rose) => Jack.love(Rose)
companyService.hire(company,employee) => Company.hire(employee)

界限上下文

界限上下文囊括了实现道的方方面面,如分离关注点,每个上下文围绕一个关注点,通过整洁架构让各层向稳定方向依赖,合理的划分界限,使各个上下文之间减小依赖

说白了界限上下文就是把一个大系统分而治之

界限上下文算是DDD中的核心知识点,但常被技术人员忽视,对于实用主义的程序员来讲,战术常常更吸引人,其实大到微服务,小到实体类,背后都渗透着上下文的概念

引入限界上下文的目的,不在于如何划分边界,而在于如何控制边界

Alberto Brandolini认为bounded context are a mean of safety(限界上下文意味着安全),safety的意思是being in control and no surprise,对限界上下文是可控制的,就意味着你的系统架构与组织结构都是可控的

显然,限界上下文并不是像大多数程序员理解的那样,是模块、服务、组件或子系统,而是你对领域模型、团队合作以及技术风险的控制

限界上下文是“分而治之”架构原则的体现,我们引入它的目的其实为了控制(应对)软件的复杂度,它并非某种固定的设计单元,我们不能说它就是模块、服务或组件,而是通过它来帮助我们做出高内聚低耦合的设计。只要遵循了这个设计,则限界上下文就可能成为模块、服务或组件。所以,文章《Bounded Contexts as a Strategic Pattern Beyond DDD》才会写到:“限界上下文体现的是高层的抽象机制,它并非编程语言或框架的产出工件,而是体现了人们对领域思考的本质。”

战术

对于开发人员而,战术是最实用的,比如聚合、实体、值对象、工厂、仓储、领域事件等等,
使用这些战术组件建模工具,DDD满足了软件真正的技术需求。这些战术设计工具使开发人员能够按照领域专家的思维开发软件

战略部分讲了,界限上下文的思想是核心,在战术组件中都有体现,比如实体,实体就是一个最小上下文,聚合就是相对实体大一点的上下文

但残酷的现实是,花费了大量的精力来学习这些DDD战术组件,却在实现项目中却用不上,为什么呢?因为事务脚本思维太深,分层也大多是从技术角度出发,没有抽象出领域模型,也就是没有OO抽象,没有一个完整的对象,实体都没有,像工厂,值对象也就成了水中花

这也是我们虽然常重构代码,也不过是大类变小类,大函数拆分成小函数,符合一下代码规范,但对整个项目而言,其实没有实质性改进

如何能有实质性改进,对于复杂系统如何运用上这些战术组件呢?

此时,结构性思维发挥作用了

第一步:过程分解,把一个复杂的系统按流程拆解成各个阶段和步骤,这也是事务脚本的强项

第二步:对象建模,过程性拆解虽然可以降低了开发难度,但领域知识被割裂,代码的业务语义也不明确,在这方面OO是强项,提升代码复用性和内聚性

结合这两步,自上而下的结构化分解+自下而上的面向对象建模,过程化分析更好地清理了模型之间的关系,而对象模型提升代码复用性和业务语义表达能力

总结

DDD是一套很好的方法论,有时我们常在理论纯洁性与实战性之间徘徊。这也许是初级阶段常有的纠结点

DDD有适用场景,事务脚本有存在的优势

不能因为现在人们开口闭口都是DDD,就硬要开展DDD

DDD难以落地除了本身带来了很多概念,还需要团队整体素质

软件开发没有银弹,不能偏执于一种理论,实际开发是场硬仗,像混合格斗一样,不在于一招一式是哪门哪派,制敌才是终极目标,所以需要根据自身情况进行裁剪,灵活运用

DDD开篇总结

发表于 2020-10-19
字数统计: 3.1k | 阅读时长 ≈ 10

之前写了两篇《DDD开篇》与《DDD应对复杂》,是时候总结一下了

对于DDD的启蒙,不管是国内还是国外思维逻辑都是一样的。或者说如果你想写本关于DDD的书,大纲似乎是一样的

首先DDD是什么?给出定义,定义有些抽象,难以一次性接受,那就通过以往问题引出DDD,这时模型、复杂度、开发流程都是自然附带出的概念,再后面就是DDD的知识结构是什么,最后就是讲解一个实例,也有些会把实例穿插到各个篇章中

在DDD这个系列中,我也打算是这种思路,很平易近人

经过了之前两篇,对于第一部分的DDD是什么?为什么需要DDD基本都问答清楚了。

DDD是什么

DDD根本上是一种软件开发的建模方法论,其中使用了面向对象分析思想,并不是独立于外全新的体系

模型是对现实世界的抽象,那建模是对现实世界的抽象过程,但模型毕竟是模型,不能代替现实,就像类比不能代替问题本身一样。建模过程与建模者的观察视角和对问题的认知有直接关系,所以我们要带着审视的眼光看待模型

软件开发的最大问题之一便是业务人员和技术人员需求某种翻译才能交流,那么模型的质量就取决于翻译的还原度

建模不仅要还原实现世界,更要把重要的东西进行显示化。是一个软件之所以是这个软件的核心

建模可以通过建模=构造块+语法范式表达,各类语言都可以通过这种范式表达出来

(1) 自然语言建模

构造块:常用的那几千个汉字(或者英语的10万单词)

语法规则:主谓宾定状补

(2)计算机语言建模

构造块:加/减/乘/除,if-else, for, while..

语法规则:程序员最熟悉,此处就不说了

(3)过程建模

构造块:函数

语法规则:整个系统描述成一个个过程,每个过程通过函数的层层调用组成

(4)对象建模

构造块:对象

语法规则: 整个系统描述成对象与对象之间的关系

(5) DDD领域建模

构造块:实体/值对象/领域服务/领域事件/聚合根/工厂/仓库/限界上下文

语法规则:就是“构造块”之间的联系(不是很明显,这个需要深入研究。也正是DDD难掌握的地方)

为什么需要DDD

之前介绍过软件复杂度以及应对复杂度之道,DDD是其中一种术

  1. 使领域专家和开发者在一起工作,这样开发出来的软件能够准确地传送业务规则
  2. “准确传达业务规则”意思是说此时的软件就像如果领域专家是编码人员时所开发出来的一样
  3. 帮助业务人员自我提高。没有任何一个领域专家或管理者敢说他对业务已经了如指掌,业务知识需要长期学习过程,在
    DDD中,每个人都在学习,同时每个人又是知识贡献者
  4. 关键在于对知识的集中,因为这样可以确保软件知识并不只是掌握在少数人手中
  5. 在领域专家、开发者和软件本身之间不存在“翻译”,意思是当大家都使用相同的语言进行交流时,每个人都能听懂他人所说
  6. 设计就是代码,代码就是设计。设计是关于软件如何工作的,最好的编码设计来自多次试验,这得益于敏捷的发现过程
  7. DDD同时提供了战略设计和战术设计两种方式,战略设计帮助我们理解哪些投入是最重要的,哪些既有软件资产是可以重新拿来使用的,哪些人应该被加到团队中?战术设计帮助我们创建DDD模型中各个部件

上面的比较学术,简单讲:理解DDD的本质是统一语言、边界划分和面向对象分析的方法

  1. 统一语言,主要思想是让应用能和业务相匹配,这是通过在业务与代码中的技术之间采用共同的语言达成的。也就是设计及代码,代码及设计
  2. 面向对象,DDD核心是领域模型,先找到业务中的领域模型,以领域模型为中心,驱动项目开发,指引我们如何实现面向服务架构或业务驱动架构。领域模型设计精髓在于面向对象分析,对事物的抽象能力,一个领域驱动架构师必然是一个面向对象分析大师
  3. 业务语义显示化,统一语言也好,面向对象也好,最终目标都是为代码可读性和可维护性服务,统一语言使得核心领域概念无损地在代码中呈现,从而提升代码的可理解性
  4. 分离业务逻辑和技术细节,让两个维度的复杂度被解开和分治,比如整洁架构

对于最终目标来讲,在软件项目中,任何方法论如果最终不能落在“减少代码复杂度”,都是有待商榷的

DDD挑战

DDD在实践中有很高的门槛

  1. 持续地将领域专家引入项目
  2. 为创建通用语言腾出时间和精力
  3. 改变开发者对领域的思考方式

最大挑战之一便是:需要花费大量的时间和精力来思考业务领域,研究概念和术语,并且和领域专家交流,以发现、捕捉和改进通用语言

在开发过程中,最大的鸿沟之一便是在于领域专家和开发者之间,通常领域专家关注放在交付业务价值上,而开发者则将注意力放在技术实现上。并不是说开发者动机是错误的,面是说开发者的眼光被自然而然地吸引到实现层面。即使让领域专家与开发者一同工作也只是表面协作,这样在开发的软件中产生了一种映射:业务人员所想的映射到开发者所理解的,软件便不能完全反映领域专家的思维模型,随着时间推移,这种鸿沟将增加软件开发成本

DDD将领域专家与开发人员聚焦在一起,这样所开发的软件能够反映出领域专家的思维模型,这并不意味着我们将精力都花在对“真实世界”的建模上,而是交付最具业务价值的软件,在实用和理想之间存在冲突时,根据它们的互异程序,在DDD中选择实用性

领域专家与开发人员一起创建一套适用于领域建模的通用语言。通用语言必须在全队范围之内达成一致;所有成员都使用通用语言进行交流,通用语言是对软件模型的直接反映

作为开发者,我们都是技术思想者,技术实现对于我们来说并不是什么难事。并不是说技术地思考不好,只是说有时少从技术层面去思考会更好。这么多年来,我们习惯了单从技术层面完成软件开发,那现在是时候考虑一种新的思考方式了。为你的业务领域开发一门通用语言便是一个好的出发点

技术负债

程序员工作时间大部分在新增新功能,996式加班大多为了赶工上线,而对赶工出来的软件质量是在打个问题,而技术负债在软件质量领域是一个很能重要概念,对于软件工程来讲,这真是个特别的东西

Martin Fowler定义技术负债就是增加新功能所需要的额外成本

软件质量不同于其他商品质量。购买一个商品,在使用过程中会直接发现该商品质量问题,而软件质量不是直接被软件系统的使用者所感知的,也就是说客户如果同时使用两种质量不同的系统,用户是无法发现两者的区别,不过随着时间推移,自己的产品交付过程越来越长了

软件质量问题不是直接面向用户,而是面向软件的开发团队。因为软件质量差,新程序员很难快速上手,无法形成生产力;修改别人的代码时可能一走在不得要领;bug丛生,修复一个可能使得系统崩溃,一个都不修复系统反而正常工作;修复bug时牵一动百,修改一处却引起其他连锁故障反应。。。,这些都是软件质量低下的外在表现

正是由于软件质量不是最终用户所能感知的,导致行业内对软件质量没有过多重视–客户都没有提出改进要求,那么一切为客户服务的软件公司自然没有动力去提升软件质量,而且行业内对软件质量存在认知误区:便宜确实没好货,但是质量高必然导致成本 上升,而客户又不会察觉质量好坏,那么产品如何卖出好价格

软件并不是质量越高,成本就越高。这似乎违反常识,背后其实也与技术负债有关。如果将技术负债看成是一种前进中的累赘,累赘遍布于代码各处,那么提高软件质量就是通过良好设计或重构来减轻这种累赘,从而能轻装上阵,新增功能就能更加快捷,交付效率也会大大提升。

降低技术负债意味着软件质量提高,软件质量越高,修改拓展起来就越方便。

如何降低技术负债?这存在一个适度问题,代码越多,复杂性越高,技术负债肯定越高,那么就需要惜墨如金。有时为了写正确可运行的简洁代码,可能要删除数十倍的代码,但也不是代码越少越好。有的代码只是考虑功能实现,没有考虑到功能的对接或扩展,那么当需要对功能实现扩展时,就发现难以下手,甚至需要采取黑客破解方式强行入侵系统,这此都是原来代码过于简单僵化的表现

适度是在过度和不足中探索平衡的结果。代码适度的一个衡量标准是单一职责原则,即每个函数或类只能有一个职责。

面向对象编程中还有另一个原则:DRY原则。对这个原则的共同理解是代码不应该重复,如果两段代码表示的是同一个职责,那么合并它们。但这种抽象合并导致共享内核或共享库,最张造成代码各处对共享库或内核的依赖,这就很自然地引入了不必要的、偶然的复杂性–一旦共享库发生修改,牵一动百的事情就可能发生。很多时候重复的代码可能会带来相当大的优势,重复能拖延决策,这是软件开发的黄金。这样,延迟到适当时机从多个专业化角度重构,这比从单个方法层面进行抽象的重构要容易数倍。

DDD应对复杂

发表于 2020-10-13
字数统计: 3.5k | 阅读时长 ≈ 11

复杂

Eric Evans所著副标题–Tackling Complexity in the Heart of Software,对于简单系统其实没有必要使用DDD,只有在复杂系统中,才能体现DDD的价值

那么何为“复杂”,或者说为什么软件是复杂的呢?

即使是研究复杂系统的专家,如《复杂》一书的作者 Melanie Mitchell,都认为复杂没有一个明确得到公认的定义。不过,Melanie Mitchell 在接受 Ubiquity 杂志专访时,还是“勉为其难”地给出了一个通俗的复杂系统定义:由大量相互作用的部分组成的系统,与整个系统比起来,这些组成部分相对简单,没有中央控制,组成部分之间也没有全局性的通讯,并且组成部分的相互作用导致了复杂行为。

这个定义同样可以表达软件复杂度的特征。定义中的组成部分对于软件系统来说,就是我所谓的“设计单元”,基于粒度的不同可以是函数、对象、模块、组件和服务。这些设计单元相对简单,然而彼此之间的相互作用却导致了软件系统的复杂行为。

《管理3.0:培养和提升敏捷领导力》中从两个维度分析复杂:行为(预测能力)和结构(理解能力),分别举了实际生活中简单的例子

理解力维度分为 Simple 与 Comlicated 两个层次,预测能力维度则分为 Ordered、Complex 与 Chaotic 三个层次

在英文中,表示复杂的有Complicated和Complex

Simple = easily knowable.

Complicated = not simple, but still knowable.

Complex = not fully knowable, but reasonably predictable.

Chaotic = neither knowable nor predictable.

参考复杂的含义,Complicated 与 Simple(简单)相对,意指非常难以理解,而 Complex 则介于 Ordered(有序的)与 Chaotic(混沌的)之间,认为在某种程度上可以预测,但会有很多出乎意料的事情发生

针对复杂的定义,分析角度以及应对的方法,绘制一张表格:

























角度 维度 表象 应对
理解力 规模

软件的需求决定了系统的规模。当需求呈现线性增长的趋势时,为了实现这些功能,软件规模也会以近似的速度增长

函数存在副作用,调用时可能对函数的结果作了隐含的假设;


类的职责繁多,不敢轻易修改,因为不知这种变化会影响到哪些模块;


热点代码被频繁变更,职责被包裹了一层又一层,没有清晰的边界;


在系统某个角落,隐藏着伺机而动的bug,当诱发条件具备时,则会让整条调用链瘫痪


不同的业务场景包含了不同的例外场景,每种例外场景的处理方式都各不相同;


同步处理与异步处理代码纠缠在一起,不可预知程序执行的顺序
分而治之,控制规模
结构


结构之所以变得复杂,在多数情况下还是因为系统的质量属性决定的

代码没有显而易见的进入系统中的路径;


不存在一致性、不存在风格、也没有统一的概念能够将不同的部分组织在一起;


系统中的控制流让人觉得不舒服,无法预测;


系统中有太多的“坏味道”,整个代码库散发着腐烂的气味儿


数据很少放在使用它的地方,经常引入额外的巴罗克式缓存层,目的是试图让数据停留在更方便的地方
保持结构的清晰与一致

所有设计质量高的软件系统都有相同的特征,就是拥有清晰直观且易于理解的结构。
预测能力 变化 在设计软件系统时,变化让我们患得患失,不知道如何把握系统设计的度。若拒绝对变化做出理智的预测,系统的设计会变得僵化,一旦变化发生,修改的成本会非常的大;若过于看重变化产生的影响,渴望涵盖一切变化的可能,一旦预期的变化不曾发生,我们之前为变化付出的成本就再也补偿不回来了。这就是所谓的“过度设计” 拥抱变化

可进化性(Evolvability)

可扩展性(Extensibility)

可定制性(Customizability)

根据上面的总结,想起《架构与架构师》中提到的架构师需要面对应用程序的两个层面需求:功能需求与非功能需求,功能性需求越多越大那对应的软件规模增长越快,可变性越大;非功能需求就要考虑质量属性,也就是整体结构要清晰,明确的规范约束,易于理解好维护。而这两个层面需求正好又对应着业务复杂度与技术复杂度

技术复杂度与业务复杂度并非完全独立,二者混合在一起产生的化合作用更让系统的复杂度变得不可预期,难以掌控。同时,技术的变化维度与业务的变化维度并不相同,产生变化的原因也不一致,倘若未能很好地界定二者之间的关系,系统架构缺乏清晰边界,会变得难以梳理。复杂度一旦增加,团队规模也将随之扩大,再揉以严峻的交付周期、人员流动等诸多因素,就好似将各种不稳定的易燃易爆气体混合在一个不可逃逸的密闭容器中一般,随时都可能爆炸:

DDD应对

这让我回想起了“结构化思维”,逻辑+套路

逻辑是一种能力,而套路是方法论,是经验。逻辑是道,方法论是术;逻辑可以学习很多思维模型理论,但套路有路径依赖,这也是去大厂的好处,可以接触到各种大牛,直接获取他们的经验和方法

DDD是如何应对这些复杂性的呢?

  1. 限界上下文分而治之,边界划分后,各子域相对整体规模就小了
  2. 结构方面通过技术角度:分层架构、六边形架构…分离技术实现与业务逻辑
  3. 变化,不再过程式编程,加强抽象领域模型,通过过程分解+对象模型,拆解并业务显示化,达到更好的复用性和易理解性

banq提出类似总结解决复杂性两种方法:拆解成松耦合的组件+使用容易让人明白的套路表达出来

拆解也就是引入了“领域或子域”以及“有界上下文”划分边界;再引入各种模式名词,比如聚合、实体、值对象、工厂、仓储、领域事件,让知晓这些模式的人能够一下子定位到功能对应实现的组件,随着人们逐步了解,对于理解DDD系统来说,就不再是复杂的

开发流程

我们在开发、运营、维护软件的过程中很多技术、做法、习惯和思想。软件工程把这些相关的技术和过程统一到一个体系中,叫“软件开发流程”,软件开发流程的目的是为了提高软件开发、运营和维护的效率,以及提升用户满意度、软件的可靠性和可维护性

发展了很多的流程,这儿复习一下最资深的瀑布模型

瀑布模型

瀑布模型(Waterfall Model)1970年温斯顿·罗伊斯(Winston Royce)提出了著名的“瀑布模型”,直到80年代早期,它一直是唯一被广泛采用的软件开发模型

当软件行业还在年幼时期,它从别的成熟行业(硬件设计,建筑工程)借用了不少经验和模型。在那些“硬”的行业中,产品大多数遵循[分析->设计->实现(制造)->销售->维护]这个流程

软件系统很难做到像建筑行业一样,程序员只要根据图样一步步实施就能完成项目。没人可以完整设计出一个大型项目的图样,这是瀑布软件工程方法的致命问题

比如A拥有一家跑车公司,可以给客户自定义生产跑车。有一天一土豪来到A的公司,跟A商谈了一个跑车项目,他们谈好了车型,材料,马力等等细节。之后,A带着团队做了6个月,做成了这架跑车,交给了土豪。可是土豪开了一天之后回来要求重做,原因是当讨论方案的时候,双方都忘记给跑车安尾灯了!

但是给跑车安装尾灯,需要重新设计车尾部,加上倒车灯,把车底拆开,安装线路,修改传动装置把倒车档和倒车灯联系起来

软件多变,而且很长周期后才让用户看到最终产品,反反复复

瀑布模型只能在产品的定义非常稳定;产品模块之间的接口、输入和输出能很好地用形式化的方法定义和验证

统一语言

软件行业相对其它传统行业有独特的难,同为构建,建筑工程在施工队进场开始构造之前,各种工程图样和材料都已经非常精确地准备到位,但在软件中没有足够时间收集所有需地,即使收集了,需求也不是从创建软件角度去描述

软件项目团队,从产品 到 开发,再到测试,应了那句不怕神一样的对手,就怕猪一样的队友,层层兜底,产品考虑不到的,开发帮兜底,开发不给力时,靠测试兜底,测试不走心的,那只能客户买单,名为兜底大队

项目开始,产品没有足够时间收集需求,但快速行动的重要性扎根程序员内心,没有足够信息来构建,可项目有交付期限,程序员只能进行创造性假设,填补产品经理留下的空白,只是为了保持项目不断推进。但产品经理或者客户经常改变主意,这意味着程序员对留白处的创建性假设经常夭折

项目实践需求与程序员的理解之间存在客观落差,但程序员负责最终完成项目,如果这种落差没有被注意或发现,就只能由程序员自己创造。这种创造是在没有指导或指示的情况下进行,当项目展示给客户时,客户才发现不是自己想要的,双方就容易争执

更多时候,程序员之所以在项目开始迟迟不亡友,是因为他们希望有人告诉该怎么做,但这种情况一般不会发生,这时就发挥自己的创造力,但这个创造有时会使项目偏离正确方向。如何发挥程序员创造力,同时又能保持项目的方向不被程序员带偏呢?

解决方法就是让程序员迟早参与创意过程,与业务专家、客户、产品经理一起参加头脑风暴会议,不断缩小双方思考或理解偏差。有逻辑性的正确设计会节省大量代码,因为编码实践来试错的代价是昂贵的。编码涉及大量技术细节,细到一个字段的字节数都需要考虑,一旦发现代码实现的功能完全不是客户要求的,就只能全部推倒重来。成千上万行代码被删除,好像它们没存在过,他们存在过的意义仅是让程序员明白:此路不通

程序员需要将业务知识的学习和思考结合起来,领域驱动核心就是多了一个领域层,这个领域是业务相关原语,随着业务价值变化而变化,通常每个企业的核心业务域是千差万别的,所以,在技术实现的时候,反复要问我们团队,我们多少比例的代码是针对业务本身,而业务本身对业务消费者有多大的价值?作为业务的技术实现方,一定要有手段通过量化方法,衡量你的业务价值,从而能精细化迭代升级

统一语言,并不是把从客户那里听到的内容翻译成程序自己的语言,减少误解,让客户更容易理解我们的草图,并且真正帮助纠正错误,帮助我们获取有关领域新知识,目标是创造可以被业务、技术和代码自身无歧义使用的共同术语

在PRD文档,设计文档,代码以及团队日常交流中,都使用同一套领域术语,极大地提升沟通和工作效率

简单理解起来的话,也就是把业务人员和开发人员的语言统一起来,用代码来感受一下大概就是:

1
2
userService.love(Jack, Rose) => Jack.love(Rose)
companyService.hire(company,employee) => Company.hire(employee)

通过统一语言,领域驱动设计的核心是建立统一的领域模型。领域模型在软件架构中处于核心地位,软件开发过程中,必须以建立领域模型为中心,以保障领域模型的忠实体现,解决OOAD中OOA分析模型与OOD设计模型割裂的弊端

DDD开篇

发表于 2020-10-11
字数统计: 5.2k | 阅读时长 ≈ 17

从知道DDD到现在已经很多年了,看了不少理论知识,在项目中也使用了DDD,碰到些问题,也有些思考,整理一下,上升一下,形成一种适合自身的方法论

在回顾过程中,首先追根溯源,什么是DDD?为什么要使用DDD?如何给别人阐述这些最基本的概念与理念,真是个难题

什么是DDD

DDD已经发展了很多年,现在的一些书也已经不太关注这个基本概念,
平时闲聊时,开口闭口都是DDD,已经不知道DDD的本体是什么,只是听得耳熟,说得顺口了,细细回想下,DDD是个什么?一时语顿,不得找一下Eric Evans大神最原味的解答

DDD来源于Eric Evans的书:
《Domain-Driven Design - Tackling Complexity in the Heart of Software》
领域驱动设计 - 软件核心复杂性应对之道

由此可看出DDD的全称Domain-Driven Design,之前常听到TDD,那何为Domain呢?

书中解释:

每人软件程序都会与其用户的活动或兴趣相关。用户在其中使用程序的主要环境称为软件的领域(domain)

一些领域会涉及到物质世界:航线预订程序的领域涉及到现实中登机的人。一些领域是无形的:财务程序的领域就是货币和金融

领域模型并不是某种特殊的图,而是图所要表达的思想。它并不仅仅是某个领域专家头脑中的知识,而是对相关知识进行严格的组织与选择性抽象

领域模型并不是尽可能地制作一个逼真的模型。即使在一个可触及的真实世界事物的领域中,我们的模型也只是一个仿真的创造物

看到这些,应该还是有点晕,到底是什么?解释这种基本元语,特别的难,有人感觉领域就是业务,那为什么不叫Business Driven Design呢?

有点钻牛角尖的意思,这其实是人类学习新知识的一种惯性思维,从以往经验中寻求类似的事物,帮助理解新事物,好处就可以快速认知新知识,坏处可能永远掌握不了新知识。既然给不了一种清晰的类比解释,那就不求甚解地理解,这个新事务的代名词叫DDD,随着认识的深入,慢慢就会从抽象到具体

但为了提前更好地理解DDD,还是有必要把一些现流行的解读罗列出来

银行业务被银行的内部人员和专家所熟知。他们知道所有的细节、所有的困难、所有可能 出现的问题、所有的业务规则。这些就是我们永远的起始点:领域

领域驱动设计当然不是架构方法,也并非设计模式。准确地说,它其实是“一种思维方式,也是一组优先任务,它旨在加速那些必须处理复杂领域的软件项目的开发”。领域驱动设计贯穿了整个软件开发的生命周期,包括对需求的分析、建模、架构、设计,甚至最终的编码实现,乃至对编码的测试与重构

领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并且表达了准确的业务含义

DDD专门解决复杂性的方法论

DDD是面向对象分析的方法论,它可以利用面向对象的特性(封装,多态)有效地化解复杂性,而传统的J2EE等事务性编程只关心数据

DDD作为一种软件开发方法,它可以帮助我们设计高质量的软件模型

由前人的种种总结,粗略可以把DDD理解为一种方法论,一种构造软件模型的方法论

模型

软件的开发目的是什么?软件开发是为了解决由需求带来的各种问题,而解决的结果是一个可以运行的交付物。那么软件设计就是在需求和解决方案之间架设一座桥梁

区别于解决简单的问题,软件的开发往往是一项长期的工作,会有许多人参与其中。在这种情况下,就需要建立起一个统一的结构,以便于所有人都能有一个共同的理解。这就如同建筑中的图纸,懂建筑的人看了之后,就会产生一个统一的认识。而在软件的开发过程中,这种统一的结构就是模型,而软件设计就是要构建出一套模型

模型是对现实世界的简化抽象

根据使用场景的不同,模型大致可以分为物理模型、概念模型、数学模型和思维模型等等

物理模型是拥有体积及重量的物理形态概念实体物体,是根据相似性理论制造的按原系统比例缩小的实物

概念模型是对真实世界中问题域内的事物的描述,是领域实体,而不是对软件设计的描述,它与技术无关。

数学模型是用数学语言描述的一类模型,可以是一个或一组代数方程,微分方程、差分方程、积分方程或统计学方程,也可以是某种适当的组合数学模型

思维模型是用简单易懂的图形、符号或者结构化语言等表达人们思考和解决问题的形式

建立模型有很多方法,并不意味着要用特定符号、工具和流程。无论使用何种建模工具和表示法,只要有助于我们对问题域的理解,均可认为是好的模型。在处理问题时,我们最好隐藏那些不必要的细节,只专注于重要的方面,抓住问题的本质。这就是建模和抽象的价值所在

在软件领域,影响最强的建模工具当属统一建模语言(Unified Modeling Language,UML)

模型,是一个软件的骨架,是一个软件之所以是这个软件的核心。一个电商平台,它不用关系型数据库,还可以用 NoSQL,但如果没有产品信息,没有订单,它就不再是电商平台了

模型的粒度可大可小,一个类是模型,把一整个系统当作一个整体理解,就是个大模型。而常听说的“高内聚、低耦合”其实就是对模型的要求。一个高内聚低耦合的模型能够有效地隐藏细节,让人理解起来也更容易,甚至还可以在上面扩展

在DDD中,模型的选择取决于三个基本用途:

  1. 模型与设计核心的相互塑型。正是模型与实现之间密切的联系使得模型与现实相关并且保证对于模型的讨论分析能够应用于最终产品–可运行的程序
  2. 模型是所有团队成员所使用语言的核心。由于模型与实现是相互绑定的,因此开发人员可以用这种语言来讨论程序
  3. 模型用来提炼知识。模型是团队在组织领域知识和辨别最感兴趣的原理时一致同意的方式

方法论

由上文可知DDD是一种开发方法,那在DDD之前,是怎么进行软件分析设计的呢?一般有两种方法:ER数据建模法和面向对象建模法

在软件的世界里,任何的方法论如果最终不能落在“减少代码复杂度”这个焦点上,那么都是有待商榷的。

ER

这是大多数人进行软件行业时,必学的方法,并且在学生时代,实践课程都是以此为示例,导致这种方法在脑海中根深蒂固

ER数据建模法是在接受到需求以后直接开始数据表ER模型的设计、实体表和表关系的设计

建模过程是一种翻译再表达的过程,其唯一要点就是翻译不走样,如果翻译过程过多引入其他干扰因素和知识,那么无疑会增加翻译的难度和复制的精确性

由于人们对数据库特别看重,SQL普及化,以至将软件的理解浓缩成了CRUD

但由于过于注重数据库技术而忽视了业务上下文。使用CRUD代替业务用语,例如使用“创建订单”替代“下单”,使用“创建帖子”代替“发贴”,使用“创建发票”替代“开票”,虽然也容易让人明白,但“开票”等用语才是真正的业务术语、业务行话,是这个行业内每个人都知晓的。作为软件系统不是去遵循这些用语习惯,而是进行转换改造,按照自己的理解生造出一些词,是不是会潜移默化地将业务需求引导到不同方向呢?

这种方法门槛低,带来了效率的提升,但是高效率不代表高质量,而软件高质量却能带来高效率

OOAD

业务初期,功能比较简单,CRUD基本可以满足。但随着系统的不断演化,业务系统越来越复杂,各模块间有着千丝万缕的关系,如何提升其扩展性

OO最大的宣言就是Everything is object。所有的事物都有两个方面:有什么(属性):用来描述对象;能够做什么(方法):告诉外界对象有那些功能。

把人们从低层的SQL计算机语言更加面向人类思维

Object-Oriented Analysis (OOA):面向对象的分析与设计的侧重点是业务领域分析,与软件所要应用的行业领域相关,而与软件技术关系不大,需要由领域专家进行。这一部分的工作被称为“需求分析”

OOA的成果:

业务领域用例图、活动图、协作图、大量的业务文档资料

Object-oriented design (OOD),用面向对象的方法为真实世界建立一个计算机中的虚拟模型,OOD的主要任务是跨越业务领域模型与可实际运行的软件系统之间的鸿沟,OOD的难度是非常大的,负责OOD工作的人被称为系统架构设计师

系统架构设计师的任务:

  1. 确定系统的总体框架—大多采用已有的领域框架
  2. 正确理解需求分析得出的领域模型,用面向对象的思想设计出软件体系结构—系统概要设计
  3. 分析现实的可获取的技术资源,分解出软件的各个组件,安排好开发任务流程—系统详细设计

从上面的表述,可以看出面向对象相比ER模型强大多了,但其实面向对象的思想至今都没有真正意义得到普及,至少我是这么认为的。为什么呢?看多少人在进行CRUD这种面向过程式编程就知道了,而且早年的J2EE开发模式,讲究 Web/Service/Dao三层结构。面向过程编程,对象只是数据的载体,没有行为。以数据为中心,以数据库ER设计作驱动。分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。该对象我们称之为贫血领域对象

贫血领域对象(Anemic Domain Object):是指仅用作数据载体,而没有行为和动作的领域对象。大量的业务逻辑写在了Service层中,随着业务逻辑复杂,业务逻辑、状态会散落在Service层中的很多处理类或方法中。将数据和行为割裂,原来的代码意图会越来越模糊,代码的理解和维护成本会越来越高。

虽然面向对象没有得到真正的普及,但面向对象还是带来了很多思想,我不确定这些思想是不是面向对象之后才有的,但至少很多思想的确是在学习面向对象后才知道的

比如之前写过一个系列《SOLID》,就是面向对象设计原则

而且像Robert C.Martin认为一个可维护性较低的软件设计,通常由于如下4个原因造成:

  1. 过于僵硬Rigidity
  2. 过于脆弱Fragility
  3. 复用率低Immobility
  4. 黏度过高Viscosity

软件工程和建模大师Peter Coad认为,一个好的系统设计应该具备如下三个性质:

  1. 可扩展性
  2. 灵活性
  3. 可插入性

而想规避缺点拥有优点,面向对象设计提供很大的便利,软件的复用(Reuse)或重用拥有众多优点,如可以提高软件的开发效率,提高软件质量,节约开发成本,恰当的复用还可以改善系统的可维护性,这些都是以面向对象设计原则为基础而来

对象与数据表的关系?

在从ER建模转向OOAD时,常把对象类比成表,以前称为表,现在换个名字叫对象,他们好似是一样的,但其实他们的差别很大,就文章开关所述,这是人的经验主义,可能永远掌握不了新知识

数据表是一种数据结构,数据表中的数据是需要SQL去操作的,也就是说,数据结构中的数据是被外部某些行为或函数操作的;虽然对象或类中封装的属性其实也是数据,但对象或类有行为方法,这些行为可以保护被封装的属性数据,外界需要改变对象中的属性数据时,必须通过公开的行为方法才能实现。因此,对象和数据结构两者的区别之一就于在对数据操作是主动还是被动,对象是主动操作数据,而数据结构是被动操作,这一区别使得两种方式下的分析设计思路和编程范式完全不同

表达业务领域中的业务概念时,强调主动操作数据的类或对象更适合表达业务概念,因为业务领域中的业务策略或业务规则都需要动态操作,它们的逻辑性需要主动操作数据完成

设计时,业务对象是定义业务行为,而数据表是定义业务数据结构。一个注重行为,一个注重数据。着重点不同,导致设计要求不同,数据表一旦形成,就不会因为一个特定的应用而进行调整,它必须服务于整个企业,因此,这种结构是许多不同应用之间平衡的选择;而使用对象可以针对具体应用进行设计,将业务行为放入对象中,更能精确反映领域概念,保证业务规则的真正逻辑一致地实现

虽然OOAD很不错,但分析和设计之间常常落差很大,甚至是分裂的。分析阶段的成果不能顺利导入设计阶段,设计阶段引入太多细节而歪曲了分析的宗旨。分析和设计分裂的根本原因是它们导向目标不同,分析人员的目标是从需求领域收集基本概念,是面向需求的,而设计人员则不同,他们负责用代码实现这些概念,因此必须指明能在项目中使用编程工具构建的组件,这些组件必须能够在目标环境(比如java)中有效执行,并能够正确解决应用程序出现的问题。条条大路通罗马,分析人员负责指出罗马方向,而设计人员负责找出通往罗马的某条道路,但是技术细节有时会让这个过程中产生绕路和不必要的复杂性,甚至走错方向,南辕北辙


DDD

【程序员其实不是在编写代码,面是在摸索业务领域知识】

这是很多程序员的真实写照,尤其复杂业务,为什么呢?

很多时候你会遇到这样的情况:一个函数写了几百行,里面的if-else写了一大堆,计算各种业务规则。另一个人接手之后,分析了好几天,才把业务逻辑彻底理清楚。

这个问题从表面来看,是代码写的不规范,要重构,把一个几百行的函数拆成一个个小的函数。从根本上来讲,就是“重要逻辑”隐藏在代码里面,没有“显性”的表达出来

这只是一个函数,推而广之,到类、到模块、到系统,是同样的道理,比如:

业务流程隐藏在多个对象的复杂调用关系里面;
某个业务核心概念没有提取出来,其职责分摊到了其他几个实体里面;
系统耦合,职责边界不清

所以,建模的本质就是把“重要的东西进行显性化,并进而把这些显性化的构造块,互相串联起来,组成一个体系”

而DDD就是为了解决这些痛点,DDD建模思想不同于以往的面向对象分析设计思想,建模和代码之间还存在落差,无法平滑衔接。它将分析和设计完美结合起来,通过引入上下文的特殊性,将项目的真正业务背景和集成复杂性引入设计建模阶段,虽然增加了设计的复杂性,但也提高 了设计的实用性。

领域驱动设计当然不是架构方法,也并非设计模式。准确地说,它其实是“一种思维方式,也是一组优先任务,它旨在加速那些必须处理复杂领域的软件项目的开发”。领域驱动设计贯穿了整个软件开发的生命周期,包括对需求的分析、建模、架构、设计,甚至最终的编码实现,乃至对编码的测试与重构。

领域驱动设计强调领域模型的重要性,并通过模型驱动设计来保障领域模型与程序设计的一致。从业务需求中提炼出统一语言(Ubiquitous Language),再基于统一语言建立领域模型;这个领域模型会指导着程序设计以及编码实现;最后,又通过重构来发现隐式概念,并运用设计模式改进设计与开发质量。这个过程如下图所示:

这个过程是一个覆盖软件全生命周期的设计闭环,每个环节的输出都可以作为下一个环节的输入,而在其中扮演重要指导作用的则是“领域模型”。这个设计闭环是一个螺旋式的迭代设计过程,领域模型会在这个迭代过程中逐渐演进,在保证模型完整性与正确性的同时,具有新鲜的活力,使得领域模型能够始终如一的贯穿领域驱动设计过程、阐释着领域逻辑、指导着程序设计、验证着编码质量。

如果仔细审视这个设计闭环,会发现在针对问题域和业务期望提炼统一语言,并通过统一语言进行领域建模时,可能会面临高复杂度的挑战。这是因为对于一个复杂的软件系统而言,我们要处理的问题域实在太庞大了。在为问题域寻求解决方案时,需要从宏观层次划分不同业务关注点的子领域,然后再深入到子领域中从微观层次对领域进行建模。宏观层次是战略的层面,微观层次是战术的层面,只有将战略设计与战术设计结合起来,才是完整的领域驱动设计

高内聚

发表于 2020-09-07
字数统计: 725 | 阅读时长 ≈ 2

最近一季度KPI中,增加了一项单元测试覆盖率

在之前工作经历中,也有过类似情况,老板开始关注单元测试情况了,就会加上覆盖率这个绩效指标,不管以前如何应对,还是再次关注了一些对于测试的文章,TDD虽然没有大流行,但这个概念还是常被人提起

这张图是在一篇文章中看到的

好写单测的系统往往比不好写单测的系统更加健壮,如果一个系统大部分代码都可以写无 Mock 单测,那么它看起来就像左图一样,外部调用只是薄薄的一层,可以随意更换。

如果你的系统大部分代码都一定要 Mock 才能测试的话,或者根本无法测试的话,就像右图一样,说明你的业务根本就没有自己的核心逻辑,而是和各种外部调用缠绕在一起

这张图让我有很多的感触,值得整理一下思绪

领域模型

这张图跟整洁架构图特别像

中心是核心的业务逻辑,依赖路径是由外向内,外围千变万化,但内核是不变的

不管是设计,还是编码,亦或是UT,都需要这样的思路;需要高内聚,核心业务代码不能分散,集中,有中心脉络

我们有时过多注重了技术层次,比如MVC,我们更多的是关注各层技术变化速率而分层,而忽略了业务层次变化

结合之前分析的《DDD分层》,也应该是三层,但不是MVC,面是是输入、领域、输出三层,类似端口适配器架构

向心力

这可能跟图的形状有关,因为是圆形。所以想到了向心力这个词,业务逻辑要内聚,单元测试也要内聚,当然了,单元测试的内聚是由于业务逻辑的内聚。

而这种高内聚,正合了向心力。没有向心力就是一群散沙,设计是,架构是,团队亦是

功夫在诗外

以前还写过一篇《功夫在诗外》,为了专业有所精进,就去看了很多专业无关的书,似乎方向搞错了,灵感不是平白无故地出现的,而是长年持续思考累积,再因为偶尔一件诗外之事激发,贯穿了之前的整个思考过程

以前看整洁架构那张图怎么没有如此些感悟呢,因为作为一名程序身在其中,但当看到测试内容时,因不是测试人员,身在事外看事,就有了感悟,所以功夫不能完全花费在诗外,也不能离诗过远,才能带来灵感

12…8
朱兴生

朱兴生

彪悍的人生需要书写

77 日志
3 分类
29 标签
© 2016 — 2021 朱兴生 | Site words total count: 193.7k
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
沪ICP备18040647号-1