软件开发是一个复杂的过程,主要体现在两个方面:业务复杂度和技术复杂度两个方面。其原因归纳起来主要有四个:
(1)识别问题域的困难(研发前)
业务繁杂、多变,既要考虑功能性需求,还要考虑非功能性需求(如性能、成本、可靠性等)。
用户或业务的想法天马行空,无法准确描述和定义明确的诉求,更别说能提出结构化的功能设计了。
(2)开发过程管理的困难(研发中)
开发过程中面临不确定性因素很多,如需求的变更、关联方的协作,任务的拆分、成员之间因认知的差异导致与实现与需求的偏差等等。随着团队规模的扩大,为了要达成认知的统一,人员之间的沟通也变得繁琐和困难。(沟通频率变高、内容变多)
具体到开发某一功能时,系统某一状态值会可能受到很多个变量的影响,这些变量状态值的相互组合往往不太容易预测,调试也会耗费较大精力。
(3)开发语言本身的灵活性为研发人员在研发过程中随意的设计带来了便利性,如果没有规范和约束的限制,则更加容易引发混乱。(研发中)
(4)用离散的软件系统来对连续的现实世界进行建模,本身就容易受外部因素的干扰。(研发后)
在面向对象出现之前,已有面向过程的分析方法,为什么面向对象被提出了呢?究其本质原因,人们发现面向过程并不是按照人正常认识事物的方式去分析软件,那么人究竟是怎么认识事物的呢,Yourdon 在《面向对象的分析》一书中提到,人类认识事物是遵循分类学的原理,分类学主要包含三点:区分对象及其属性;区分整体对象及其组成部分;不同对象类的形成及区分。
可以回想下我们认识事物的过程,是不是和分类学所提到的 3 个要点很相似,看到一个事物,大概会感知到它的组成结构是怎样的,形状是怎样的,属于什么分类。所以,人认识事物是以对象的视角切入的,然后赋于对象具体的概念,比如苹果、梨子、汽车等等概念名称。
面对的现实世界是非常复杂的,应对复杂事物的有一个重要的方法即是抽象,抽象在实际应用过程中,又体现在两种方法上:分层和分类。分类即是将有差异的事物归类到不同的分组中,正如我们常听到的"物以类聚、人以群分"的道理一样,产生分类的原因有两点:一点是事物间的关联紧密程度,不需要将所有的事物都耦合在一起;另一点是人掌握事物是有局限的,只能掌握少量的要点,比如 5~7 个要点,超过了容易忘记。
分层是通过不同的视角看事物,每一层的关注点是不一样的,这种关注点不同是由自己的视角造成的,比如我们理解计算机,并不需要深入到二进制电信号去理解计算机。层次特性在软件设计中我们经常遇到,比如计算机体系结构、TCP 七层协议等,层次特性有一个特点:越往上越具体、越往下越抽象,越往上的内容越不稳定,也即是容易变化。
我们把需要解决的问题称之为问题域,或者问题空间,把解决方案称之为解空间。正像前面提到的事物有层次特性,不同的人理解的事物是站在各自理解的视角,这样大家的理解、沟通并不一致的。如果我们看到的问题空间是表层的,那么基于浅层次理解设计出来的方案就会不稳定,可能下次有一个小变化导致方案需要重新设计。
我们可以把一个软件划分成三层:场景、功能和实体,场景层是经常会变的,比如发放优惠券场景就非常多,比如有天降红包领取优惠、分享有礼领取优惠券、新人注册领取优惠券等,这种场景的更迭随着业务的调整变化得非常快,因此场景层是不稳定的。功能支撑某一些的场景集合,对比场景,功能相对而言稳定些,就像前面提到的发放优惠券场景,本质就是给用户发放优惠券,只需要提供发放优惠券的功能即可,至于哪些场景来调用它并不关注,但功能还是基于场景的集合抽象出来的,如果场景场景类型变化了,功能也就随之变化,比如担保交易和预售交易就不一样。实体是稳定的,以担保交易和预售交易为例,它的订单模型大致是一样的,只是新增加了一些信息而已。
因此,我们希望从问题空间到解空间,大家看到的、理解的是一致的,而且看到的是问题的本质而非表象,往往场景、功能是不稳定的,而面向过程又是以功能驱动的,所以在易变化的场景下,它面临的问题就比较多。比较稳定的是问题空间中的实体对象,所以面向对象分析是现实的需要。面向过程和面向对象是两个不同的视角的分析方法:面向过程是一种归纳的分析方法,由外到内的过程;面向对象是一种演绎的分析方法,由内到外的过程。
面对复杂的软件系统,每个个体处理复杂系统的精力是有限的,我们该如何应对这些问题?自古以来我们运用“分治”的思想,把一个复杂的问题拆解为更细力度的小问题,逐个突破解决。
面向对象的方法通过分析问题域里的对对象,这些对象之间是互相独立的,没有交错的。更重要的是,面向对象可以对问题域逐层拆解,抽象不出层次不同的对象。
面向对象分析(Object-Oriented Analysis),在问题域内发现和描述对象。
面向对象设计(Object-Oriented Design),如何定义软件对象以及它们之间如何协作以实现需求。
在设计任务确立前,首先要进行调研和可行性研究,理解工作范围和所花费的代价,然后做出软件计划。
对用户要求进行具体分析。确定用户要求软件系统做什么,并用软件需求规格说明书表达出来,作为用户和软件人员之间共同的约定。
根据需求说明建立软件系统的结构,包括数据结构和模块结构。这部分又分为总体设计和详细设计两个阶段。
按软件设计的要求为每个模块编写程序。
软件测试
发现和排除程序中留存的错误。经过测试排错,得到可交付运行的软件。软件测试又分为单元测试和集成测试两个阶段。
软件维护
经过测试的软件仍然可能有错,另外,用户的需求和系统的操作环境也可能发生变化,因此,交付运行的软件仍然需要继续排错、修改和扩充。这就是软件的维护。
提到面向对象,有部分人会提到封装、继承、多态等特性,然后这些并不是面向对象的本质特性,比如封装,面向过程中也有封装,多态面向过程也有体现,这些特性算不上面向对象特有的特性。面向对象的底层逻辑是基于现实事物做的抽象映射:现实事物对应软件中的对象,我们讨论解空间能对应到问题空间中的对象,两者是一一直接映射的,其它的分析方法是问题空间到解空间的间接映射。
从顶层看,我们要完成需求到编码的工作,然而从需求到编码又会经过多个阶段,如需求分析、方案设计等,从大的层面讲,我们主要遇到三个问题:
看似这是一个简单的问题,但在复杂的业务场景下,对做什么的理解太重要了,因为不同的人对需求的理解是不同的,比如有一个项目,其中的一个业务判断规则是只针对跨境订单计税,最开始开发人员的理解是判断卖家类型是否是跨境卖家,然而到了测试阶段,发现大家对这个业务规则判断理解是不一致的,跨境订单跟卖家类型是没有关系的,真正的跨境订单计税场景是 shipTo(收货地址)和 shipFrom(发货地址)国家地址是不一样的。在大项项目中,涉及到多个团队之间的协同,这样的问题异常突出。而且从业务诉求到产品需求,再到技术方案,这其中是经过了 2 次变换,每次变换是不同的角色在里面,大家的认识也会不一样。
落实到事情具体要怎么做时,往往大家并不会出大的问题,怎么做偏具体执行阶段,程序员往往在逻辑严密性上没多大的问题,往往出问题是在第一个问题上,相当于方向弄错了,所做的工作也是无用的。
我们往往希望不劳而获得到一种万能的方法,能够应对所有的问题,同时又看不起低级的方法,比如大部分人对用例分析方法嗤之以鼻,想要能体现技术水平高大上的方法。其实自上世纪 70、80 年代,软件的分析设计方法并没有太大的变化,而且大都学过,只是大家并不认为它是一种高大上的方法而已。
软件开发会经历需要分析、概要设计、详细设计、编码、测试、上线主要阶段,我们不希望每块是割裂的,比如分析做完之后,做设计阶段又要重新去做分析的工作,那么这里面就涉及到一致性的问题,即需求到分析的一致性、分析到设计的一致性、设计到编码的一致性。这样做的好处可以保证无信息失真,因此我们急需求一种分析设计方法能做到这一点,面向对象分析与设计就能做到,因此全流程是以对象作为分析与设计的目标,在最终编码中也都是对象。
面向对象分析与设计的过程可以归纳为以下几个阶段:业务建模(需求模型)——>概念建模(领域模型)——>系统建模(设计模型)——>代码设计(实现模型)。
(1)业务建模,通过和业务沟通,结合行业经验和知识,明确用户的诉求,搞清楚业务想要什么。
(2)概念建模,基于需求模型,把松散的、非结构化的业务需求提炼出领域相关的概念并形成统一语言和结构化表达的业务规则。
(3)系统建模,基于领域模型,运用面向对象的设计方法和原则,完成类的设计。
(4)代码实现,以设计模型为基础,讲设计模型转化为具体的语言实现,完成编码。
以上流程各阶段环环相扣,上一步的输出时下一步的输入。
现实世界和对象世界有一道鸿沟,这道鸿沟就是「抽象」。抽象是面向对象的精髓所在,同时也是面向对象的困难所在。因此,我们需要一种工具和方法来帮助我们将OOAD落地,这种工具可以帮助我们从现实世界中抽象出对象世界。而UML(统一建模语言)正是帮助我们跨越「抽象」这道鸿沟的桥梁。
前面已经提到,问题空间到解空间是一一映射,我们讨论解空间中的对象时,其实它映射到问题空间中的对象,而问题空间中的对象主要来源于业务概念、业务规则、关键事件。大部分的对象是显式的,我们通过理解业务能发现,有的对象是隐性的,需要持续对业务有更深的理解才能发掘出来。好的对象模型是需要经过多次迭代打磨出来的,并非一次就能设计得十全十美。
如何发现对象呢?主要分成四个步骤:
当我们分析出一堆的对象后,还需要经过一定的组织,正如前面提到,人对事物理解是有局限的,不能一下子接受太多的事物,因此可以将它们分成一个个小的域,比如商品域、订单域、税务域等,这样当聚集一个问题时,可以只看某个子域里的对象模型即可。
面向对象最难的点有两个:一个是找出对象;另一个是分配职责。UML 把职责定义为“类元的契约或义务”,因此职责的划分从本质来讲还是类元本身决定的,比如订单,它要提供订单渲染、订单创建、订单修改、订单查询的业务。
职责分为两类:一类是认知职责;另一类是行为职责。
职责有两类,认知职责是对象自身的认知范围,即它只能基于自身属性完成相应的职责,举一个例子,假如一主多子的订单,要计算总的订单金额,怎么分配职责呢?首先商品只能查到自身价格的信息,它的认知是基于商品 price 属性,一个子订单可以有多个商品,那么它也只能计算出子订单的金额信息,它的认知是基于 item 和 quantity 两个属性,主订单包含所有子订单的信息,那么就可以计算出总的订单金额。
从中可以看出,认知职责是基于对象属性的,正所谓"不在其位、不谋其政",认知职责一定不会超过它的认识范围的。
行为职责是偏领域服务的,有的时候一个职责不属于某一个对象,比如转账,就是一个行为,让其它的职责承担并不合适,这类行为职责往往是一个显著的业务活动,比如订单渲染、订单创建就是行为职责而非认知职责。
分配职责一定要遵循"信息专家"模式,它的含义是将职责分配给具有完成该职责所需要信息的那个类,也即上面提到的认识产生职责。
我们期望分配的职责满足"高内聚、低耦合",怎么检验呢?我们再回过头来思考职责的定义:类元的契约或义务,换句话讲,职责是满足其它对象来调用的,这个就与我们画顺序图的目的是一致的,每次发生调用,即意味着其它的对象要提供一个职责出来,因此我们可以在顺序图中看对象间的调用频次,如果一个对象被调用得非常频繁,有可能这个对象承担了太多的职责,是不是可以对其拆分,把职责分配一部分出去。因此,对象职责分配并不是一蹴而就的,需要不断审视、检验。
分配职责是要遵循一定的原则,如创建者模式、信息专家模式、纯虚构模式等。
GRASP(General Responsibility Assignment Software Pattern)是通用职责分配软件设计模式。
它由《UML和模式应用》(Applying UML and Patterns)一书作者Craig Larman提出。在面向对象设计的过程中一般的通用方式是构思对象的职责、角色和协作。通常来说,在编码过程中先分析问题域,从中抽象出对象解决问题。简单的面向对象和优良的面向对象设计的区别在于将如何更合理的划分对象的角色,给对象赋予合理的职责以及对象之间的交互关系。
在GRASP九大原则、SOLID七大原则、GOF23种设计模式这三类设计原则、模式中。GRASP处于最上层,SOLID基于它进一步细化阐述,GOF再根据这些原则进一步的归纳出具体的模式。
GoF模式是针对特定问题而提出的解决方案,而GRASP站在一个更高的角度来看待面向对象软件的设计,它是GoF设计模式的基础。GRASP是对象职责分配的基本原则,其核心思想是职责分配,用职责设计对象。
UML不是OOA/D,也不是方法,仅仅是一种图形表示法。如果不掌握对象思想,UML或任何case工具(如Paradigm、ROSE等)将毫无意义。
UML即Unified Modeling Language 统一建模语言,帮助我们在OOA/D的过程中标识元素、构建模块、分析过程、并可通过文档说明系统中的重要细节。
UML包括:
静态模型(static model)
创建并记录一个系统的静态特征,反映一个软件系统基础、固定的框架结构,创建相关问题域主要元素的视图。
主要有用例(况)图、类图、对象图、组件图、部署图等。
动态模型(dynamic model)
用以展示系统的行为。主要有时序图(顺序图)、协作图、状态图、活动图等。
UML 的类图和顺序图和大多数人讨论软件设计的时候随手画的草图很接近。但是标准化这样的草图付出的资源并不值得。建模很重要,图也很重要。但是,用来交流的图应该是:1. 不能完全脱离文字;2. 在附带少量文字的基础上做到简单易懂。UML 定义标准图形还是太重了。
早期,UML的各种图被用于RUP的各个阶段。而RUP因为太重被业界抛弃,被敏捷过程所代替了。敏捷过程中的很多阶段不强调文档,注重的是代码、沟通、快速迭代;所以图也不那么正规,以草图居多。
UP(Unified Process,统一过程)是一种通用过程框架,可以广泛用于各种软件系统,包括不同应用领域、组织架构的系统,也不分系统的性能水平、项目规模。UP基于构件,使用UML建模。所谓的统一,是因为UP:
RUP(Rational Unified Process)是原Rational公司(已被IBM收购)开发和维护的过程产品。它完全兼容UP,但比UP更加完整、详细。或者说,Rational让UP落地。请注意,Rational公司也是UML的创造者。基本上,可以将UP与RUP等同。
在创建敏捷宣言时,有不少“轻量级”开发流程;此后出现了其他此类方法。它们现在统称为“敏捷”方法。
常用的敏捷开发方法有:
还有许多其他的敏捷方法在使用。这包括scrumban、crystal、BDD、TDD、FDD等混合方法,以及各家公司开发的许多内部定制。
软件分析到设计的过程,由粗到细,最终落实到我们接触到的 UML 知识上。从需求提出到编码实现,这中间有两个关键问题:一是界定目标,即是定义清楚要做什么的问题,相当于是我们做事的方向、目标;二是具体如何做的问题,即通过怎样具体的方案支撑需求目标实现。因此,我们需要一种方法能够帮助我们界定目标和表示具体方案,而且是大家互认的一种通用的方法。
通过用例图可以帮我们界定目标,用例中有三个关键要素:用户、场景和目标。比如交易下单是一个用例,它的用户是买家,场景包含下单成功和下单失败两个场景,用例的目标是买家可以购买心仪的商品。当用例目标确定了,相当于界定了目标,知道需求要做什么,这个过程要反复和业务方确认好,至到最终大家对目标的理解是一致的,方向对了,具体怎么做就好办了。
具体怎么做用顺序图表示,画顺序图需要注意的一点是顶层的对象层次要一致,不能有的对象表示具体的实体对象,有的表示系统对象,即对象的层级是一致的,要么大家都是系统,比如导购系统调用交易系统,交易系统调用支付系统,要么大家都是对象,比如商品、订单等。通过时序图可以看到一个完整功能的执行步骤,它就包含具体执行的细节,如正常流程、异常流程。
其实在上面有一个问题,在画顺序图时要确定好对象,那么这个对象是怎么来的呢?它是由类图分析出来的,它里面有三个关键的对象:一个是边界对象,这个比较好理解,比如UI界面就是边界对象;另一个是控制对象,即是控制业务流程的对象,如下单服务就可以看作是控制对象;实体对象即是问题空间中的业务对象,比如订单。画类图是有规则的,一般是边界对象调用控制对象,控制对象产生实体对象,比如用户下单界面是边界对象,下单服务是控制对象,订单就是实体对象。
面向对象是基于现实事物做的抽象映射,重要的不是要面向对象具体技术的使用上,而是分析问题的思维上,这是最难的,它最大的好处是问题空间到解空间是一一直接映射的,它意味着我们在讨论方案的时候,完全可以映射到问题空间,如果是间接映射,也就意味着设计的方案后面会面临重新设计的可能性,因为它是基于场景或功能做出的归纳设计,而且是表层的设计。真正掌握了面向对象分析和设计的方法,也体会到其中的益处,对理解业务、方案设计、编码开发都有好处。
一些传统的银行和汽车公司,不允许开发人员在没有上级的情况下做出任何架构决策,需要几个上级架构师的签字和监督。这是一个较慢的过程,架构师可能会被许多请求淹没。因此,这些架构师创建了更正式的文档,希望使用更多常见文献描述的工具,使系统更加清晰。这些公司通常希望为开发人员进行优化,使其更多地作为可交换的资源,允许他们在短时间内重新分配人员从事不同的项目。
这些公司如何完成任务呢?
大多数团队和项目(无论大小)都采用类似的设计和实现方法:
为什么这些方法与文献中通常提到的不同? 实际上,这些方法在原则上与大多数指南并没有什么不同。几乎所有指南都建议从业务问题开始,概述解决方案和权衡:这也是通常所做的。不做的是使用许多书籍所倡导的许多更复杂的工具。而使用最直接的工具尽可能简单地记录设计:如wps等。
设计系统的目标应该简单。 系统越简单,理解起来越简单,发现问题就越简单,实现起来就越简单。描述的语言越清晰,设计就越容易理解。避免使用团队中每个成员都无法理解的行话:让经验最少的人能够清楚地理解事物。
干净的设计类似于干净的代码:它易于阅读和理解。编写干净代码有很多好方法。但是,很少会听到有人建议从设计模式应用于编码开始。干净的代码从单一责任、清晰的命名和易于理解的约定开始。这些原则同样适用于清晰的架构。
那么架构模式的作用是什么呢? 它们与编码设计模式的作用相似。可以提供如何改进代码或体系结构的想法。对于编码模式,当看到一个单例模式时,就会注意到,当看到一个充当外观的类时,通常不会深入挖掘,而只执行调用。还没有想到“这需要一个抽象的工厂模式”等待。事实上,会花费很多时间来理解这些模式的作用,并且在使用大量依赖注入之后,这种模式实际上非常普遍和有用。花费时间学习设计模式对成为一名更好的程序员的影响远远小于从其他工程师那里得到的代码的反馈。
同样,了解常见的架构模式是一件好事:它有助于缩短讨论,大家理解的方式与您相同。但架构模式不是目标,它们不会取代更简单的系统设计。在设计系统时,可能会发现自己不小心应用了一个众所周知的模式:这是一件好事。稍后,可以更轻松地参考您的方法。但是最后要做的事是采用一种或多种架构模式,将其用作锤子,寻找钉子来使用。
架构模式诞生于工程师观察在某些情况下如何做出类似的设计选择,并且这些设计选择的实现方式相似。然后,这些选择被命名,写下来,并被广泛探讨。架构模式是解决方案解决后出现的工具,希望使其他人更轻松。作为一名工程师,其目标应该是解决方案并通过它们来学习,而不是选择一个闪亮的架构模式,希望其能够解决你的问题。
很多人询问在架构和设计系统方面的技巧。一些有经验的人会建议阅读架构模式和软件架构书籍。这是一方面,这里还有一些建议。
通过白板与队友讨论设计。 画出你正在做什么以及你为什么要这样事。确保他们理解,并征求他们的反馈。
将设计写在一个简单的文档中,并与团队分享,寻求反馈。 无论你正在做的事情多么简单或复杂,这可能是一个较小的重构或一个大型项目。以对你有意义的方式和其他人可以理解的方式去做——例如,以允许评论的格式(如 Office365 或其他格式)与您的团队共享。请人们添加其想法和提出问题。
设计两种不同的方式,并对比两种设计。 当大多数人设计架构时,会采用一种方法:脑海中突然出现的一种方法。然而,架构不是非黑即白的。想出第二种设计也可以工作。对比两者,解释为什么一个比另一个更好。简要列出第二种设计作为考虑的替代方案,讨论为什么反对它。
明确说明所做的权衡, 为什么要进行权衡,以及对哪些方面进行了优化。必须考虑明确存在的约束。
查看他人的设计。做得更好。 假设有一种文化,大家通过白板和会话或文档共享其设计,那么从这些评论中可以获得更多好处。在审查过程中,大多数人只是试图接受事物,成为单向观察者。相反,对不清楚的部分提出澄清问题。询问他们考虑过的其他替代方案。询问他们采取了哪些权衡以及有哪些限制。扮演魔鬼的代言人,并提出另一个可能更简单的选择——即使它不是一个更好的选择——询问他们对你的建议的看法。即使你没有像展示的人那样考虑设计,你仍然可以增加很多经验并了解更多信息。
最好的软件设计是简单易懂的。在你开始一个新项目时,不要想“我将如何构建这个系统,我应该使用什么经过实战测试的模式,我应该用什么正式方法记录它?”,而是想“我怎样才能想出最简单的设计,以一种任何人都容易理解的方式?”。
软件架构最佳实践、企业架构模式和描述系统的形式化方式都是有用的工具,有一天可能会派上用场。但是在设计系统时,从简单开始,并尽可能保持简单。尽量避免更复杂的架构和正式工具固有的复杂性。
面向对象分析与设计的底层逻辑
Software Architecture is Overrated, Clear and Simple Design is Underrated