196919831995200220042009
结构化面向对象设计模式设计原则DDDDCI

序言

20 世纪 60 年代前,计算机起步,软件用机器码或汇编写,规模小,文档、系统化开发方法通常也没有

60 年代中期 高级语言(FORTRAN)、os(IBMSYS)、第一代数据库(IMS)发展

软件规模,复杂性,可靠性问积累爆发软件危机,落后的软件生产方式无法满足软件增长需求,开发与维护出现一系列严重问题:

  • 开发费用和进度失控
  • 可靠性差
  • 难以维护

1968 年国际会议讨论软件危机,正式提出 “软件工程” ,新兴的工程学科应运而生

结构化程序设计

模块化设计为中心,分若干独立模块,每模块工作单纯而明确,为设计较大软件打下良好基础

结构化程序设计观点,任何算法功能都可以由三种基本程序结构的组合: 顺序结构、选择结构和循环结构来实现

  1. 自顶向下,逐步求精

  2. 模块化,模块间通过接口传递信息

  3. 语句结构化

结构化程序设计的概念、方法和支持这些方法的一整套软件工具,构成了结构化革命

软件发展中的第三个里程碑,其影响比前两个里程碑(子程序、高级语言)更为深远

1973 年初,C 语言主体开发完成,逐步成为结构化编程语言中最流行的语言

Nicklaus Wirth著名的公式:程序 = 数据结构 + 算法

“面向过程” 在 “面向对象” 出现之后为与之相对而提出的,可看作是 “结构化” 的别名

面向对象程序设计

软件复杂后,结构化程序设计弱点:

  1. 审视问题域的视角

现实世界中存在的客体是问题域中的主角(客体指客观存在的对象实体和主观抽象的概念,人类观察问题和解决问题的主要目标)

例如,学生管理系统,围绕学生和老师客体

结构化设计不将客体作为一个整体,而将依附于客体之上的行为抽取出来,以功能为目标来设计构造应用系统

将客体所构成的现实世界映射到由功能模块组成的解空间中,增加程序设计复杂程度,背离了人观察和解决问题的基本思路

问题域中,客体是稳定的,行为是不稳定的

国家、学校、国际图书馆,都含有图书这个客体,但管理图书的方法可能是截然不同的

结构化设计方法将审视问题的视角定位于不稳定的操作之上,并将描述客体的属性和行为分开,程序日后维护和扩展相当困难,甚至一个微小的变动,都会波及到整个系统

改变软件设计方法与人类解决问题的常规方式扭曲的现象是提出面向对象的首要原因

2、抽象级别

抽象是人类解决问题的基本法宝,良好的抽象策略可以控制问题的复杂程度,增强系统的通用性和可扩展性

抽象主要包括过程抽象和数据抽象

结构化设计用的是过程抽象

将问题域中具有明确功能定义的操作抽取出来,并将其作为一个实体看待

这种抽象级别对于软件系统结构的设计显得有些武断,并且稳定性差,很难准确无误地设计出系统的每一个操作环节

一旦某个客体属性的表示方式发生了变化,就有可能牵扯到已有系统的很多部分

数据抽象是较过程抽象更高级别的抽象方式,将描述客体的属性和行为绑定在一起,实现统一的抽象,达到对现实世界客体的真正模拟

3、封装体

将现实世界中存在的某个客体的属性与行为绑定在一起,放置在一个逻辑单元内

结构化设计没有做到客体的整体封装,只是封装了各个功能模块,而每个功能模块可以随意地对没有保护能力客体属性实施操作

并且由于描述属性的数据与行为被分割开来,所以一旦某个客体属性的表达方式发生了变化,就有可能对整个系统产生影响

4、可重用性

标识着产品可复用能力,衡量软件成功与否的重要标志

结构化程序设计方法的基本单位是模块,每个模块只是实现特定功能的过程描述,可重用单位只能是模块

参与操作的某些数据类型发生变化时,就不能够再使用那些函数了

面向对象技术基本特征主要有 抽象性、封装性、继承性和多态性

20 世纪 80 年代,面向对象程序设计在业界大行其道,逐渐成为主流

C++(1983)恰好在这个时期诞生

面向对象是一种思想,它让我们在分析和解决问题时,把思维和重点转向现实中的客体中来,然后通过 UML 等工具理清这些客体之间的联系,最后用面向对象的语言实现这种客体以及客体之间的联系

面向对象的分析 (OOA)、设计 (OOD) 、编程实现 (OOP) 三个大的步骤

  1. 分析需求,先不要思考怎么用程序实现它,先分析需求中稳定不变的客体都是些什么,这些客体之间的关系是什么

  2. 第一步分析出来的需求,通过进一步扩充模型,变成可实现的、符合成本的、模块化的、低耦合高内聚的模型

  3. 使用面向对象的实现模型

设计模式

设计面向对象的软件比较困难,设计可复用的面向对象的软件更加困难

必须找到相关的对象,以适当的粒度将它们归类,再定义类的接口和继承层次,建立对象之间的基本关系

面向对象设计,新手则面对众多选择无从下手,总是求助于以前使用过的非面向对象技术

GoF 将模式概念引入软件工程领域,标志着软件模式的诞生

设计原则

设计原则是设计模式的灵魂

面向对象软件系统的设计而言,支持可维护性同时,提高系统可复用性至关重要

面向对象设计原则是对面向对象思想的提炼,比面向对象思想核心要素(封装、继承和多态)更具可操作性,但与设计模式相比,却又更加的抽象

形象的讲,面向对象思想类似法理的精神,设计原则类似基本宪法,设计模式好比各式各样的具体法律条文

面向对象设计原则是用于评价一个设计模式的使用效果的重要指标之一,设计模式的学习中,经常会看到诸如 “XXX 模式符合 YYY 原则”、“XXX 模式违反了 ZZZ 原则” 这样的语句

对于设计原则,比如 SOLID 原则和迪米特法则,大家都能耳熟能详,但大多数人对它们的理解都不太深入,建议初学者精读 Robert C. Martin 在 2002 年的经典著作《敏捷软件开发—原则、模式与实践》

领域驱动设计

领域 > 分析师 > 分析模型 > 设计师 > 设计模型 > 程序员 > 实现

分析模型和设计模型的分离,导致分析师头脑中业务模型和设计师头脑中业务模型不一致,通常要映射一下

伴随着重构和 bug fix 的进行,设计模型不断演进,和分析模型的差异越来越大

有时分析师站在分析模型角度认为某个需求较容易实现,设计师站在设计模型的角度认为该需求较难实现,双方都很难理解对方的模型

长此以往,在分析模型和设计模型之间就会存在致命的隔阂,从任何活动中获得的知识都无法提供给另一方

Eric Evans 2004 出版领域驱动设计(DDD, Domain-Driven Design)开山之作《领域驱动设计——软件核心复杂性应对之道》,抛弃将分析模型与设计模型分离的做法,寻找单个模型来满足两方面的要求,这就是领域模型

许多系统的真正复杂之处不在于技术,而在于领域本身,在于业务用户及其执行的业务活动

设计时没有获得对领域的深刻理解,没有通过模型将复杂的领域逻辑以模型概念和模型元素的形式清晰地表达出来,无论使用多么先进、多么流行的平台和设施,都难以保证项目的真正成功

领域驱动设计两个阶段:

1、以领域专家、设计人员和开发人员都能理解的通用语言作为相互交流的工具,交流的过程中发现领域概念,然后将这些概念设计成一个领域模型

2、由领域模型驱动软件设计,用代码来表达该领域模型

领域驱动设计的核心是建立正确的领域模型

领域专家、设计人员和开发人员一起创建一套适用于领域建模的通用语言,通用语言必须在团队范围内达成一致

所有成员都使用通用语言进行交流,每个人都能听懂别人在说什么,通用语言也是对软件模型的直接反映。领域专家、设计人员和开发人员一起工作,这样开发出来的软件能够准确的表达业务规则。领域模型基于通用语言,是关于某个特定业务领域的软件模型

一个通用领域驱动设计的架构性解决方案包含四个概念层,就是经典的四层模型

1、User Interface 向用户展现信息以及解释用户命令

2、Application 很薄的一层,定义软件要完成的所有任务

对外为展现层提供各种应用功能(包括查询或命令),对内调用领域层(领域对象或领域服务)完成各种业务逻辑,应用层不包含业务逻辑

3、Domain 领域层,负责表达业务概念,业务状态信息以及业务规则,领域模型处于这一层,是业务软件的核心

4、Infrastructure 层为基础实施层,向其他层提供通用的技术能力;提供了层间的通信;为领域层实现持久化机制;总之,基础设施层可以通过架构和框架来支持其他层的技术需求

DCI 架构模式

James O. Coplien 和 Trygve Reenskaug 在 2009 年发表论文《DCI 架构:面向对象编程的新构想》,标志着 DCI 架构模式的诞生 James O. Coplien 也是 MVC 架构模式的创造者,大叔一辈子就干了两件事,即年轻时创造了 MVC 和年老时创造了 DCI,其他时间都在思考,让我辈望尘莫及

面向对象编程本意是将程序员与用户的视角统一于代码中:对提高可用性和降低程序理解难度都是一种恩赐 可是虽然对象很好地反映了结构,但在反映系统的动作方面却失败了,DCI 的构想是期望反映出最终用户的认知模型中的角色以及角色之间的交互

传统上,面向对象编程语言拿不出办法去捕捉对象之间的协作,反映不了协作中往来的算法 就像对象的实例反映出领域结构一样,对象的协作与交互同样是有结构的

协作与交互也是最终用户心智模型的组成部分,但在代码中找不到一个内聚的表现形式去代表它们

本质上,角色体现的是一般化、抽象的算法。角色没有血肉,并不能做实际的事情,归根结底工作还是落在对象的头上,而对象本身还担负着体现领域模型的责任

人们心目中对 “对象” 这个统一的整体却有两种不同的模型,即 “系统是什么” 和 “系统做什么”,这就是 DCI 要解决的根本问题

用户认知一个个对象和它们所代表的领域,而每个对象还必须按照用户心目中的交互模型去实现一些行为,通过它在用例中所扮演的角色与其他对象联结在一起。正因为最终用户能把两种视角合为一体,类的对象除了支持所属类的成员函数,还可以执行所扮演角色的成员函数,就好像那些函数属于对象本身一样。换句话说,我们希望把角色的逻辑注入到对象,让这些逻辑成为对象的一部分,而其地位却丝毫不弱于对象初始化时从类所得到的方法。我们在编译时就为对象安排好了扮演角色时可能需要的所有逻辑。如果我们再聪明一点,在运行时知道了被分配的角色,才注入刚好要用到的逻辑,也是可以做到的。

算法及角色 - 对象映射由 Context 拥有。Context“知道” 在当前用例中应该找哪个对象去充当实际的演员,然后负责把对象 “cast” 成场景中的相应角色。(cast 这个词在戏剧界是选角的意思,此处的用词至少符合该词义,另一方面的用意是联想到 cast 在某些编程语言类型系统中的含义。)在典型的实现里,每个用例都有其对应的一个 Context 对象,而用例涉及到的每个角色在对应的 Context 里也都有一个标识符。Context 要做的只是将角色标识符与正确的对象绑定到一起。然后只要触发 Context 里的 “开场” 角色,代码就会运行下去。

于是有了完整的 DCI 架构(Data、Context 和 Interactive 三层架构):

  1. Data 层描述系统有哪些领域概念及其之间的关系,该层专注于领域对象和之间关系的确立,让程序员站在对象的角度思考系统,从而让 “系统是什么” 更容易被理解

  2. Context 层:尽可能薄的一层。Context 往往被实现得无状态,只是找到合适的 role,让 role 交互起来完成业务逻辑即可。但是简单并不代表不重要,显示化 context 层正是为人去理解软件业务流程提供切入点和主线

  3. Interactive 层主要体现在对 role 的建模,role 是每个 context 中复杂的业务逻辑的真正执行者,体现 “系统做什么”。Role 所做的是对行为进行建模,它联接了 context 和领域对象。由于系统的行为是复杂且多变的,role 使得系统将稳定的领域模型层和多变的系统行为层进行了分离,由 role 专注于对系统行为进行建模。该层往往关注于系统的可扩展性,更加贴近于软件工程实践,在面向对象中更多的是以类的视角进行思考设计

DCI 目前广泛被作为对 DDD 的一种发展和补充,用于基于面向对象的领域建模

显式的对 role 进行建模,解决了面向对象建模中充血和贫血模型之争 DCI 通过显示的用 role 对行为进行建模,同时让 role 在 context 中可以和对应的领域对象进行绑定 (cast),既解决了数据边界和行为边界不一致的问题,也解决了领域对象中数据和行为高内聚低耦合的问题

面向对象建模一个棘手问题是数据边界和行为边界往往不一致 遵循模块化的思想,通过类将行为和其紧密耦合的数据封装在一起。但复杂的业务场景下,行为往往跨越多个领域对象,这样的行为放在某一个对象中必然导致别的对象需要向该对象暴漏其内部状态

所以面向对象发展到后来,领域建模出现两种派别之争,一种倾向于将跨越多个领域对象的行为建模在领域服务中。这种做法使用过度经常导致领域对象变成只提供一堆 get 方法的哑对象,这种建模导致的结果被称为贫血模型

另一派坚定认为方法应该属于领域对象,所有业务行为仍然被放在领域对象中,导致领域对象随着支持的业务场景变多而变成上帝类,而且类内部方法的抽象层次很难一致。另外由于行为边界很难恰当,导致对象之间数据访问关系也比较复杂。这种建模导致的结果被称为充血模型

DCI 和袁英杰大师提出的 “小类大对象” 殊途同归,即类应该是小的,对象应该是大的。上帝类是糟糕的,但上帝对象却恰恰是我们所期盼的。而从类到对象,是一种多对一的关系:最终一个对象是由诸多单一职责的小类——它们分别都可以有自己的数据和行为——所构成。而将类映射到对象的过程,在 Ruby 中通过 Mixin;在 Scala 中则通过 Traits;而 C++ 则通过多重继承

生活中的例子:

人有多重角色,不同的角色履行的职责不同:

1、作为父母:要给孩子讲故事,陪他们玩游戏,哄它们睡觉 2、作为子女:要孝敬父母 3、作为下属:在老板面前,需要听从其工作安排 4、作为上司:需要安排下属工作,并进行培养和激励 5、…

这里人(大对象)聚合了多个角色(小类),在某种场景下,只能扮演特定的角色:

1、在孩子面前,我们是父母 2、在父母面前,我们是子女 3、职场上,在上司面前,我们是下属 4、在下属面前,你是上司 5、…

对于通信系统软件,没有 UI 层,应用层也很薄,所以传统的 DDD 的四层模型并不适用

DCI 提出后,针对通信系统软件,将 DDD 的分层架构重新定义一下

1、Schedule 调度层,维护 UE 的状态模型,除过业务本质状态,还有实现状态。调度层收到消息后,将委托 Context 层的 Action 进行处理

2、Context 环境层(对应 DCI 中 Context),以 Action 为单位,处理一条同步消息或异步消息,将 Domain 层的领域对象 cast 成合适的 role,让 role 交互起来完成业务逻辑

3、Domain 层定义领域模型,不仅包括领域对象及其之间关系的建模(对应 DCI 中的 Data),还包括对象的角色 role 的显式建模(对应 DCI 中的 Interaction)

4、Infrastructure 基础实施层,为其他层提供通用的技术能力;提供了层间的通信;为领域层实现持久化机制;总之,基础设施层可以通过架构和框架来支持其他层的技术需求

领域专用语言

DSL(Domain Specific Language)针对某个特定领域而开发的语言

C、Java 等都属于通用语言,可以为各个领域编程,通用性有余,但针对性不强,DSL 为弥补通用语言这个劣势而出现

软件开发 “教父”Martin Fowler 在 2010 出版的《领域特定语言》是 DSL 领域的丰碑之作,掀起来 DSL 编程的热潮

实际面向对象编程中,大家会自觉不自觉的使用 DSL 的一些方法和技巧

比如,如果定义了一些非常面向业务的函数,然后这些函数的集合就可以被看作一种 DSL 了

虽然 DSL 和面向业务的函数之间是有一些类似之处,但这只是问题的一个方面,DSL 更多是从客户的角度出发看待代码,定义函数则更多的从解决问题的方案的角度看待代码

诚然两者都有交集,但是出发点却截然不同

按 Martin Fowler 的看法,DSL 可以分为两种基本类型,内部 DSL 和外部 DSL

外部 DSL 相当于实现一种编程语言,也许不如实现一门通用语言那么复杂,但是工作量不小

内部 DSL 就是在一种通用编程语言的基础上进行关键字的定义封装来达到 DSL 的目的,这种 DSL 的扩展性可能会受到母语言的影响,对于不熟悉母语言的人来说可能不是那么好理解,不过好处就是可以利用母语言本身的功能

transaction DSL 是一种内部 DSL(it is C++),用于降低业务的实现复杂度,使得调度层只需处理业务的本质状态,而所有非稳态都是原子的事务过程

有了 transaction DSL 之后,针对通信系统软件的 DDD 四层模型可以演进为五层模型

1、Schedule 是调度层,维护 UE 的状态模型,只包括业务的本质状态,将接收到的消息派发给 transaction DSL 层。

2、transaction DSL 是事务层,对应一个业务流程,比如 UE Attach,将各个同步消息或异步消息的处理组合成一个事务,当事务失败时,进行回滚。当事务层收到调度层的消息后,委托环境层的 Action 进行处理。

3、Context 是环境层(对应 DCI 中的 Context),以 Action 为单位,处理一条同步消息或异步消息,将 Domain 层的领域对象 cast 成合适的 role,让 role 交互起来完成业务逻辑。

4、Domain 层定义领域模型,不仅包括领域对象及其之间关系的建模(对应 DCI 中的 Data),还包括对象的角色 role 的显式建模(对应 DCI 中的 Interaction)。

5、Infrastructure 基础实施层,为其他层提供通用的技术能力;提供了层间的通信;为领域层实现持久化机制;总之,基础设施层可以通过架构和框架来支持其他层的技术需求

微服务架构模式

软件 “教父”Martin Fowler 在 2012 年提出微服务这一概念,于是出现了两种服务架构模式,即单体架构模式和微服务架构模式

微服务是指开发一个单个小型的但有业务功能的服务,可以选择自己的技术栈和数据库,可以选择自己的通讯机制,可以部署在单个或多个服务器上。这里的“微”不是针对代码行数而言,而是说服务的范围不能大于DDD中的一个BC(Bounded Context,限界上下文)

微服务架构模式的优点:

  1. 微服务只关注一个BC,业务简单
  2. 不同微服务可由不同团队开发
  3. 微服务是松散耦合的
  4. 每个微服务可选择不同的编程语言和工具开发
  5. 每个微服务可根据业务逻辑和负荷选择一个最合适的数据库

微服务架构模式的挑战:

  1. 分布式系统的复杂性,比如事务一致性、网络延迟、容错、对象持久化、消息序列化、异步、版本控制和负载等

  2. 更多的服务意味着更高水平的DevOps和自动化技术

  3. 服务接口修改会波及相关的所有服务

  4. 服务间可能存在重复的功能点

  5. 测试更加困难