软件设计的演变过程

“Design is there to enable you to keep changing the software easily in the long term” —— Kent Beck

软件设计的演变

软件设计的演变过程分析模型和设计模型的分离,会导致分析师头脑中的业务模型和设计师头脑中的业务模型不一致,通常要映射一下。伴随着重构和bug fix的进行,设计模型不断演进,和分析模型的差异越来越大。有些时候,分析师站在分析模型的角度认为某个需求较容易实现,而设计师站在设计模型的角度认为该需求较难实现,那么双方都很难理解对方的模型。长此以往,在分析模型和设计模型之间就会存在致命的隔阂,从任何活动中获得的知识都无法提供给另一方。

Eric Evans在2004年出版了领域驱动设计(DDD, Domain-Driven Design)的开山之作《领域驱动设计——软件核心复杂性应对之道》,抛弃将分析模型与设计模型分离的做法,寻找单个模型来满足两方面的要求,这就是领域模型。许多系统的真正复杂之处不在于技术,而在于领域本身,在于业务用户及其执行的业务活动。如果在设计时没有获得对领域的深刻理解,没有通过模型将复杂的领域逻辑以模型概念和模型元素的形式清晰地表达出来,那么无论我们使用多么先进、多么流行的平台和设施,都难以保证项目的真正成功。

领域驱动设计分为两个阶段:

  1. 以一种领域专家、设计人员和开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型;
  2. 由领域模型驱动软件设计,用代码来表达该领域模型。

由此可见,领域驱动设计的核心是建立正确的领域模型

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

ddd-layer
  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. 作为上司:需要安排下属工作,并进行培养和激励;

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

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

对于通信系统软件,没有UI层,应用层也很薄,所以传统的DDD的四层模型并不适用。DCI提出后,针对通信系统软件,我们将DDD的分层架构重新定义一下,如下图所示:

软件设计的演变过程有了transaction DSL之后,针对通信系统软件的DDD四层模型可以演进为五层模型,如下图所示:
软件设计的演变过程微服务是指开发一个单个小型的但有业务功能的服务,可以选择自己的技术栈和数据库,可以选择自己的通讯机制,可以部署在单个或多个服务器上。这里的“微”不是针对代码行数而言,而是说服务的范围不能大于DDD中的一个BC(Bounded Context,限界上下文)。

微服务架构模式的优点:

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

微服务架构模式的挑战:

  1. 分布式系统的复杂性,比如事务一致性、网络延迟、容错、对象持久化、消息序列化、异步、版本控制和负载等
  2. 更多的服务意味着更高水平的DevOps和自动化技术
  3. 服务接口修改会波及相关的所有服务
  4. 服务间可能存在重复的功能点
  5. 测试更加困难

尽管微服务架构模式对“个子”的要求比较高,但随着容器云技术的不断成熟,微服务架构模式却越来越火,似乎所有系统的架构都在尽情拥抱微服务,这是不是意味着单体架构模式不再是我们的选择了呢者认为需要根据具体情况而定,我们看看下面这张图:

软件设计的演变过程上图直观的说明了单体架构和微服务架构在不同系统复杂度下不同的生产力,以及两者的对比关系。对于那种需要快速为商业模式提供验证的系统,在其功能较少和用户量较低的情况下,单体架构模式是更好的选择,但在单体架构内部,需要清晰的划分功能模块,尽量做到高内聚低耦合。
总而言之,微服务架构有很多吸引人的地方,不过在拥抱微服务之前要认清它所带来的挑战。每一种架构模式都有其优缺点,我们需要根据项目和团队的实际情况来选择最合适的架构模式

小结

本文较为详细的阐述了软件设计的演变过程,包括结构化程序设计、面向对象程序设计、设计模式、设计原则、DDD、DCI、DSL和微服务架构模式,通过对这些设计思想的全面梳理,可以帮助我们做出更好的设计决策。

作者:张晓龙
链接:https://www.jianshu.com/p/18d1d582f5c2
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

来源:皮皮机器人

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2019年4月22日
下一篇 2019年4月22日

相关推荐