UML和模式应用

时间:2022-10-28 16:36:56

引言

Applying UML and Patterns,以一个商店POS系统NextGen和一个掷骰子游戏Monopoly为例,围绕OOA/D的基本原则GRASP,以迭代作为基本方法、以UML为表达工具,配以GoF的基本模式,系统地展示了一个较为完整的OOA/D过程。相较原书第1版,此番重读该书第3版,吾仍深为所动,遂将其精华采撷如下,并适当加以注解,权作温故而知新所用。

摘录

P4

在OO开发中至关重要的能力,是熟练地为软件对象分配职责。

P5

面向对象分析(OOA),强调的是在问题领域内发现和描述对象(或概念)。

面向对象设计(OOD),强调的是定义软件对象以及它们如何协作以实现需求。

P8

应用UML的三种方式:草图、蓝图和编程语言。

应用UML的三种透视图:概念透视图、规格说明(软件)透视图和实现(软件)透视图。

我更偏向于使用UML用作草图,再利用工具将代码逆向呈现为UML图,敏捷建模同样强调使用UML作为草图的方式。至于透视图,只是粒度和层次不同而已。

P15

迭代开发(Iterative development),每次迭代都有相对独立的需求分析、设计、实现和测试活动,产生经过测试、集成并可执行的局部系统。既没有匆忙地编码,也没有进行长期的设计以试图在编程前完成所有的设计细节。

迭代和进化式开发,抱以接受变更和改写的态度,并以此为真正的本质驱动力。

在最终确定所有的需求或经过深思熟虑而定义完整的设计之前,快速地实施一小步的方式可以得到快速反馈。

最好及早解决和验证具有风险的、关键的设计决策。

小步骤、快速反馈和调整,是迭代开发的主要思想。

每次迭代,意味着系统的一个局部得以实现,并以此种增量方式不断发展完善。无需一次考量所有的设计细节或方案。

P16

不要让瀑布思维侵蚀迭代或UP项目。别贪大求全

P18

借由用例列表和特性描述系统需求,从中选择10%进行深入分析后使之成为一次迭代的需求。然后围绕迭代目标进行建模和设计工作,完成编码与测试后,提交工作成果,以建立迭代的基线。如此往复,逐步推进整个项目的建设进度。

P22

建模的真正行为能够并且是应该能够对理解问题或解决方案空间提供更好的方式。

使用UML建模的目的,不是为编程者递交详细的UML图,而是为良好的设计快速探索可选的方案和途径。

可以将简单的设计问题推迟到编程阶段,尽可能使用最简单的工具。

UML的细节是否精准并不重要,关键是建模者能够凭此相互沟通和理解。

敏捷建模的思想,在于化整为零、简单明了、易于沟通。

P29

要避免的误区:

  • 在开始设计或实现之前试图定义大多数需求。
  • 在编程之前花费较多精力进行UML建模,并认为UML图必须精准到可以直接映射代码。
  • 认为初始阶段=需求阶段,细化阶段=设计阶段,构造阶段=实现阶段,将瀑布模型叠加到UP之上。
  • 认为细化的目的是完整仔细地定义模型。
  • 坚信迭代周期应该尽可能长一点。
  • 认为UP就意味着大量的活动以及文档,是要遵循大量步骤和繁琐过程的。
  • 试图制定一个完整详尽的计划,试图预测所有的可能。

P37

初始阶段作为UP的第一个阶段,不需要完成所有需求或者建立可靠计划。

对是否存在过于简化的风险,其理念是,就新系统的总体目标和可行性而言,只进行足以形成合理判断的调查,并能够确定是否值得继续深入研究即可。

初始阶段只需确定项目是否值得认真研究,而不是立即进行深入研究以确定某种设想。

初始阶段:预见项目的范围、设想和业务案例,确定项目设想是否值得继续认真研究。

初始阶段,是解决项目的可行性与必要性,可应景项目背景,解决项目有否必要、有否收益等问题,而不是预期何时、多少等问题。

P39

判断是否已深陷认识误区:

  • 认为初始阶段会持续较长时间。
  • 在初始阶段定义大部分的需求。
  • 期望初始阶段的预算和计划是可靠的。
  • 定义架构(本应放在细化阶段,以迭代方式来定义架构)。
  • 认为正确的顺序应该是:定义需求->设计架构->实现。
  • 没有业务案例或设想制品。
  • 详细编写所有用例。
  • 没有编写任何用例。

P40

需求,就是系统必须提供的能力,和必须遵从的条件。

需要分析最大的挑战,是寻找、沟通、记录哪些才是真正需要的,并能与客户、开发团队达成共识。

P42

按FURPS+对功能分类:

  • 功能性Feature:特性、功能、安全性
  • 可用性Usability:UI因素、帮助、文档
  • 可靠性Reliability:故障频率、可恢复性、可预测性
  • 性能Performance:响应时间、吞吐量、准确性、有效性、资源利用率
  • 可支持性Supportability:适应性、可维护性、国际化、可配置性
  • +:实现(资源限制、语言、工具、硬件),接口(外部系统接口),操作(操作的系统管理),产品包装,产品授权方式

P43

关键的业务制品:

  • 用例模型:系统典型场景的描述
  • 补充性规格说明:用于非功能性需求的描述
  • 词汇表:术语、概念、参数,有效性规则、值域、阈值等
  • 设想:项目的主要思想,可以借由用例模型和规格说明进行细化
  • 业务规则:更持久、应用更广泛,并限于一个项目的规则

P45

早期把精力集中于系统要实现的意图、能提供的反馈、能实施的检测等主动行为,回答"做什么",而不是"怎么做"的问题。

P47

用例,就是一组相关的成功与失败的场景集合,用来描述参与者如何使用系统,以实现其目标。

撰写100%用例摘要,撷取其中的10%作为关键用例进行详述。

P50

详述风格的用例:

  • 涉众及其各自关注点:非常重要的部分,因为它回答了用例必须包含的所有事物,以满足不同涉及者关注的内容,从而演变出系统的职责。
  • 尽可能使用系统或者参与者能检测到的事物,作为用例当中扩展分支(替代流程)的转入条件。比如"系统检测到……"
  • 对于如何实现技术的具体细节,比如使用何种具体设备等,可暂时记录在用例中,稍后可归入补充性规格说明等制品当中。
  • 以无UI约束的本质风格来编写用例。不是"用户输入账号及密码,系统显示……",而是"用户标识个人身份,系统进行认证"。
  • 编写黑盒用例,说明系统必须"做什么",而不是回答"怎么做"。从而通过职责描述系统,而不用深入系统内部了解具体机制。
  • 采用参与者以及参与者目标的视角,以此产生对特定参与者具有价值的可观察结果。

P62

如何发现用例:

  • 选择系统边界:可能会有外部支付等系统并不在系统边界之类,可作为外部参与者。
  • 确定主要参与者:什么样的问题有助于寻找参与者和目标?有助于寻找参与者、目标和用例的另一个方法是确定外部事件。老板测试(你这一整天都干了些什么?)、EBP测试(特定场景、业务事件、业务价值、以持久状态留下数据)、规模测试(用例由一系列的步骤组成,单独的活动或步骤无法定义为用例)。
  • 确定每个主要参与者的目标
  • 定义满足用户目标的用命,并根据其目标对用例命名

P77

  • 补充性规格说明:所有未在用例中描述的需求。
  • 词汇表:要尽早编制,包括概念,定义等,可充当数据字典的角色。(用于在统一建模语言中消除概念语义的二义性。)
  • 业务规则:规则描述,规则可能的变化性,规则来源(制度、政策、法律等)
  • 设想:高阶目标,优先级(高->低),可能涉及的问题,目前选择的解决方案
  • 特性:系统实现<某某特性>

P94

在初始阶段的可能活动与制品:

  • 简短的需求讨论会。
  • 大多数以摘要方式编写的用例,包括参与者、目标和用例名称。
  • 确定大多数具有影响和风险的质量需求。
  • 编写设想和补充性规格说明。
  • 风险列表
  • 架构、技术构件、候选工具、系统原型等。
  • 迭代计划。

P95

细化阶段的关键思想和最佳实践:

  • 实现短时间定量、风险驱动的迭代。
  • 及早开始编程。
  • 对架构的核心和风险部分进行适应性的设计、实现和测试。
  • 及早、频繁、实际地测试。
  • 基于来自测试、用户、开发者的反馈进行调整。
  • 通过一系列讨论会,详细地编写大部分用例和其他需求,每个细化迭代至少举行一次。

P96

通过风险、覆盖范围、关键程度,来组织需求和迭代:

  • 风险:包括技术复杂性,也包括工作量或可用性等其他一些不可确定性因素。
  • 覆盖范围:在早期迭代中至少要涉及系统的所有主要部分,可以是对大量构件的一种广泛而肤浅的实现。(比如仅有接口定义和简单实现)
  • 关键程度:对客户而言具有高业务价值的功能。

P100

领域模型,指的是对现实世界概念类的表示,而不是软件对象的表示,是对领域内的概念类或现实世界中的对象的可视化表示。

领域模型是可视化字典,表示领域的重要抽象、领域词汇和领域的内容信息。

创建领域模型,为了使我能理解领域内的关键概念和词汇。

为了缩小我们的思维与软件模型之间的表示差异,所以可以将领域层软件类的名称源于领域模型中的名称,以使软件对象具有源于领域的信息和职责。

领域模型和语言层次的类模型等软件业务对象图并不能决然地划上等号,但我更愿意以DDD的方式去理解和对待领域模型,而不是如此累赘和饶舌。

P104

如何创建领域模型:

  • 寻找概念类。
  • 将其绘制为UML类图中的类。
  • 添加关联和属性。

如何找到概念类:

  • 重用和修改现有模型。这是首要、最佳、最简单的方法。
  • 使用分类列表。根据概念类所属类别,比如业务交易、交易项目、记录载体、用例参与者、协作的外部系统等,列出可能的概念。
  • 确定名词短语。利用语言分析器,找出文本性描述中出现的名词和名词短语,将其作为候选的概念或属性。但由于自然语言的二义性,可能会出现重复、歧义等,因此不能直接建立名词到类的直接映射。通常将这种方法与分类列表结合使用。

P108

报表、票据——模型里是否有必要实现?

  • 通常情况下,在领域模型中显示其他信息的报表并没有意义,因为其所有信息都是源于或复制于其他信息源的。
  • 特殊情况下,当有业务规则要求,报表或者票据将直接影响参与者的权益,或者影响业务价值时,需要在模型中表示。

P109

在创建领域模型时最常见的错误,把应该是概念类的事物表示为属性。

如果我们认为某个概念类不是现实世界中的数字或者文本,那么它就可能是概念类,而不是属性。此时,这种概念总会占据一定的空间、有可能的实体或者组织,成为一种大规模的事物,比如Store、Airport。

P109

描述类包含描述其他事物的信息。(类似"历史旧照",具有当时性、历史意义的信息,比如订单中的产品价格仅限于下单当时。P377将阐述"时间间隔"的问题,正应此题。)

何时使用"描述"类建模?

  • 需要将对类的描述,独立于任何该类的现有实例。
  • 删除其所描述的事物的实例后,将导致信息丢失,而这些信息是需要被慎重对待的。
  • 减少冗余或重复信息,将信息集中在描述类中表示。

P111

关联是类之间的关系,表示有意义和值得关注的连接,需要被记住的信息。是否需要记录关联,应该基于现实世界的需要,而不是基于软件的需要。

以"类名-动词短语-类名"的格式为关联命名,但Has/Uses这样拙劣的动词可能无法增强对领域模型的理解,所以尽量少用或不用。

P114

如何在常见关联列表中找到关联:

  • A是与B相关的交易
  • A是B中的一个项目
  • A是B的产品或服务
  • A是与B相关的角色
  • A是B的物理或者逻辑上的组成
  • A是B的描述
  • A在B中被感知、记录、捕获等
  • A是B的成员
  • A是B的组织树中的子单元
  • A使用、管理、拥有B
  • A与B相邻

P119

什么样的属性类型是恰当的?属性的类型应该是原生类型,比如bool、char、string,还有更复杂一点的Address、Color、ZIP等。

把复杂的领域概念建模为属性是最常见的错误,并且应该使用关联而不是导航属性来表示概念类之间的关系。

(这一节的描述,将涉及属性、关联、数据类型、值对象等。应该如此理解:在模型层面上,应该将概念类之间的关系用关联进行表达,而不是在概念类置入一个与之关联的另一概念类对应的属性,而数据类型是用来定义属性的。在数据类型和值对象的问题上,由于二者极其近似,因此可以将数据类型理解为C#里的值类型等具体概念,数据类型并不直接参与建模,而将值对象理解为DDD领域内的一种模型概念,成为模型的构成元素之一。在编程实现上,实体的等价用==运算符表示,而值对象的等价用Equals方法表示。)

P120

对数据类型建模的准则:

  • 由不同的小节组成。比如人名、区号-电话号码
  • 具有与之相关的解析或校验操作等。比如社会保障号
  • 具有其他属性。比如促销价格有生效期间
  • 单位的数据。比如货币、带包装规格的事物。

P122

对数量和单位建模:在对价格、重量等用数量表示的概念时,除了数字本身,还应考虑其计量单位。

可以考虑用Amount[Quantity quantity]->Quantity[int number]->Unit[string name]这样的关联进行表达。

P126

系统顺序图(SSD)是为阐述与系统相关的输入和输出事件而快速/简单地创建的制品。它关注的是事件,是操作契约和对象设计的输入。

P129

系统通常要对三种事件进行响应:1)来自于参与者的外部事件(系统事件);2)时间事件;3)错误或者异常(通常也源自外部)。

系统事件,指外部输入的事件,是系统行为分析的重要部分,应当以意图的抽象级别,而不是物理输入级别来确定系统事件和操作的名称。

系统行为,是指描述系统做什么,而无需解释如何做的"黑盒"动作。(把系统当作一个黑盒,勾勒出系统事件与系统行为后,当细化阶段进入设计时才进入白盒。)

系统操作,则是响应系统事件的系统行为。

不应该花费太大时间来绘制SSD,只需为下次迭代所用的场景绘制SSD,而不是所有场景。

SSD是用例模型的一部分,将用例场景的文本性表述及其隐含的交互,采用可视化的方式表示出来。

大部分的SSD在细化阶段创建,这有利于识别系统事件、明确系统操作、利用编写系统操作契约。

P133

操作契约,使用前置和后置条件作为表达形式,描述领域模型里的对象在系统操作前后发生的详细变化,反映该操作的结果。

(系统事件和系统操作,可以分别与Event Sourcing里的事件和事件处理器进行类比。)

后置条件可以分为三类:1)创建或者删除实例;2)属性值发生变化;3)形成或消除关联。

(删除实例的后置条件非常罕见,人们通常不会关心是否要明确销毁现实世界中的事物。这也回答了DDD中删除实体的操作有否必要这样类似的问题。)

后置条件,通常用说明性的、被动式的过去时态表达。(Event Sourcing里,对事件Event也采用类似的命名方法。)

后置条件的本质,就是舞台和幕布(两个快照的对比):1)操作前,对舞台拍照;2)落下幕布,应用系统操作;3)打开幕布,拍摄第二张照片;4)比较前后两张照片,舞台上发生的变化即为后置条件。

编写后置条件时,最易犯的错误是遗漏了关联的形成与消除。

P140

对领域模型对象状态的改变,在设计时有两种方案:一是使用状态模式(State),另一种是使用会话对象(Session)。

P143

OOA的重点是做正确的事,而OOD则强调正确地做事。

要尽早引发变更,因为越早发现变化就能越早改变设计,避免设计蠕变。

P146

逻辑架构,是软件中类的宏观组织结构,它将软件组织为包(命名空间)、子系统和层等。

部署架构,则是逻辑架构的不同组成部分和元素,在物理计算机系统上的具体部署。

层是对类、包或子系统不同粒度的分组,形成负有内聚职责的系统组成部分。层是对系统的垂直划分,分区则是水平方向的划分。

具体的物理部署部件不能混入逻辑视图。

模型-视图分离原则:不要将UI对象方法中加入应用逻辑,不要将非UI对象直接与UI对象连接或耦合。

从UI层发送到领域层的消息,将是SSD中所描述的消息。

(分层是经验的结晶,核心是各种"分离"。同一层内部强内聚,不同层之间保持低耦合。)

P157

在选择UML CASE工具时,能与常用的IDE集成并提供逆向生成UML图功能的,应当是优先考虑的。

对象模型有两种类型:动态模型有助于设计逻辑、代码行为或方法体,静态模型有助于设计包、类名、属性和方法特征的定义。

花费较短时间创建交互图(动态),然后转到对应的类图(静态),之后交替进行。

UML初学者一般会认为静态视图的类图是重要的图形,然而事实上,大部分具有挑战性、有益和有效的设计工作,都会在绘制UML动态视图的交互图时发生。需要哪些对象、它们如何通过消息和方法进行协作,通过动态对象建模(比如绘制SSD),才能真正落实这些准确和详细的结论。

应该把时间花费在交互图上,而不仅仅是类图上。要重视动态图,比如发哪些消息、消息发给谁、以何种顺序发。

在绘制UML对象图时,要回答:1)对象的职责是什么?2)对象在与谁协作?3)应该使用什么设计模式?

P162

UML规范更多以顺序图为核心,然而通信图更方便手绘。

  • P166 顺序图的基本表示法
  • P175 通信图的基本表示法

P180

UML类图(设计类图DCD),表示类、接口及其关联,用于静态对象建模。

P184

对数据类型对象使用属性文本表示法,对其他对象使用关联线。

P188

泛化与编程语言中的继承的意义是否相同?这要视条件而定。从领域模型概念视图的角度看,答案为否。而从DCD的软件对象视图的角度看,答案为是。

P189

依赖,是从客户到提供者的联系。依赖可以视为另一种版本的耦合。

在DCD中,使用依赖描述对象之间的全局变量、参数变量、局部变量和对静态方法调用的依赖。

P191

聚合Aggregation,是UML中一种模糊的关联,其并不精确,但暗示了整体和部分的关系。不要在UML中费心地去使用聚合,相反,在适当的时候,要使用组合。

组合Composition,也称为组合聚合(Composite aggregation),是一种很强的整体和部分的关系。

组合有以下几层含义:1)在某一时刻,部分的实例只属于一个组成实例;2)部分必须总是属于组成;3)组成要负责创建和删除其部分,既可以是由组成自己来完成创建或删除的部分,也可以是和其他对象协作来完成。

组成聚合的概念,与DDD中的聚合有高度的一致性。

P198

思考软件对象设计的流行方式是,考虑其职责、角色和协作,这是被称为职责驱动设计的大型方法的一部分。

RDD中将职责分为两种类型:

  • 行为:1)自身执行一些行为,比如创建或者计算;2)初始化其他对象的动作;3)控制和协调其他对象中的活动。
  • 认识:1)对私有封装数据的认知;2)对相关对象的认知;3)对其能够导出或计算的事物的认知。

由于领域模型描述了领域对象的属性和关联,因此其通常产生了与"认知"相关的职责。

职责与方法并非同一事物,职责是一种抽象,而方法实现了职责。

RDD是一种隐喻,把软件对象想象成具有某种职责的人,他要与其他人协作以完成工作。RDD使我们把OOD看作是一些有职责的对象进行协作的共同体。

在UML之中,绘制交互图是考虑软件对象职责(实现为方法)的时机,绘图时就是在决定职责的分配。

P203

创建者:B在何种条件下负责创建A?谁应该负责创建某类的新实例?

  • B包含或组成聚合了A
  • B记录A
  • B直接使用A
  • B具有A的初始化数据

创建者模式的基本意图,是寻找在任何情况下,都与被创建者具有连接的创建者。比如组合聚集部分,容器容纳内容,记录者进行记录,都是常见的创建模式应用范围。

当对象的创建具有相当的复杂性,或基于某些外部条件,需要创建一个或者一族的对象时,最好是将创建者的职责交托给工厂模式。

P205

信息专家(Information Expert):B在何种条件下应该负责为其他对象提供A的相关信息?给对象分配职责的基本原则是什么? (把职责与数据放在一处,"知其责、行其事")

首先从清晰地描述职责开始,然后寻找职责需要或包含哪些信息,进而查找这些信息分布在模型的哪些对象之中,最后综合耦合、内聚等因素确定由这些对象中的哪一个来充当信息专家。

由于完成职责往往需要分布在不同对象中的信息,这意味着许多"局部的"信息专家需要通过协作来完成任务,因此消息交互不可避免。

信息专家是对真实世界的模拟,我们一般基于"Do It Myself"策略,由对象拟人化地完成它们所知信息有关的任务。

某些情况下,尽管符合信息专家模式应用场景,但会导致负面的耦合与内聚问题时,应该放弃这一模式。比如数据库的持久化逻辑,应该与对象本身剥离。

P206

低耦合(Low Coupling):如何合理分配对象的职责,保证因变化产生的影响最小?如何降低类之间的依赖性,减少变化带来的影响,提高重用性?

在实践中,耦合程度不能脱离专家、高内聚等其他原则孤立地考虑,但它的确是改进设计所要考虑的因素之一。

低耦合是制定设计决策期间必须牢记的原则,它是重要的评估原则。

在OOP中,类型X与类型Y之间耦合的常见形式包括:

  • X具有引用Y的实例或者Y自身的属性
  • X调用Y的服务
  • X具有任何形式引用Y的实例或Y自身的方法。比如包括Y的参数或局部变量,或由消息返回的对象是Y的实例。
  • X是Y的直接或间接子类
  • Y是接口,而X是Y的实现

低耦合的极端例子是没有耦合,但这违反了对象技术的核心隐喻:系统由相互连接的对象构成,对象之间通过消息通信。耦合度过低会产生不良设计,其中会使用一些缺乏内聚性、膨胀、复杂的主动对象来完成所有的工作,并且存在大量被动、零耦合的对象来充当简单的数据知识库。对象之间的适度耦合,对于创建面向对象的系统来说是正常和必要的,其中的任务是通过被连接的对象之间的协作来完成的。

必须在降低耦合和封装事物之间进行权衡,把关注的精力放在极不稳定或需要进化的地方。

P207

控制器(Controller):如何将UI层连接到应用逻辑层?谁负责从UI层接收消息?

通常的选择包括:

  • 外观控制器:代表全部"系统"、"根对象"、运行软件的主设备、主要的子系统。它们都属于Façade的各种变体。
  • 用例控制器:代表发生系统操作的某个特定场景,比如会话、用例等。对于同一用例场景的所有系统事件使用相同的控制器。而会话则是与参与者进行交谈的实例。会话可以具有任意长度,但通过按照用例来组织(用例会话)。

控制器设计中最常见的错误,是分配的职责过多。正常情况下,控制器应当把需要完成的工作委派给其他对象。控制器只是协调或控制这些活动,本身并不完成大量工作。臃肿的控制器设计有以下迹象:

  • 只有一个控制器来接收系统中全部的系统事件
  • 为了处理系统事件,由控制器完成诸多必要的任务,而不是将工作委派出去
  • 控制器包含太多属性,维护着领域的重要信息,或者复制了一些可在其他地方找到的信息

使用控制器模式导致的结果,是UI对象和UI层都不应具有实现系统事件的职责,系统操作应当在应用逻辑层或领域层进行处理,而不是在UI层处理。

Web-MVC与GRASP中的控制器不同。前者是UI层的一部分,并且控制UI层的交互及页面流。而GRASP的控制器属于领域层的一部分,它负责控制或协调工作请求的处理,它本身并不知道具体使用的何种UI技术。GRASP的控制器,可以理解为DDD中的Application Service这样的构成元素。)

对服务器端系统操作的适当处理,在很大程度上受所选择的服务器技术构架的影响,同时也是不断推进的目标,但仍然能够继续应用模型-视图分离的基本原则。

Controller+Command将是不错的选择,请参考P309 间接性)

P209

内聚可以非正式地用于衡量软件元素操作在功能上的相关程度,也可用于衡量软件元素完成的工作量。

高内聚:如何使对象保持有内聚,可理解、可管理,同时具有支持低耦合的附加作用。(不良内聚和不良耦合通常是相伴相生的,不良内聚通常会导致不良耦合,反之亦然。)

内聚性低的类,通常表示大粒度的抽象,或承担了本应委托给其他对象的职责。(如果一个人承担了过多不相关的工作,特别是本应委派给其他人的工作,那么此人一定没有很高的工作效率。)

某些情况下,可以接受低内聚。一种情况是将一组职责或者代码放入一个类或组件中,以方便维护。比如本地化的字符串、SQL语句。另一种情况是具有分布式服务器对象的低内聚构件。这是为了在分布式情况下,能有一些数量较少但规模较大的低内聚服务器对象,以便为大量操作提供接口。比如用一个粗粒度的操作SetData代替三个细粒度的操作SetName/SetSalary/SetHireDate。

P231

OOD更近于科学而非艺术,尽管它存在巨大的创造性和优雅设计的空间。

P233

这一节关乎设计的诸多细节,需要仔细阅读。其中的关键工具包括:

  • 行为:梳理后置条件,确定由谁来创建或者改变对象
  • 认知:确定由谁负责获知信息
  • 存在多个候选者时如何权衡并做出选择
  • 如何实现和设计系统的初始化行为,尽管它总是最后才考虑的

基本步骤包括:

  • 确定具体问题:如何设计makePayment
  • 编写操作契约:操作的前置与后置条件如何,产生何种操作结果?
  • 应用具体模式:确定设计方案,做出设计抉择。

P235

在编码时,至少首先要编写启动初始化的程序。但在OOD建模时,则要最后才考虑启动初始化,知道哪些对象是真正需要被创建和被初始化的,然后再针对初始化进行设计,以支持其他用例场景实现的需要。(这是为最大程度地发现所有要启动和初始化的信息。)

P239

如果某对象要发送消息到另一个对象时,它必须拥有对接收消息的那个对象的可见性。(可见性,是涉及耦合与内聚的又一个重要问题。)

P245

当存在多个可选设计时,应更深入地观察可选设计所存在的内聚和耦合,以及未来可能存在的进化压力。选择具有良好的内聚、耦合和在未来出现变化时能保持稳定的设计。

P253

OOD并不是一对一地模拟现实领域的活动,尤其是关于人的行为。(拟人但不是完全按人的职责去分配。)

  • 当有多个局部信息专家有待选择时,将职责赋予具有支配作用的专家,即持有主要信息的对象,这将有助于支持低耦合。
  • 当存在多个设计有待选择时,考虑每个设计对耦合和内聚的影响,由此选择最好的方案。
  • 当基于上述准则还是无法明确地选择出适当的方案时,则要考虑这些软件对象在未来可能的演化,以及信息专家、内聚、耦合等方面的影响。

P257

CQS原则指出,任何一个方法只能是如下情况之一:

  • 执行动作的命令方法。这种方法通常具有改变对象状态的副作用,并且是没有返回值的。
  • 向调用者返回数据的查询。这种方法通常是没有副作用的,不会永久地改变对象的任何状态。

CQS被公认是计算机科学理论中具有价值的原则,而且它是个简单的模式。

CQS原则通常是要严格遵循的。如果突然采用其他方法,将会产生令人不快的意外,从而违反最小意外原则(Least Surprise)。

P261

可见性(Visibility),是对象"看到"或者引用其他对象的能力。

实现A到B的可见性通常有4种方式:

  • 属性可见性—B是A的属性。这是一种相对持久的可见性,活跃于整个A的生存期,最为常见。
  • 参数可见性—B是A的方法中的参数。这是一种相对暂时的可见性,活跃于A的方法范围内,次为常见。
  • 局部可见性—B是A的方法中的局部变量。这是一种临时的可见性,活跃于A的方法的局部范围内,更为次常见。
  • 全局可见性—B具有某种方式的全局可见性。这是一种相对持久的可见性,通常以Singleton、全局变量等方式活跃在系统中,最为鲜见。

P265

如果对象实现的是接口,那么使用接口而不是具体类来声明变量。(在逆变与协变中,建议参数、定义用基类或接口,而返回类型用派生类)

类的实现以及测试,要按照耦合度从低到高的顺序来完成。

P279

TDD与Refactoring

P292

迭代2的开始及需求

P298

何时展示子类:

  • 子类有额外的属性
  • 子类有额外的关联
  • 对子类的影响、处理、反应和操作,与超类或者其他子类存在显著的差异

将所有的超类声明为抽象类。

对所有的子类名称都附以超类的名称。

将没有任何特殊之处的子类也定义为一个独特的概念。(比如Null Object模式)

P301

多态:如何处理基于类型的选择?如何创建可插拔的软件构件?(选择消息,不要问"什么类型")

不要测试对象的类型,也不要用if或者case判断来执行基于类型的不同选择。

除非在超类需要有默认的行为,否则应该将超类中的多态方法声明为抽象方法。(之前提到的Null Object模式中,有一个独特的子类,其多态方法的实现将不做任何动作,这被称为NO-OP方法)

当你想要支持多态,但又不想约束于特定的类层次结构时,可以使用接口Interface。

P306

纯虚构(臆构):当你并不想违背高内聚、低耦合及其他一些目标,但基于专家模式又不合适时,哪些对象应该承担这一职责?

纯虚构类的设计可分为:

  • 通过表示解析而产生的选择:从真实世界映射到对象的解析。比如目录这样存在于真实领域中的概念。
  • 通过行为解析而产生的选择:为了行为的抽象,而将行为集中,并因此解析得到行为的分组,使行为本身成为对象。比如报销规则。

信息专家所支持的目标是,将职责与这些职责所需的信息结合起来赋予同一个对象,以实现对低耦合的支持。但是,如果滥用纯虚构,会导致出现大量的行为对象,其职责与执行职责所需的信息没有结合起来,这样将会对耦合产生不良的影响。

P309

间接性:为了避免两个或多个事物之间直接耦合,应该如何分配职责?将职责分配给中介对象,使其作为其他服务或构件之间的媒介,从而避免它们之间的直接耦合。

多数间接性中介,都是纯虚构。

P310

防止变异(PV):如何设计对象、子系统和系统,使其内部的变化或不稳定性,不会对其他元素产生不良影响?

PV是一个根本原则,促成了大部分编程和设计的机制和模式,用来提供灵活性和防止变化。PV是一切模式的根基与出发点:隔离变化!

"不要和陌生人说话"原则规定,只应给以下对象发送消息:

  • this
  • 方法的参数
  • this的参数
  • 作为this属性的集合中的元素
  • 在方法中创建的对象

PV替换了Liskov替换原则(LSP)、"不要和陌生人说话"和德墨忒耳定律(Law of Demeter),后者是前者的特例。

在类似Fluent API这样的调用链情况下,遍历的路径越长,其本身亦将更脆弱。

PV基本上等同于信息隐藏(由于困难和可能的变化,而对其他模块隐藏与设计相关的信息。)和开闭原则(模块应该对扩展、可适应性开发,并同时对影响客户的更改封闭,简称OCP)。

P317

适配器(Adapter):通过中介适配器对象,使构件的原有接口转换为其他接口,解决不相容的接口问题,使其更加稳定。

P319

工厂(Factory):通过纯虚构的工厂对象,来处理存在复杂创建逻辑、为改良内聚而分离创建职责情况下的对象创建职责分配问题。(书中的例子使用了纯虚构的工厂ServicesFactory来创建需要的Adapter

P321

单实例类(Singleton):对类定义一个静态的方法以返回唯一的实例,解决对象需要全局性的可见性和单点访问需要。

在Singleton的实现上,有缓式初始化的与预先初始化的。前者是在GetInstance方法中判断是否已有实例,没有时才创建;而后者是在类初始化时即创建实例,GetInstance只是简单返回已创建的实例即可。人们通常倾向于选择缓式初始化方式,这是因为:1)如果该实例暂时不需要被访问,则可节省创建工作和内存资源等;2)缓式初始化时,可以在GetInstance方法里加入复杂和有条件的创建逻辑,特别是当它对其他类有依赖时。

Singleton使用类的实例而不是静态类本身,是因为:1)实例方法则允许定义子类,以进一步进行精化,静态方法不允许多态覆写。2)多数面向对象的远程通信机制,只允许实例方法的远程调用,而不支持静态方法。3)类并非在所有应用场景中都需要是Singleton,使用实例将保留这种灵活性。

P324

策略(Strategy):在单独的类中分别定义每种算法、政策或策略,并使其具有共同的接口,以应对变化但又彼此相关的算法或政策。

被策略所应用或评价的对象,被称为语境对象(Context Object)。在可见性上,策略对象的属性能被语境对象"看见"。而策略本身需要的外部参数,则可以通过构造参数等传递。

书中的例子,使用工厂创建策略,并且是一个工厂负责一族策略对象的创建。

P328

组合(Composite):定义组合及原子对象的类,使它们具有共同的接口,以能够象处理非组合的原子对象一样,多态地处理一组对象或者具有组合结构的对象。

密码安全策略、报销制度的规则,都是组合模式的用武之地。书中的例子,将工厂、策略和组合模式结合使用,实现了商品打折的业务需求。

P334

将聚合对象作为参数传递:避免将子对象从父对象或聚合对象中提取出来,并且传递这些子对象。相反地,应该将包括该子对象的聚合对象整个地传递给调用者。

(这与《Implementing Domain-Driven Design》一书的观点截然相反。在IDDD一书中文版P203中,要求只传递有关聚合最小程度的信息,而不是整个聚合。 这样既可减少依赖性,还能避免一不小心对聚合的修改。)

P335

外观(Façade):给子系统定义一个唯一的接触点,外部系统通过这个唯一的接口与子系统交互,从而避免子系统内部的变化影响整个系统。比如规则引擎、工作流的设计。

接触点的设计将成为Façade暴露的对外接口,需要慎重!

P337

观察者-发布者:定义订阅者或监听者接口,由订阅者实现该接口并向发布者注册自己。由发布者维护订阅者的列表,在发布者变化时触发事件,再逐一将事件通知给订阅者,以解决不同类型的订阅者对发布者状态变化的关注。

将事件本身再作一次封装,把事件相关的信息移入事件对象,从而解决事件委托的泛型定义困难,避免引入复杂的参数列表。

P344

迭代3的开始和需求,更加底层、游戏规则更加完整。

P346

活动图:表示一个过程中多个顺序活动与并行活动。

  • 活动图对参与者众多、业务过程复杂的过程具有价值,简单的业务过程只用文本即可。
  • 在业务建模过程中,可以保持较高的抽象水平,从而保持图形清晰、简洁的品质。
  • 要尽量保持同一张图中所有动作节点的抽象水平一致。

P352

状态机图:描述某个对象的状态,感兴趣的事件,以及对象响应事件时的行为。

书中的例子,用状态机为UI导航建模,并说明了状态机图的一些主要应用领域,比如通信协议、单个UI窗口的事件处理、会话流程等等。

P357

用例关联:如何组织用例,理清不同用例之间的关系。(然并卵,只是改善对用例的理解、减少重复。)OCT(31)=DEC(25)

P365

泛化:在多个概念中识别共性,从而定义超类(普通概念)与子类(具体概念)的关系。

正确的子类(同时具备):

  • 超类的属性、关系,必然100%地适用于子类
  • 总可以用"is a kind of "的自然语言,来表述子类与超类之间的关系。

将概念类划分为子类的动机(其中之一):

  • 子类有额外的、有意义的属性或关联
  • 子类的操作、处理、反应或者使用的方式,不同于超类或其他子类,而这些方式是我们需要区别对待并关注的。
  • 子类表示了一个活动体(拟人的自治体),其行为与超类或其他子类不同,而这种不同点也正是我们关注的。

不要将X的状态,建模为X的子类,有两个办法:

  • 将状态单独封装为类,与X建立关联。
  • 将状态作为X的属性,由状态图进行表达。

P373

泛化与继承的区别:继承是语言的,泛化是模型的;泛化可以用继承表达,但也可以用其他语言特性实现,并不一定是继承。

P374

关联类:如果C可能同时有多个相同的属性A,则不要将A置于C之中,而应该将A另放在一个类中,并使之与C关联。

在模型中增加关联类的可能线索:

  • 有某个属性与关联相关
  • 关联类的实例具有依赖于关联的生命期
  • 两个概念之间有多对多的关联,并且存在与关联自身相关的信息。

P375

在下列情形下,可考虑组合关系:

  • 部分的生命期在组成的生命期界限之内,部分的创建和删除依赖于整体
  • 在物理或者逻辑的组装上,整体-部分的关系很明确
  • 组成的某些属性(比如位置),会传递给部分
  • 对组成的操作(比如销毁、移动和记录等),可能传递给部分

P377

时间间隔:有时效性的信息,需要考虑时间间隔的问题。比如商品价格。

具体有两种实现方法:

  • 在持有信息的概念类中始终保存当前值,而在关联类中写入具时效性的值。比如Product保存当前价格,在OrderLine中保存下单时价格。
  • 增加一个带时间间隔的类,然后将一组这种带时间间隔的类与概念类关联。比如新建一个持有价格及其生效期间的Price类,用一个Price的列表保存Product的历史价格。(这种方法更可取、更稳健)

P379

受限关联可以减少关联的多重性,但在领域模型中不要使用这样限定查找关键字的决策。因为限定词并没有增加更加有用的信息,反而会使我们落入"设计思维"的陷阱。

P381

在将领域模型划分为包时,满足以下条件的元素可以放在一起:

  • 在同一个主题领域、概念或者目标密切相关的元素
  • 在同一个类层次结构中的关系
  • 参与同一个用例的元素
  • 有很强关联性的元素

使用包来组织领域模型,这和BC的划分有些类似,可作参考。

P390

优秀的架构师,其价值在于他们具有知道问什么问题的经验,并且能够熟练地选择各种方法来解决这些问题。

在UP中,基于早于第一次开发迭代时,就应该开始架构分析,因为架构分析的失败将会导致高风险,因此分析和早期的开发活动是齐头并进的。

架构分析(Architecture Analysis),是在功能需求的语境中,识别和处理系统的非功能性需求。比如可靠性、容错性、组件许可费用、可适应性、可配置性、产品名称等等。

架构分析的常用步骤:1)识别和分析对架构有影响的非功能性需求作为架构因素,比如对初始阶段的补充性规格说明进行更仔细的调查和完善。2)对这些架构因素进行分析,找出可供选择的办法,通过架构决策确定最终的解决方案。

因素表(Factor Table):因素,度量和度量场景,可变性(当前的灵活性和未来的演化性),该因素对涉众、架构以及其他因素的影响,对于成功的优先级,困难或风险。

技术备忘录:记下架构选择、决策以及当时的动机,即为什么做出这样的选择。

关于架构分析的总结:

  • 架构关注的是非功能性需求
  • 架构分析涉及系统级别的、大尺度的、涉及面广的问题
  • 架构分析面临的诸多问题存在相互依赖性,需要做出权衡
  • 架构分析总要面临可选方案的规划和评估

UP中的架构分析:

  • 初始阶段:如果不能确定技术上是否可以满足关键性的架构需求,可以先实现一个架构概念验证原型(Architecture POC)来确定其可行性。
  • 细化阶段:采用因素表、技术备忘录和软件架构文档SAD等手段实现核心的风险架构元素,完成大部分的架构分析。
  • 移交阶段:修订SAD,确保与最终部署的系统一致。
  • 后续进化循环:在设计新版本前,重温架构因素和决策。

P403

逻辑架构的精化:如何合理分层、打包,确保层内的高内聚、层间的低耦合。

P416

包的设计:如何构造物理意义上的build包。

关系内聚(Relationship Cohesion)= 内部关系的数量/类型的数量,值越大则关系越紧密。

P422

以缓存设计与异常处理为例,介绍GoF中的更多模式:代理 Proxy,抽象工厂Abstract Factory

P446

ORM设计为例,介绍如何进行框架设计,穿插了GoF中的模板Template、状态State、虚代理Virtual Proxy等等

P484

关于迭代式开发和敏捷项目管理的进一步讨论

结语

书中大量的准则、问题、方案,无一不闪烁着思想的光芒。每次重温,都会有新的收获。GRASP与GoF的灵活运用,是本书的核心。