码农戏码

新生代农民工的自我修养

0%

对于Java理论在《Java异常处理》中已经阐述了,看看理论如何指导落地

现流行的文章SpringBoot如何优雅处理异常,落地的确方便,使用AOP统一处理异常,但只是处理了api层次的异常

应用中抛出异常有两种方式:

  1. 带有ErrorCode的异常
  2. 明确类型的异常

对于controller层,也是面向用户的,需要error code,所以采用第一种方式

前端通过映射关系给出更好用户体验的提示语,也有很多项目都是controller层直接拼接出提示语,前端直接展示

所以一般会定义一个接口ErrorCode

1
2
3
4
interface ErrorCode {
String getErrorCode();
String getMessage();
}

具体的实现可以通过enum

1
2
3
4
5
6
7
8
@Getter
enum ApiErrorCode implements ErrorCode {

USER_NOT_FOUND("10000","用户不存在");

private String errorCode;
private String message;
}

再定义一个统一异常

1
2
3
4
5
6
public class ApiException extends RuntimeException {

public ApiException(ErrorCode errorCode) {

}
}

在aop拦截时,直接拦截此异常就行

api层次的异常可以这么处理,那业务层呢?很多时候都是缺失设计的,这也在上篇说过exception从语法层面看很简单,但要设计一个好的异常是很难的

大多项目直接把api exception拿来当做business exception使用

对于一个良好的业务接口,应该采用第二种方法:细致的异常

1
User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException; 

已检查异常要比错误返回码(许多老式的语言中使用)好很多。迟早(或许不久),人们将不能检查一个错误返回值;使用编译程序来实施正确的错误处理是一件好事。同参数与返回值一样,这样的已检查异常对一个对象的API来说是整体的一个不可分割部分

这样的接口更丰富,也更面向对象,可也给客户端带来的麻烦,缺点在上篇已经阐述

对可恢复的情况使用已检查异常,对程序错误使用运行时异常

在大多项目中,其实业务层抛出异常后,通常会“可恢复”吗?大多数情况也需要用户手工干预,系统无法自行恢复,比如UserNotFoundException, PasswordNotMatchException系统能怎么处理,无非还是得给用户重新输入用户名和密码

在controller层去调用service方法,也只能如此处理

1
2
3
4
5
6
7
8
9
Response login(String username,String password) {
try {
userService.login(username,password);
}catch(UserNotFoundException ue) {
throw new ApiException(ApiErrorCode.USER_NOT_FOUND);
}catch(PasswordNotMatchException pe){
throw new ApiException(ApiErrorCode.Password_Not_Match);
}
}

这样处理也就理论化了,带来了多少优点呢?这些缺点不正是checked exception被嘟囔的地方吗

那我们把业务异常也定义为runtime exception,这样减少客户端压力,想处理就处理,不想处理,我们也可在拦截器中兜底

不过抛出运行期异常,减少客户端的压力,但也带来了接口不明确的困惑

非检查型异常的最大风险之一就是它并没有按照检查型异常采用的方式那样自我文档化。除非 API 的创建者明确地文档化将要抛出的异常,否则调用者没有办法知道在他们的代码中将要捕获的异常是什么

总结起来,还是那句话,异常语法很简单,但设计好异常不易;现在技术快速发展,通过技术手段可以达到更大的便捷性,但不能只有技术手段而忽略设计,没有设计的代码称不上好代码,可以取舍,但不能全舍

Java异常,本身知识体系很简单,但要设计好异常,却不是易事

Java异常如何使用,尤其checked exception,好些语言(c#,python)都没有此类型异常,只有unchecked exception;对于java为什么有checked exception,是不是设计过渡,在java初期被讨论了很多回,以及如何使用异常也被讨论了很多次,最近我在落地DDD时,又思考到此问题,不得不再翻回这个老问题,翻阅《Effective java》、《J2EE设计开发编程指南》这些经典

按普世标准,处理异常最佳实践有:

  • 【强制】异常不要用来做流程控制,条件控制。说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多
  • 异常应该只用于异常的情况下:它们永远不应该用于正常的控制流,设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常
  • 对可恢复情况使用受检异常,对编程错误使用运行时异常
  • 抛出与抽象相对应的异常
  • 每个方法抛出的异常都要有文档
  • 优先使用标准异常

再来看看前人的论述:

在使用UseCase来描述一个场景的时候,有一个主事件流和n个异常流。异常流可能发生在主事件流的过程,而try语句里面实现的是主事件流,而catch里面实现的是异常流,在这里Exception不代表程序出现了异常或者错误,Exception只是面向对象化的业务逻辑控制方法。如果没有明白这一点,那么我认为并没有真正明白应该怎么使用Java来正确的编程。

而我自己写的程序,会自定义大量的Exception类,所有这些Exception类都不意味着程序出现了异常或者错误,只是代表非主事件流的发生的,用来进行那些分支流程的流程控制的。例如你往权限系统中增加一个用户,应该定义1个异常类,UserExistedException,抛出这个异常不代表你插入动作失败,只说明你碰到一个分支流程,留待后面的catch中来处理这个分支流程。传统的程序员会写一个if else来处理,而一个合格的OOP程序员应该有意识的使用try catch 方式来区分主事件流和n个分支流程的处理,通过try catch,而不是if else来从代码上把不同的事件流隔离开来进行分别的代码撰写

很多人喜欢定义方法的返回类型为boolean型的,当方法正确执行,没有出错的时候返回true,而方法出现出现了问题,返回false。这在Java编程当中是大错而特错的!

方法的返回值只意味着当你的方法调用要返回业务逻辑的处理结果的。如果业务逻辑不带处理结果,那么就是void的,不要使用返回值boolean来代表方法是否正确执行。

1
boolean login(String username, String password);

很多人喜欢用boolean返回,如果是true,就是login了,如果false就是没有登陆上。其实是错误的。还有的人定义返回值为int型的,例如如果正确返回就是0,如果用户找不到就是-1,如果密码不对,就是-2

1
int login(String username, String password);

然后在主程序里面写一个if else来判断不同的流程

1
2
3
4
5
6
7
int logon = UserManager.login(xx,xx);;  
if (logon ==0); {
...
} else if (logon == 1); {
...
} else if (logon ==2); {
..}

这是面向过程的编程逻辑,不是面向对象的编程逻辑。

应该这样来写:

1
User login(String username, String password); throws UserNotFoundException, PasswordNotMatchException; 

主程序这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try {  
UserManager.login(xx,xx);;
....
用户登陆以后的主事件流代码

} catch (UserNotFoundException e); {

...

用户名称没有的事件处理,例如产生一个提示用户注册的页面

} catch (PasswordNotMatchException e); {

....

密码不对的事件处理,例如forward到重新登陆的页面
}

看到这个示例,似乎明显违背了最佳实践的第一条:不要用来流程控制

如果这不是流程控制,那这种写法与流程控制有什么区别呢?再进一步,什么时候使用异常呢?

什么时候使用异常

在异常最佳实践中:异常只用于异常情况下!

需要捕捉的异常也有两种,一种是自己的程序抛出的,一种是系统抛出的

什么叫做程序抛出的异常,什么叫做系统抛出的异常,你能明确界定吗?FileNotFoundException你说算是系统异常呢?还是程序异常?站在某些程序员的角度,他会觉得是系统异常,不过像我喜欢看JDK源代码的人来说,我对Sun的程序什么情况下抛出FileNotFoundException很清楚,这些代码对我来说,和我自己写的代码能有什么不同吗?对我来说,FileNotFoundException就是程序异常。既然JDK可以抛出异常,凭什么我就不能抛出异常?

站在底层程序员的角度来看,根本没有什么系统异常可言,否则的话,还不如不要定义任何异常得了,干脆就是函数调用返回值,你说为什么Sun不定义0,1,2这样的返回值,而是抛出异常呢?Java程序无非就是一堆class,JDK的class可以抛异常,我写的class为什么不能抛出?

异常不异常的界定取决于你所关注的软件层面,例如你是应用软件开发人员,你关心的是业务流程,那么你就应该捕获业务层异常,你就应该定义业务层异常,向上抛出业务层异常。如果是底层程序员,你就应该定义和抛出底层异常。要不要抛出异常和抛出什么异常取决你站在什么软件层面了,离开这个前提,空谈异常不异常是没有意义的

因为0,1,2这样的值表达的含义不够丰富,但是作为返回值,又不合理。
————函数有它的本身的返回值。

因此,返回一个异常,其实就是一个封装完好的,返回的对象。这个对象Type不是在函数名的前面说明,而是在一个更加特别的地方,函数的后面说明。这就是异常的本质————非正常的返回值。这个返回值,为什么不能用传统的方法处理呢?因为Object x=method();表明它只能接受某一个特定的对象,如果出现Exception的对象,就会报错。因此需要catch来接手处理这样的返回值。

checked与unchecked选择

对于何时抛出异常,上面的论述大致已经清楚,使用Exception的关键是,你站在什么样的角度来看这个问题,这也得看大家对异常写法的习惯,异常并不只是单单的异常,在OO中,异常也是方法返回值的一部分

Java正统观点认为:已检查异常应该是标准用法,运行时异常表明编程错误,这也正如上面的例子,方法申明异常表明了有这些异常情况,那业务调用方需要考虑这些情况,但是检查异常引起了几个问题

  1. 太多的代码:开发人员将会因为不得不捕捉他们无法合理地处理的已检查异常(属于“某个东西出了可怕错误”种类)并编写忽略(忍受)它们的代码而感到无力。
  2. 难以读懂的代码:捕捉不能被正确地处理的异常并重新抛出它们没有执行一点有用的功能,反而会使查找实际做某件事的代码变得更困难
  3. 异常的无休止封装:一个已检查异常要么必须被捕捉,要么必须在一个遇到它的那个方法的抛出子句中被声明。这时要么重新抛出数量不断增长的异常,或者说捕捉低级异常,要么重新抛出被封装在一个较高级的新异常中的它们
  4. 易毁坏的方法签名
  5. 已检查异常对接口不一定管用

异常受检的本质并没有为程序员提供任何好处,它反而需要付出努力,还使程序更为复杂

被一个方法单独抛出的受检异常,会给程序员带 来非常高的额外负担。如果这个方法还有其他的受检异常,它被调用的时候一定已经出现在一个try块中,所以这个异常只需要别外一个catch块

非检查型异常的最大风险之一就是它并没有按照检查型异常采用的方式那样自我文档化。除非 API 的创建者明确地文档化将要抛出的异常,否则调用者没有办法知道在他们的代码中将要捕获的异常是什么

Rod Johnson采取了一种比eckel 稍正统的观点,因为Johnson认为已检查异常有一定用武之地,在一个异常相当于来自方法的一个可替代返回值得地方,这个异常无疑应该被检查,并且该语言能帮助实施这一点就再好不过了。但是觉得传统的java方法过分强调了已检查异常。


使用Checked Exception还是UnChecked Exception的原则,我的看法是根据需求而定。

如果你希望强制你的类调用者来处理异常,那么就用Checked Exception;
如果你不希望强制你的类调用者来处理异常,就用UnChecked。

那么究竟强制还是不强制,权衡的依据在于从业务系统的逻辑规则来考虑,如果业务规则定义了调用者应该处理,那么就必须Checked,如果业务规则没有定义,就应该用UnChecked。

还是拿那个用户登陆的例子来说,可能产生的异常有:

IOException (例如读取配置文件找不到)
SQLException (例如连接数据库错误)
ClassNotFoundException(找不到数据库驱动类)

NoSuchUserException
PasswordNotMatchException

以上3个异常是和业务逻辑无关的系统容错异常,所以应该转换为RuntimeException,不强制类调用者来处理;而下面两个异常是和业务逻辑相关的流程,从业务实现的角度来说,类调用者必须处理,所以要Checked,强迫调用者去处理。

在这里将用户验证和密码验证转化为方法返回值是一个非常糟糕的设计,不但不能够有效的标示业务逻辑的各种流程,而且失去了强制类调用者去处理的安全保障。

至于类调用者catch到NoSuchUserException和PasswordNotMatchException怎么处理,也要根据他自己具体的业务逻辑了。或者他有能力也应该处理,就自己处理掉了;或者他不关心这个异常,也不希望上面的类调用者关心,就转化为RuntimeException;或者他希望上面的类调用者处理,而不是自己处理,就转化为本层的异常继续往上抛出来。


Checked Exception与UnChecked Exception:

  1. 抛出Checked Exception,给直接客户施加一个约束,必须处理,但也是一种自由,客户可分门别类的处理不同异常;
    UnChecked Exception则给直接客户以自由,但也是一种欺瞒,因为客户不知道将要发生什么,所有的处理将是系统默认的处理(如打印堆栈到控制台,对开发者、用户都返回一样的内容,不管别人懂与不懂)。
    二者的选择其实是约束与自由的权衡。
  2. “对可恢复的情况使用已检查异常,对程序错误使用运行时异常。”而不是一咕脑的全抛出Checker Exception,这服务提供者是友好的
  3. 所以,若不需要客户依据不同异常采取不同后续行为,那么抛出UnChecked Exception是友好的;但若客户需要根据不同异常类采取不同行动,抛出Checked Exception是友好的。

对于checked exception转unchecked exception,大家都有共识,只是偏执于两方哪一方多些,在前期还是后期

Rod Johnson在spring的data access exception中就是个好示例,一是把异常细分化,更明确具体异常;二是把检查异常SQLException都转化为了unchecked exception

ErrorCode

异常对代码和开发、维护及管理一个应用的人都有用是至关重要的

对于开发、维护人

异常消息串具有有限的价值:当这些消息串出现在日志文件中时,他们对解释问题可能是有帮助的,但它们将无法使调用代码正确地做出反应,并且不能依靠它们本身来把它们显示给用户。当不同的问题可能需要不同的动作时,相应的异常应该被建模为一个公用超类的独立子类。有时,该超类应该是抽象的。现在,调用代码将可自由地在相关的细节级别上捕捉异常

已检查异常要比错误返回码(许多老式的语言中使用)好很多。迟早(或许不久),人们将不能检查一个错误返回值;

使用编译程序来实施正确的错误处理时一件好事。同参数和返回值一样,这样的已检查异常对一个对象的api来说是整体的不可分部分

用户

应该通过在异常中包括错误代码来处理

1
2
3
String getErrorCode();

String getMessage();

在spring早期代码中,就有ErrorCoded接口定义这两个方法,errorCode能够把为终端用户而计划的错误与为开发人员而计划的错误消息区分开。getMessage()用户记录日志,并且是面向开发人员


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

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

架构

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

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

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

软件模块(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. 最后整理,确定技术选型,输出设计方案

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

究竟什么才是“软件架构”?架构师的工作内容究竟是什么?

架构

“架构”这个词给人的直观感受就充满了权力与神秘感,因此谈论架构总让人有一种正在进行责任重大的决策或者深度技术分析的感觉。毕竟,进阶到软件架构这一层次是我们走技术路线的人的终极目标

应用程序有两个层面需求,第一类是功能性需求;第二类是非功能性需求

架构的重要性就在于非功能需求,这些非功能需求决定一个应用程序在运行时质量,比如可扩展性和可靠性。它们也决定了开发阶段的质量,包括可维护性、可测试性、可扩展性和可部署性。

架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解、易于修改、方便维护、并且能轻松部署。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。

软件的系统架构应该为该系统的用例提供支持;软件系统的架构设计图也应该非常明确地凸显该应用程序会有哪些用例

架构设计不是与框架相关的,不应该是基于框架来完成,框架只是一个可用的工具和手段

一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例

不管什么样的架构,它们都具有同一个设计目标:按照不同关注点对软件进行切割。也就是这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层

计算机系统的软件架构是构建这个系统所需要的一组架构,包括软件元素、它们之间的关系以及两者的属性

所谓“架构”,是“以组件、组件之间的关系、组件与环境之间的关系为内容的某一系统的基本组织结构,以及指导上述内容设计与演化的原则”。之所以要确定系统的组件、组件关系以及设计与演化的原则,目的是通过不同层面的结构视图来促进团队的交流,为设计与开发提供指导。架构不仅仅是指我们设计产生的输出文档,还包括整个设计分析与讨论的过程,这个过程产生的所有决策、方案都可以视为是架构的一部分

架构这么多定义,怎么描述架构呢?

Phillip Krutchen在他经典的论文《Architectural Blueprints —The 4+1 View Model of Software Architecture》中提出了软件架构的4+1视图

  • 逻辑视图:开发人员创建的软件元素。在面向对象的语言中,这些元素是类和包。它们之间的关系是类和包之间的关系,包括继承、关联和依赖。

  • 实现视图:构建编译系统的输出。此视图由表示打包代码的模块和组件组成,组件是由一个或多个模块组成的可执行或可部署单元。在JAVA中,模块是JAR文件,组件通常是WAR文件或可执行JAR文件。它们之间的关系包括模块之间的依赖关系以及组件和模块之间的组合关系。

  • 进程视图:运行时的组件。每个元素都是一个进程,进程之间的关系代表进程间通信。

  • 部署视图:进程如何映射到机器。此视图中的元素由(物理或虚拟)计算机和进程组成。机器之间的关系代表网络。该视图还描述了进程和机器之间的关系。

除了这四个视图以外,4+1中的+1是指场景,它负责把视图串联在一起。每个场景负责描述在一个视图中的多个架构元素如何协作,以完成一个请求。例如,在逻辑视图中的场景,展现了类是如何协作的。同样,在进程视图中的场景,展现了进程是如何协作的

4+1视图是描述应用程序架构的绝佳方式。每一个视图都描述了架构的一个重要侧面。场景把视图中的元素如何协作串联在一起

良好的架构有如下特点:

  • 独立于框架
  • 要被测试
  • 独立于UI
  • 独立于数据库
  • 独立于任何外部机构

架构师

架构师干什么?画PPT吗?写不写代码?

首先,软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议

软件架构师应该是能力最强的一群程序员,他们通常会在自身承接编程任务的同时,逐渐引导整个团队向一个能够最大化生产力的系统设计方向前进

概括一下:

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

也不知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,让团队更有意愿地参与上花心思,让团队成为一个学习型组织,有工程师文化的组织

近两年设计了几个系统,不管是直接使用传统设计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分层》 以及《分层架构》

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

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

之前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系统文章,现在已经更新到每四篇,有很多异于常规的地方,收获良多,总结一下

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包被复用。当团队形成规范后,可以快速的定位到相关代码

作者的模块划分和依赖关系,细分析之后发现了些妙处

Infrastructure模块包含了Persistence、Messaging、External等模块。比如:Persistence模块包含数据库DAO的实现,包含Data Object、ORM Mapper、Entity到DO的转化类等。Persistence模块要依赖具体的ORM类库,比如MyBatis。如果需要用Spring-Mybatis提供的注解方案,则需要依赖Spring

Persistence从infrastructure剥离出来,解决了之前碰到的循环依赖问题(repository接口在domain层,但现实在infra层,可从maven module依赖讲,domain又是依赖infra模块的)

对于为什么拆分得这么细,是不是也解决这个问题,特意请教了作者,作者回复:

这块儿可拆可不拆,拆的好处是每个模块职责比较简单,但不拆问题也不大的
domain没必要依赖infra啊?domain里自带Repository接口,所以从maven角度来看,infra是依赖domain
infra只依赖application、domain即可。这里面还有一个Start的模块,把infra依赖进来,然后Spring的DI会自动注入的
这里,domain或app依赖的外部的接口而已,这个一般是独立的jar包
这个也可以是ACL里面的Facade,但是具体的调用实现还是infra

之前也思考过,的确得DIP,domain在最下层,infra在上面,但有个问题,把对外部依赖的接口都是放在infra里面的,所以倒置不了,以及之前在《DDD分层》里面提到的

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

对于这段话,发给作者,让他点评了一下,作者回复:

EventPublisher接口就是放在Domain层,只不过namespace不是xxx.domain,而是xxx.messaging之类的
这里面2个概念,一个是maven module怎么搞,一个是什么是Domain层。这两个不是同一件事
Repository、eventpublisher接口再Domain这个module里,但是他们从理论上是infra层的。

我一般的理解:从外部收到的,属于interface层,比如RPC接口、HTTP接口、消息里面的消费者、定时任务等,这些需要转化为Command、Query、Event,然后给到App层。
App主动能去调用到的,比如DB、Message的Publisher、缓存、文件、搜索这些,属于infra层

所以消息相关代码可能会同时存在2层里。这个主要还是看信息的流转方式,都是从interface-》Application-〉infra


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开篇总结》的前三篇已经阐述了几个内容

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