这片文章想讲一下软件架构的基础,一些基本的原则和思想。我理解的架构是指导我们设计和实现软件的方法论;这意味着我们对复杂度、可扩展性、稳定性(分别对应迭代上手成本、迭代效率、事故损失)这三个方面有非常高的要求,尤其是复杂度,对修改代码的影响范围、成本、难易程度都有显著的影响,是架构要解决的核心问题。
另外,我们也不额外考虑性能,因为业务中的性能瓶颈距离我们讨论的问题往往还有几个数量级,往往是异步、流程优化、io等待等方面更加能有作为,采取什么样的设计方法能起到的作用微乎其微。

原则

关于原则,我想说一句话,原则是开发迭代中总结出来的经验,而不是开发迭代的规范。判断是不是原则适用的场景(而不是无脑用原则)往往更加有效,也更加困难。

KISS: Keep it simple, stupid,不要引入不必要的复杂
DRY: Don‘t repeat youself,不要重复代码
迪米特法则: The Least Knowledge Principle,使用者不知道实现细节
SOLID原则:

  • S 单一职责原则:对象应该仅具有一种单一功能
  • O 开闭原则:软件的目标应该是可扩展,不可修改
  • L 里氏替换原则:子类在任何代码中替换父类,行为一致
  • I 接口隔离原则:多个特定接口要好于一个宽泛用途的接口
  • D 依赖反转原则:抽象不应该依赖于具体,具体应当依赖于抽象(DDD就是这样,实际需求 ->抽象领域 -> 实现细节)

上面这些法则,几乎都在说:
代码简单可复用,架构高内聚低耦合

OOP还是FP

面向对象还是面向过程(函数式)?
这里的面向过程是对过程做结构化抽象,并不是平摊代码,这种明显的靶子行为不在我们讨论范围内。

我们先看看面向对象的核心特征(Java为例):封装、继承、多态

  • 封装:和自己数据有关的操作封装成成员方法,并控制可见性
  • 继承:得到父类的属性和方法
  • 多态:不同类型调用同一个成员方法会产生不同的结果

我们看看非面向对象的做法(Go为例):

  • 封装:和自己数据有关的操作封装成接收器,并控制可见性(非常类似);和自己数据无关的操作,直接做成普通函数
  • 继承:得到别人的属性和方法都用组合的方式,避免了构造函数、重载、层次设计
  • 多态:类型之间没有继承关系,不同类型实现同一个接口的方法。调用的时候按自己的类型去做出对应行为

编程的宗派,王垠
封装:函数和数据的耦合

对象作为数据访问的方式,是有一定好处的。然而“面向对象”(多了“面向”两个字),就是把这种本来良好的思想东拉西扯,牵强附会,发挥过了头。很多面向对象语言号称“所有东西都是对象”(Everything is an Object),把所有函数都放进所谓对象里面,叫做“方法”(method),把普通的函数叫做“静态方法”(static method)。实际上呢,只有极少需要抽象的时候(为访问对象数据提供间接的封装),你需要使用内嵌于对象之内,跟数据紧密结合的“方法”。其他的时候,你其实只是想表达数据之间的变换操作,这些完全可以用普通的函数表达,而且这样做更加简单和直接。
这种把所有函数放进方法的做法是本末倒置的,因为函数并不属于对象。绝大部分函数是独立于对象的,它们不能被叫做“方法”。强制把所有函数放进它们本来不属于的对象里面,把它们全都作为“方法”,导致了面向对象代码逻辑过度复杂。很简单的想法,非得绕好多道弯子才能表达清楚。很多时候这就像把自己的头塞进屁股里面。

继承VS组合

不要仅仅为了代码复用而继承。当你使用组合来实现代码复用的时候,是不会产生继承关系的。过度使用继承的时候,如果修改了父类,会损坏所有的子类。这是因为子类和父类存在紧耦合。
不要仅仅为了多态而继承。如果你的类之间没有继承关系,并且你想要实现多态,那么你可以通过接口+组合的方式来实现,这样不仅可以实现代码重用,同时也可以实现运行时的灵活性。

考虑到实际的工程往往都是多层次的组合和复用,继承能带来的收益非常小,但是后期的维护成本却是很难评估的。所以我建议在任何时候都不要使用继承。

我们讨论完面向对象的核心特质:封装、继承、多态,发现这些行为的滥用都需要格外地小心。这也是为什么工业界越来越多的go,仅仅从这一章节讨论的结果来说,go汲取了面向对象(数据封装、成员方法)和函数式(接口、一等公民)的优点,取消了他们诸多迷惑的特性,这是在任何语言都可以借鉴的行为。

为什么我们需要DDD

数据和方法

数据是肉,方法是血。按数据里有多少量的方法,我们划分为下面几种模型

失血模型

失血模型中,领域对象相当于一个数据实体,里面只有get和set方法。没有dao,service直接操作数据库。

  • service:操作数据库、做业务逻辑
  • model/do:数据实体和一些get/set方法

贫血模型

贫血模型中,领域对象包含了自己的原子领域逻辑,而组合逻辑在Service层。

  • service :跨领域的组合服务
  • model/manager:领域原子服务
  • dao/do:数据实体和一些get/set方法

Note:如果没有model层,就是传统的MVC架构,其实是个失血模型

优点:
系统层次清楚
单向依赖

充血模型

充血模型中,绝大多业务逻辑都应该被放在领域对象里面,包括持久化逻辑,而Service层是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。

  • service :组合服务 也叫事务服务
  • model:包含get/set方法、数据持久化、原子服务的逻辑

对开发者的要求高?还是模型使用不合理?

对比项目 贫血模型 充血模型
结构 service-model-dao service-model
面向 面向过程 面向对象
组装方式 数据和业务逻辑是不同的类 业务逻辑类继承数据类
和复杂度关系[争议] 适用于业务逻辑简单 适用于业务逻辑复杂
实现方式 userManager.save(User user) user.save()

领域的名词