聊一下,面向对象软件的一般过程与方法

1,082 阅读1小时+

image

代码中,看到了那么多的类,他们是如何被创造出来的吗,合理吗?

或许只是习惯性反射式的 class 或者 struct,无class不兄弟,不编程~

发现很多新同学,不知道面向对象的一般过程与方法
发现很多老同学,以为自己已经深刻理解了面向对象,实则缺乏体系和本质认知

前面一篇文章 再聊一下,架构升级那些事 讲的是稍宏观的架构思考,本文下探到稍微观层去看类与职责。

软件是一门哲学,但也有套路与方法。下面我们就一起来探究,那些出现在我们“代码中的类是如何来的,合不合理”

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

面向对象设计OOD:Object-Oriented Design,强调的是定义软件对象(属性和方法)以及它们如何协作(交互图)以实现需求。

面向对象实现OOP:Object-oriented programming,面向对象编程,是把我们OOD的设计结果转为代码。

image

上面是面向过程的过程和相关制品介绍,下面会分布展开进行,来一场对象之路~

一、OOA:定义用例,看明白需求

很多技术同学对用例并没有特别在意,其实用例才是最值得深入研究的~
不清楚用例,是很难做出好设计,写出好代码的!

用例不是面向对象制品,而只是对情节的记录,是需求分析中一种常用的工具。

将人们如何使用应用的情节或场景,编写成用例(use case)。

用例是文本形式的情节描述,广泛应用于需求的发现和记录工作中,用以说明某参与者使用系统以实现某些目标。

我们会提出这样的问题:“谁使用系统?他们使用的典型场景是什么?他们的目的是什么?

用例是一种优秀的方法,使领域专家或需求提供者自己编写(或参与编写)用例成为可能,并使这项工作难度降低。

情节化文本形式描述,这样使得用例定义和评审更为简单,对于客户参与更有帮助,这样也降低了失败的风险。

1. 分清类型,看功能需求

定义新产品、服务、流程或系统的第一步是定义需求,即特定的功能性或非功能性需求:

  • 功能需求:描述产品或服务如何满足客户需求,这包括记录用户如何与产品或服务交互的用例中的特性和功能。

  • 非功能性需求:对用户来说并不明显的操作和产品属性,包括性能、可用性、耐用性、安全性和财务等。

功能性需求可以被认为是产品需要为客户做的事情,而非功能性需求被认为是产品或服务在设计时需要满足的约束。

按照“FURPS+”需求类型,用例强调了“F”(功能性或行为性),但也可以用于其他类型,特别是与用例紧密相关的那些类型。在统一过程和其他现代方法中,用例被推荐作为发现和定义需求的核心机制。

image

image

2. 用户故事,初步需求提炼

Use Story更适合比较早期的探索需求阶段,因为他表述起来非常的简单,一个User Story只需要几句话就可以完成,另外一个因素就是用户需求的细节是非常易变的,而其高层描述却是相对稳定的,所以我们可以通过使用User Story的方法来从高层确定其需求(包括功能性的和非功能性的),这些单独的Use Story相当于系统中可能要实现的一点,而由我们通过与用户交流所得到的所有User Story则构成了一个面,它就是整个系统所需要实现的功能。

image

如上图所示,用户故事是一种快速捕获产品需求的“谁”、“什么”和“为什么”的轻量级方法。简而言之,用户故事是表达用户想要的需求的想法。用户故事也是“待办事项”列表,可帮助您确定项目路径中的步骤,它们有助于确保您的流程和最终产品满足您的要求。

3.用户用例,进一步需求细化

Use Case更适合于需求分析阶段,因为该阶段需要比较详细的、更系统的需求分析。

3.1 用例形式与准则

用例能够以不同形式化程度或格式进行编写,主要有下面几种。

摘要形式:简洁的一段式概要,通常用于描述主要成功场景。在早期需求分析过程中,为快速了解主题和范围。

处理销售:顾客携带所购商品到达收银台。收银员使用POS系统记录每件商品。系统连续显示累计总额,并逐行显示细目。顾客输入支付信息,系统对支付信息进行验证和记录。系统更新库存信息。顾客从系统得到购物小票,然后携带商品离开。

非正式形式:非正式的段落格式。用几个段落覆盖不同场景。前例中处理退货就是非正式形式的用例。

主成功场景:

顾客携带商品到收银台退货。收银员使用POS系统记录并处理每件退货……

交替场景:

如果客户之前用信用卡付款,而其信用卡账户退还交易被拒绝,则告知顾客并使用现金退款。

如果在系统中未查找到该商品的标识码,则提示收银员并建议手工输入标识码(可能标识码已经损坏)。

如果系统检测到与外部记账系统通信失败,则……

详述形式:详细编写所有步骤及各种变化,同时具有补充部分,如前置条件和成功保证,模板如下:

image

遵循一些用例准则,能更好的帮助我们进行用例设计和编写,下面是一些主要的准则。

准则1:以无用户界面约束的本质风格编写用例,Constantine将摒除UI细节并集中于用户真实意图的用例编写风格称为本质(essential)风格。

在需求研讨会上,收银员可能会说其目标之一表述为“登录”。此时收银员大概会想象有图形界面、对话框、用户ID和口令。这是实现目标的一种机制,但不是目标本身。通过对目标层次的研究(“何为目标之目标?”),系统分析员会发现与实现机制无关的目标:“标识自己的身份并得到认证”,或是更为高层的目标:“防盗……”。

准则2:编写简洁的用例,你喜欢阅读大量的需求吗?我不这么认为。所以应编写简洁的用例。删除“噪音”词汇,因为即使一些细微之处也会累积为繁琐,例如编写时应用“系统认证……”,而不是“这个系统认证……”

准则3:编写黑盒用例,黑盒用例(black-box use case)是最常用和推荐使用的类型;它不对系统内部工作、构件或设计进行描述。反之,它以通过职责来描述系统,这是面向对象思想中普遍统一的隐喻主题—软件元素具有职责,并且与其他具有职责的元素进行协作。通过使用黑盒用例定义系统职责,人们可以规定系统必须做什么(行为和功能行需求),而不必决定系统如何去做(设计)。实际上,“分析”与“设计”的区别就在于“什么”和“如何”的差异。这是在优秀软件开发中的重要主题:在需求分析中应避免进行“如何”的决策,而是规定系统的外部行为,就像黑盒一样。此后,在设计过程中创建满足该规格说明的解决方案。

image

准则4:为满足主要参与者的目标,产生可观测的结果而定义用例,基本的过程如下:

  • 步骤1:选择系统边界

  • 步骤2和3:寻找主要参与者和目标

image

参与者有其目标,并且使用应用以达成这些目标。用例建模的观点就是寻找这些参与者及其目标,创建产生有价值结果的解决方案。这里要对用例建模者强调一个细微差别。在开始用例建模时,首先要询问的是“谁来使用系统,他们的目标是什么?”而非“系统的任务是什么?”实际上,用例的名称应该反映出用户的目标,这样能够强调这一观点,例如,“目标:获取或处理销售,用例:处理销售”。

  • 步骤4:定义用例,一般来说,为每个用户目标分别定义用例。用例的名称应该和用户目标类似。例如,目标:处理销售;用例:处理销售。用例名称应使用动词开头。对于每个目标的一个用例来说,常见的例外是,将分散的CRUD(创建、提取、更新、删除)目标合并成一个CRUD用例,并习惯性地称为管理<X>。例如,管理用户用例可以同时满足“编辑用户”、“删除用户”等目标。

准则5:一些测试有助于发现有用的用例,一般来说不要提出“什么是有效用例”这样的问题,更为实际的问题是“对应用需求分析来说,表示用例的有效级别是什么?”下面给出一些经验方法:

老板测试,你的老板问:“你整天都做了些什么?”你回答:“登录系统!”你的老板会高兴吗?如果不会,那么该用例不会通过老板测试,这意味着该用例与达到可量化价值的结果没有太大关系。这也许是更低级别目标的用例,但不是需求分析所适用的级别。

EBP测试,EBP即基本业务过程(Elementary Business Process),是源于业务过程工程领域的术语[8],定义如下:一个人于某个时刻在一个地点所执行的任务,用以响应业务事件。该任务能够增加可量化的业务价值,并且以持久状态留下数据。EBP测试与老板测试类似,尤其是对于业务价值可量化这一限制而言。

规模测试,用例很少由单独的活动或步骤组成,相反,用例通常包含多个步骤,在详述形式的用例通常需要3~10页文本。用例建模中的一个常见错误就是仅将一系列相关步骤中的一个步骤定义为用例,例如将输入商品ID定义为用例。你会因其规模过小而获得错误的提示,上述用例名称错误地将一系列步骤中的一个步骤作为用例,如果你以详述形式想象一下,你会发现该用例出奇的短。

3.2 用例间关联精化

用例彼此之间可能具有联系。例如,处理信用卡支付用例可作为处理销售、处理租金等常规用例的一部分。

通过关联将用例组织起来,不会对系统需求和行为产生影响。相反,这只是简单的组织机制,能够(理想地)促进对用例的理解和沟通、减少文本重复、改善用例文档的管理。

但要避免陷入用例关系的陷阱,但是首先应意识到,用例关系具有一些价值,但更重要的工作是编写用例文本。说明需求是通过编写用例文本完成的,而不是通过组织用例完成的,组织用例只是可选步骤,可能会改善对用例的理解、减少重复。此外,在UP和其他迭代方法中,将用例关系组织起来可以在细化阶段逐步演化;在项目开始阶段,如同瀑布方法那样,试图定义和精化完整的用例图及其关系,并没有益处。

包含关系,这是最常见、最重要的关系。多个用例中存在部分相同的行为,这是常见的现象。例如,处理销售、处理租金、向失业救济计划捐献等用例可能都包含了信用卡支付的交互行为。与其重复文本描述,不如将这部分交互行为分离为单独的子功能用例,并使用包含关系加以指示。这是对文本的简单重构和链接,可以避免冗余。

对大部分涉及用例间关系的问题,都可以使用包含关系处理。概括如下:如下情形可以分解出子功能用例并使用包含关系:用例在其他用例中重复使用。用例非常复杂并冗长,将其分解为子单元便于理解

image

扩展关系,假设某个用例文本因为某些原因不能被修改(至少不能大改)。不断地修改用例,添加无数新的扩展和条件步骤,这样可能会导致用例难以维护,或者用例作为稳定的制品已经成为了基线,并且不能改动。那么,在不修改原始文本的情况下,如何向用例添加内容?此时是使用扩展技术最为现实的动机。

image

泛化关系,许多用例专家即使不使用这种可选关系也能够成功完成用例工作,这些关系对用例增加了另一层复杂度,并且业内人士也未就如何从中获益达成共识。用例顾问们共同的观测结论是,大量用例关系会导致复杂结果,并且会花费大量徒劳的时间。

image

4. 用图表示,理解沟通更高效

原始人没有文字,漫长的进化过程诞生了文字后,人类处理图像的进程比语言快了60000倍。我们回忆图片类的信息要比文字类信息容易6倍,这也是“一图胜千言”的原因。

4.1 用例图,关联角色功能

用例图,UML提供了用例图表示法,用以描述用例名称和参与者及其之间的关系,简单的用例图还是能够为系统提供简洁可视的语境图,能够阐述外部参与者及其对系统的使用。

image

用例图是,一种优秀的系统语境图(context diagram),也就是说,用例图能够展示系统边界、位于边界之外的事物以及系统如何被使用。用例图可以作为沟通的工具,用以概括系统及其参与者的行为

4.2 顺序图,描述系统交互

系统顺序图(SSD)表示的是,对于用例的一个特定场景,外部参与者产生的事件,其顺序和系统之内的事件。所有系统被视为黑盒,该图强调的是从参与者到系统的跨越系统边界的事件。应为每个用例的主成功场景,以及频繁发生的或者复杂的替代场景绘制SSD

SSD是用例模型的一部分,将用例场景隐含的交互可视化。用例文本及其所示的系统事件是创建SSD的输入。SSD中的操作可以在操作契约中进行分析,在词汇表中被详细描述,并且(最重要的是)作为设计协作对象的起点。

image

image

为什么绘制SSD,软件设计中一个有趣且有用的问题是:我们的系统中会发生什么事件?为什么?因为我们必须为处理和响应这些事件(来自于鼠标、键盘、其他系统……)来设计软件。基本上,软件系统要对以下三种事件进行响应:来自于参与者(人或计算机)的外部事件 ,时间事件和错误或异常(通常源于外部)。因此,需要准确地知道,什么是外部输入的事件,即系统事件。这些事件是系统行为分析的重要部分。

在对软件应用将如何工作进行详细设计之前,最好将其行为作为“黑盒”来调查和定义。系统行为(system behavior)描述的是系统做什么,而无需解释如何做。这种描述的一部分就是系统顺序图。

UML没有定义所谓的"系统顺序图",而只是定义了"顺序图",SSD展示了用例中一个场景的系统事件,因此它是从对用例的考察中产生的。

SSD中所示的元素(操作名称、参数、返回的数据)是简洁的。需要对这些元素加以适当的解释以便在设计时能够明确地知道输入了什么,输出了什么。词汇表是详细描述这些元素的最佳选择。对大多数制品来说,一般在词汇表中描述其细节。比如,一条返回线含有描述:“change due, receipt”。其中关于票据(复杂报表)的描述是含糊的。所以,在UP词汇表中可以加入票据条目,显示票据样本(可以是数码图片)、详细内容和布局。

操作契约,目标: 定义系统操作,为系统操作创造契约

在UP中,用例和系统特性是用来描述系统行为的主要方式,并且足以满足要求。有时需要对系统行为进行更为详细和精确的描述。操作契约使用前置和后置条件的形式,描述领域模型里对象的详细变化,并作为系统操作的结果。领域模型是最常用的OOA模型,但是操作契约和状态模型也能够作为有用的与OOA相关的制品。

image

image

image

契约的主要输入是SSD中确定的系统操作(例如enterItem)、领域模型和领域专家的见解。该契约也可以作为对象设计的输入,因为它们描述的变化很可能是软件对象或数据库所需要的。

后置条件(postcondition)描述了领域模型内对象状态的变化。领域模型状态变化包括创建实例、形成或消除关联以及改变属性。后置条件不是在操作过程中执行的活动,相反,它们是对领域模型对象的观察结果,当操作完成后,这些结果为真,就像浓烟散去后所能够清晰看到的事物。

概括说来,后置条件可以分为以下三种类型:

  • 创建或删除实例

  • 属性值的变化

  • 形成或消除关联(精确地讲,是UML链接)

为什么需要后置条件首先,后置条件并不总是必要的。在大多数情况下,系统操作的效果对开发者而言是相对清晰的,他们可以通过阅读用例、与专家交流或根据自己的知识对此进行理解。但有时需要更详细和精确的描述。契约正提供了此类描述。契约是优秀的需求分析或OOA工具,能够详细描述系统操作(就领域模型对象而言)所需的变化,而无需描述这些操作是如何完成的。

在UP中,用例是项目需求的主要知识库。用例可以为设计提供大部分或全部所需细节。这种情况下,契约就没有什么作用。但有时对于用例而言,所需状态变化的细节和复杂性难以处理或过于细节化。后置条件的形式提供并提倡了十分精确的分析性的语言,能够支持充分的详细程度。如果开发者在没有操作契约的情况下,能够准确地理解所需要完成的工作,则可以不编写契约。

如何创建和编写契约,创建契约时可以应用以下指导:

1)从SSD中确定系统操作

2)如果系统操作复杂,其结果可能不明显,或者在用例中不清楚,则可以为其构造契约

3)使用以下几种类别来描述后置条件:创建和删除实例。修改属性。形成和清除关联。

在UML中,操作契约也可以使用更严谨的对象约束语言(OCL)来定义,建议参考Warmer和Kleppe所著的《对象约束语言:UML精确建模》(The Object Constraint Language:Precise Modeling with UML)。

4.3 活动图,描述业务会话

活动图,一种有助于使工作流和业务过程可视化的图。因为用例涵盖过程和工作流分析,所以活动图能够成为编写用例文本的有用的辅助措施,对于那些描述复杂工作流的业务用例来说更是如此,因为其中涉及大量参与者和并发活动。

image

image

活动图描述了从一个活动到另一个活动的控制流,描述了活动的顺序,活动表示正在处理的事物的动作和状态。活动图的视角是系统中对象的不同活动之间的交互,活动之间的交互可以更好地理解问题。

活动图常用于业务过程建模,尽管可以用例文本描述业务过程,但活动图恰是“图画胜于千言”这一说法的最好例证,通过活动图可视化的手段来理解其复杂的业务过程。分区有助于观察多个参与者以及业务过程中涉及的并行动作,对象节点可以描述动作周围移动的事物。对当前的业务流程建模之后,客户可以可视化地变更和优化业务过程。

image

image

活动图常用于数据流建模,从20世纪70年代开始,数据流图(DFD)就已经成为流行的方法,用于对软件系统过程中所涉及的主要步骤和数据进行可视化,DFD可以用来记录主要数据流或进行较高层面的数据流设计。

对于文档化和探索来说,DFD模型所提供的信息都具有效用,但UML中并没有包含DFD表示法。UML活动图能够满足同一目的——用于数据流建模,从而代替传统的DFD表示法

image

image

活动图常用于并发编程和并行算法建模,并发编程问题中的并行算法涉及多个分区、分叉点和连接点等行为。使用UML活动图分区(partition)来表示不同的操作系统线程或进程。使用对象节点(object node)对共享对象和数据进行建模。同时,分叉点(fork)用于对多个线程(或进程)的创建和并行执行进行建模,每分区一个线程(或进程)。

image

在活动图建模方面,有下面一些准则:活动图通常对于涉及众多参与者的非常复杂的业务过程建模具有价值。对于简单的业务过程,用例文本就够用了。在进行业务过程建模时,可以利用耙子(rake)符号和子活动图。在level 0图的概览中,保持较高的抽象水平,从而使图形具有清晰、简洁的品质。在level 1,甚至可能是level 2的子图中扩展细节。与上一条相关的是,尽量保持同一张图中所有动作节点的抽象水平一致。

image

更多的活动图学习,可以参考vp

二、OOA:定义领域模型,熟悉领域

面向对象分析,关注从对象的角度创建领域描述, 面向对象分析需要鉴别重要的概念,属性和关联。

面向对象分析的结果可以表示为领域模型,在领域模型中展示重要的领域概念或者对象。

需要注意的是,领域模型并不是对软件对象的描述,它是真实世界领域中的概念和想象的可视化。因此,它也被称为概念对象模型。

相关的用例概念和专家的观点将作为创建领域模型的输入。反过来,该模型又会影响操作契约、词汇表和设计模型,尤其是设计模型中领域层(domain layer)的软件对象。

image

1. 什么是领域模型

在思维和认知层面上,如果要加速对业务和某领域的学习,创建领域模型是我们能更好的理解其关键概念和词汇重要途径之一。

领域模型这个名词比较抽象,也没有一个准确的定义,要想很好的理解,先看下一下哲学层面概念、内涵和外延。

概念,是一种思维方式,我们通过概念来认识、指称、分辨事物。

概念的内涵与外延是哲学中的关键概念,涉及到概念的本质和范围。这两个概念最早由德国哲学家弗朗茨·布伦塔诺在他的著作《心灵论》中提出,并随后在逻辑学、语义学和认知科学领域得到广泛运用。

基尔班是20世纪逻辑学的杰出代表,他在《逻辑学导论》基尔班认为:内涵是指一个概念所包含的一切属性和特征,是概念的意义或含义;外延则是指概念所能涵盖的所有具体实例或事物的集合,是概念的范围。

image

例如,考虑购买交易事件的概念类。我可以使用符号Sale对其命名。Sale的内涵可以陈述为“表示购买交易的事件,并且具有日期和时间。”Sale的外延是所有销售的例子,换句话说,就是世界上所有销售实例的集合。

image

对领域模型应用UML类图表示法会产生概念透视图(conceptual perspective)模型。

image

概念类,简单理解就是对概念的内涵(属性)进行符号化表示,常用UML类图进行表示。

领域模型(domain model)是在特定领域内,对概念类或现实世界中对象的可视化表示。领域模型也称为概念模型、领域对象模型和分析对象模型。

领域模型一般使用图形化UML表示法,当然描述的信息也可以采用纯文本方式(UP词汇表)表示。但是在可视化语言中更容易理解这些术语,特别是它们之间的关系,因为我们的思维更擅长理解形象的元素和线条连接。因此,领域模型是可视化字典,表示领域的重要抽象、领域词汇和领域的内容信息。

image

应用UML表示法,领域模型被描述为一组没有定义操作(方法的特征标记)的类图(class diagram)。它提供了概念透视图。它可以展示:领域对象或概念类,概念类之间的关联,概念类的属性。

领域模型不是数据模型(通过对数据模型的定义来表示存储于某处的持久性数据),所以在领域模型里,并不会排除需求中没有明确要求记录其相关信息的类(这是对关系数据库进行数据建模的常见标准,但与领域建模无关),也不会排除没有属性的概念类。例如,没有属性的概念类是合法的,或者在领域内充当纯行为角色而不是信息角色的概念类也是有效的。

综上:领域模型是在特定领域内,对概念类或现实世界中对象的可视化表示,通过创建领域模型,我们能更好的理解这个领域的重要概念对象,对象间关系和以及对象的内涵属性等,常用UML图形化表示。领域模型也称为概念模型、领域对象模型和分析对象模型。

2. 为什么创建领域模型

思维和认知上加速学习,加速对业务和领域的学习,创建领域模型的原因之一就是能够使我理解其关键概念和词汇。

领域模型能指导软件代类定义,实现上降低表示差距,降低与OO建模之间的表示差异,这是OO的关键思想:领域层软件类的名称要源于领域模型中的名称,以使对象具有源于领域的信息和职责,这样可以减小我们的思维与软件模型之间的表示差异。

image

3. 如何创建领域模型

领域模型的确定不是一蹴而就的,需要反复进行分析、精华和验证。

xx.png

3.1 寻找概念类

既然领域模型表示的是概念类,那么关键问题是如何才能找到概念类。

方法1:重用和修改现有的模型。这是首要、最佳且最简单的方法,如果条件许可,通常从这一步开始。在许多常见领域中都存在已发布的、绘制精细的领域模型和数据模型(可以修改为领域模型),这些领域包括库存、金融、卫生等等。总之:经验积累,行业研究,竞品抄作业等都是极好的方法。

image

方法2:积累和使用分类列表,我们可以通过制作概念类候选列表来开始创建领域模型。平时注意积累,归纳各个领域和场景场景的概念类的类别。

image.png

方法3:通过识别名词短语寻找概念类,一种有效(因为简单)技术是语言分析(linguistic analysis),即在对领域的文本性描述中识别名词和名词短语,将其作为候选的概念类或属性。使用这种方法时必须小心,不可能存在名词到类的映射机制,并且自然语言中的词语具有二义性。尽管如此,语言分析仍不失为另一种灵感来源。详述形式用例中的描述对这种分析极为适合。

领域模型是重要领域概念和词汇的可视化。那么从哪里找到这些术语?其中某些术语来源于用例。另外一些术语则源于其他文档,或是专家的想法。无论如何,用例都是挖掘名词短语的重要来源之一。其中一些名词短语是候选的概念类,有些名词所指的概念类可能会在当前迭代中忽略,还有一些可能只是概念类的属性。建议以区分概念类和属性。

这种方法的弱点是自然语言的不精确性,不同名词短语可能表示同一概念类或属性,此外可能还有歧义。

在创建领域模型时最常见的错误是,把应该是概念类的事物表示为属性。下面是预防这种错误的经验。

如果我们认为某概念类X不是现实世界中的数字或文本,那么X可能是概念类而不是属性。

  1. 在现实世界中有没有非常具象的表示或者占据物理空间,比如Store商店不会被认为是数字或文本,这一术语表示的是合法实体、组织和占据空间的事物。因此Store应该是概念类。

  2. 需不需要对对象进行更多信息的描述与刻画,有可能短期不需要,长期需要。如果需要的话,可以作为概念类

3.2 建立关联关系

找到并表示出关联是有助的,这些关联能够满足当前所关注场景的信息需求,并且有助于理解领域。

关联(association)是类(更精确地说,是这些类的实例)之间的关系,表示有意义和值得关注的连接。

image

在UML中,关联被定义为“两个或多个类元之间的语义联系,涉及这些类元实例之间的连接”。

何时表示关联?如果存在需要保持一段时间的关系,将这种语义表示为关联(“需要记住”的关联),一般后续有场景需要对关联进行使用。例如,我们需要记住SalesLineItem实例和Sale实例之间的关系吗?答案是肯定的,否则将无法再现销售、打印票据或计算销售总额。

应该避免加入大量关联,我们要避免在领域模型中加入太多的关联。回顾离散数学的相关知识,可以知道,在具有n个节点的图中,节点间有(n*(n-1))/2个关联,这可能是个非常大的数值。具有20个类的领域模型可以有190条关联线!连线太多会产生“视觉干扰”,使图变得混乱。所以要谨慎地增加关联线。

由于领域模型是从概念角度出发的,所以是否需要记录关联,要基于现实世界的需要,而不是基于软件的需要,尽管在实现过程中,会有出现大量对关联的需要。

关联是否会在软件中实现?在领域建模过程中,关联不是关于数据流、数据库外键联系、实例变量或软件方案中的对象连接的语句;关联声明的是针对现实领域从纯概念角度看有意义的关系。也就是说,这些关系的大部分将作为(设计模型和数据模型中的)导航和可见性路径在软件中加以实现。但是,领域模型不是数据模型;添加关联是为了突出我们对重要关系的大致理解,而非记录对象或数据的结构。

关联的UML表示,关联被表示为类之间的连线,并冠以首字母大写的关联名称。关联的末端可以包含多重性表达式,用于指明类的实例之间的数量关系。关联本质上是双向的,就是说无论从哪个类实例出发,到另一端的逻辑遍历都是可能的。这种遍历是纯抽象的,这与软件实体间的连接无关。

可选的“阅读导向箭头”指示阅读关联名称的方向;但它并不表示可见性或导航的方向。如果没有表示箭头,习惯是从左向右读,或自上向下读,尽管UML没有将其作为规则

image

在UML中如何对关联命名准则以“类名-动词短语-类名”的格式为关联命名,其中的动词短语构成了可读的和有意义的顺序。关联名称应该使用首字母大写的形式,因为关联表示的是实例之间链接的类元。在UML中,类元应该首字母大写

关联的角色和多重性,关联的每一端称为角色(role)。角色具有如下可选项:多重性表达式、名称、导航。

多重性(multiplicity)定义了类A有多少个实例可以和类B的一个实例关联。多重性的值表示在特定时刻(而不是在某个时间跨度内)有效关联的实例数量。例如,二手车可能会在一段时间内重复卖给多个二手车经销商。但是在某一时刻,这辆二手车只Stocked-by(被存货于)一个经销商。二手车在任何时刻并不Stocked-by多个经销商。类似的,在一夫一妻制的国家里,某一时刻一个人只能Married-to另一个人,即使在一段时间内,可以与多个人结婚。

image

3.3 补充类属性

确定概念类的属性是有助的,能够满足当前所关注场景的信息需求,属性(attribute)是对象的逻辑数据值。

何时展示属性 ?当需求(例如,用例)建议或暗示需要记住信息时,引入属性和导出属性。下面是UML属性表示法

image

image

导出属性Sale中的total属性可以从SalesLineItems中的信息计算或导出。当我们需要表达:这是重要属性,但是导出的,那么可以使用UML的约定:在属性名称前加以“/”符号。

什么样的属性类型是适当的 ?通俗地说,大部分属性类型应该是“简单”数据类型,例如数字和布尔。通常,属性的类型不应该是复杂的领域概念,通过关联而不是属性来表示概念类之间的关系。

何时定义新的数据类型类?商品的标识符是对各种常用编码方案的抽象,这些方案包括UPC-A,UPC-E和EAN方案族。这些数字编码方案的不同部分可以标识制造商、产品、国家(对于EAN)和用于验证的校验和数字。因此,应该加入数据类型ItemID类,因为它能满足上述大部分准则。price和amount属性的数据类型应该是Money类,因为它们是货币单位的数量。address属性的数据类型应该是Address类,因为其具有不同的小节。

image.png

image

总之,如果需要关注更多信息,才将基本类型定义为新的数据类型。

任何属性都不表示外键, 任何属性都不表示外键,外键的方式用属性的关联,可以用许多方法来表示对象之间的关系,外键就是其中一种方法,我们将推迟到设计时再决定如何实现对象关系,以避免设计蠕变(design creep)

image

3.4 模型是否正确

没有所谓唯一正确的领域模型,所有模型都是对我们试图要理解的领域的近似。领域模型主要是在特定群体中用于理解和沟通的工具。有效的领域模型捕获了当前需求语境下的本质抽象和理解领域所需要的信息,并且可以帮助人们理解领域的概念、术语和关系。

image

在迭代开发中,我们会通过若干迭代对领域模型进行增量地演进。在每个迭代中,领域模型都会限定于之前和当前要考虑的场景,而不是膨胀为瀑布风格的“大爆炸”模型,以过早试图捕获所有可能的概念类和联系。

避免瀑布思维倾向,为完成详尽或“正确”的领域模型而进行大量建模工作,这些方式都应避免,这种过量的建模工作反而会导致分析停滞,这种投入几乎不会有什么回报。每次迭代的领域建模时间不超过几个小时

4. 领域模型进一步精化

使用泛化、特化、关联类、组合和包等概念精化领域模型。

泛化和特化是领域建模中支持简练表达的基本概念,更进一步讲,概念类的层次结构经常成为激发软件类层次结构设计的灵感源泉,软件类层次结构设计利用继承机制减少了代码的重复;关联类捕获关联关系自身的信息;时间间隔反映了某些业务对象仅在有限的一段时间内有效这一概念;使用包可以将大的领域模型组织成较小的单元。

4.1 泛化特化层次

CashPayment、CreditPayment、CheckPayment这些概念很相似。在这种情形下,可能和有用的方法是将它们组织成泛化-特化类层次结构(或简称为类层次结构),其中超类Payment表示更为普通的概念,子类表示更为特殊的概念。

image

泛化(generalization)是在多个概念中识别共性和定义超类(普遍概念)与子类(具体概念)关系的活动。此活动对概念进行分类学意义上的分类,并将其在类层次结构中表示出来。

在领域模型中识别父类和子类是一个有价值的活动,这样可以使我们对概念有更概括、精炼和抽象的描述。它可以精简表示、改善理解、减少重复信息。尽管我们现在关注领域模型,而不是软件设计模型,但软件设计模型设计和使用继承将超类和子类实现为软件类也会得到更好的软件质量。

在UML中,从较特殊元素指向较一般元素的中空心箭头表示元素之间的泛化关系,无论独立箭头风格还是共享箭头风格,都可以使用。

image

概念超类和子类之间是什么关系?

  • 100%规则",概念超类的定义必须100%适用于子类,子类必须100%与超类一致:属性关联

  • "is-a"规则,子类集合的所有成员必须是其超类集合的成员。使用自然语言,通常可以通过构造下面的陈述语句进行非正式的测试:子类是一种(is-a)超类。

image

image

image

何时定义概念子类 ?将概念类划分为子类的动机主要是“需要特殊关注定义概念类”,比如:

  1. 子类有额外的有意义的属性。

  2. 子类有额外的有意义的关联。

  3. 子类概念的操作、处理、反应或使用的方式不同于其超类或其他子类,而这些方式是我们所关注的。

  4. 子类概念表示了一个活动体(例如动物、机器人等),其行为与超类或者其他子类不同,而这些行为是我们所关注的。

何时定义概念超类 ?通常在识别出潜在子类的共性时,将其泛化为公共的超类。下面是泛化和定义超类的动机:

  1. 潜在的概念子类表示的是相似概念的不同变体。

  2. 子类满足100%和Is-a规则。所有子类都具有相同的属性,可以将其解析出来并在超类中表达。

  3. 所有子类都具有相同的关联,可以将其解析出来并与超类关联。

抽象类定义: 超类需要满足,如果类C的每个成员也必须是某个子类的成员,则类C被称为抽象概念类(abstract conceptual class)。假定每个Payment类的实例必须更为特化地是某个子类如CreditPayment、CashPayment或CheckPayment的实例。由于每个Payment类的成员也是某个子类的成员,因此按照定义,Payment类是一个抽象概念类。

image

image

对变化的状态建模,对状态的划分和建模一般有两种方式,方式之一是建模为不同的状态子类,方式二是对状态进行建模,采用关联方式进行表达。一般采用对单独的状态进行建模,并且在领域模型中忽略概念的状态,而在状态图中加以反应。

image

因为这里的讨论是关注领域模型的概念观点,而非软件对象,因此关于概念类层次结构的这个讨论并未提及继承关系。采用一种面向对象语言,通过创建软件类层次结构(software class hierarchies),软件子类继承超类的属性和操作定义。继承(inheritance)是使超类的特性适应于子类的软件机制,它支持对子类代码的重构,将其置入超类之中,形成类层次结构。因此,在领域模型中讨论继承没有意义,而当我们转向设计和实现视图时它才具有重要意义。

image

这里所产生的概念类层次结构未必会反映在设计模型中,实现的时候可以被扩展或折叠形成另一种软件类层次结构,这取决于编程语言特性和其他因素,C++模板类有时可能会减少类的数量。

4.2 构建关联类

增加关联类的准则如下:准则在领域模型中增加关联类的可能线索有:

  1. 有某个属性与关联相关。

  2. 关联类的实例具有依赖于关联的生命期。

  3. 两个概念之间有多对多关联,并且存在与关联自身相关的信息。

多对多关联的存在是使用关联类的最常见线索。当你见到多对多关联,则需要考虑使用关联类。

image

聚合(aggregation)是UML中的一种模糊关联,其不精确地暗示了整体-部分关系(如同多数普通关联)

听从UML创始人的建议,不要在UML中费心去使用聚合;而且,在适当的时候要使用组合。

组合(composition)也称为组成聚合(composite aggregation),这是一种很强的整体-部分聚合关系,并且在某些模型中具有效用。组合关系意味着:

  1. 在某一时刻,部分(例如方格)的一个实例只属于一个组成实例(例如棋盘);

  2. 部分必须总是属于组成(不存在随意游离的手指);

  3. 组成要负责创建和删除其部分——即可以自己来创建/删除部分,也可以与其他对象协作进行创建/删除部分。与该约束相关的是,如果组成被销毁,其部分也必须被销毁,或者依附于其他组成——不允许存在游离的手指!

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

  1. 部分的生命期在组成的生命期界限之内,部分的创建和删除依赖于整体。

  2. 在物理或者逻辑组装上,整体-部分关系很明确。

  3. 组成的某些属性(例如位置)会传递给部分。

  4. 对组成的操作(例如销毁、移动和记录等)可能传递给部分。

显示组合关系的益处,有利于澄清部分对整体依赖的领域约束,在组合关系中,部分的生命期不能超越整体的生命期。

  1. 在设计工作中,这会影响软件类和数据库元素(就参考完整性和层叠删除路径而言)的整体和部分之间的创建/删除依赖关系。

  2. 有助于使用GRASP创建者模式时识别创建者(组成)。

  3. 对整体的复制、拷贝这些操作经常会传递给部分。

image

4.3 划分包管理

领域模型可以很轻易地发展到足够大,这时理想的做法把它分解成与概念相关的包,因为这样有助于理解,并且有利于由不同的人在不同的子领域并行地进行领域分析工作。

如何划分领域模型?领域的划分和精细是非常复杂的,有非常多和非常细的参考原则,可以应用下面的通用准则:

  1. 在同一个主题领域,概念或目标密切相关的元素

  2. 在同一个类层次结构中的关系

  3. 参与同一个用例的元素

  4. 有很强的关联性的元素

  5. 康威定律,参考组织结构

  6. 更多可以参考:商品发布架构设计与实践,领域划分、自治与进化

image

image

三、OOD:定义交互图,理清职责和协作

面向对象设计关注软件对象的定义----他们的职责和协作。

顺序图(sequence diagram,UML的一种交互图)是描述协作常见的表示法。它展示出软件对象之间的消息流,和由消息引起的方法调用,是对象间动态的视图

image

对象模型有两种类型:动态和静态模型。动态模型有助于设计逻辑、代码行为或方法体,例如UML交互图(顺序图或通信图),动态模型倾向于创建更为有益、困难和重要的图形。静态模型有助于设计包、类名、属性和方法特征标记(但不是方法体)的定义。

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

应该把时间花费在交互图(顺序图或通信图),而不仅是类图上。忽视这一准则是十分常见的UML错误实践。在应用职责驱动设计和GRASP原则的动态建模过程中,这一准则尤为重要。

UML使用交互图(interaction diagram)来描述对象间通过消息的交互。交互图可以用于动态对象建模(dynamic object modeling)。交互图有两种类型:顺序图(sequence diagram)和通信图(communication diagram)

image

image

每种图都有其优点,而建模者也有其各自的偏好,因此没有所谓绝对“正确”的选择。然而,UML工具通常强调的是顺序图,因为顺序图具有更强的表示能力。顺序图在某些地方优于通信图。可能因为是首选,UML规范更多是以顺序图为核心,对其表示法和语义投入了更多的精力因此,顺序图对工具的支持更好,并且有更多有效的表示法选项。同时,采用顺序图可以更方便地表示调用流的顺序,仅需要由上至下阅读即可。而对于通信图,我们则必须查阅顺序编号,例如“1:”和“2:”。因此,顺序图在文档化方面更胜一筹,或者说,用UML工具对代码逆向工程生成的调用流顺序,使用顺序图更容易查阅。

但另一方面,在墙上绘制“UML草图”(敏捷建模实践)时利用通信图更具有优越性,因为其更具有空间效用。这是因为可以在任何位置(水平或垂直)方便地放置或擦除框图

image

下面是UML的常见画法和元素,可以做一些参考

image

四、OOD:定义设计类图,构建软件世界

丰富多彩的软件世界里面,交织着五彩斑斓的类,简单来看,对象主要有:

  • 来源于领域对象:领域对象更侧重于业务领域的抽象和描述,强调问题领域中的概念、关系和行为,它是从业务需求出发的。

  • 来源于数据对象:更关注数据的组织和结构,描述数据在计算机系统中的存储方式,它是从数据管理和系统实现出发的。比如数据库存储对象,缓存对象、搜索doc对象等等。

  • 行为和辅助对象: 为了实现特定的功能,满足低耦合高内聚而创建的一些功能性辅助类。

image

与领域模型表示的是真实世界的类不一样的,设计类图表示的软件世界的类,真实世界类并不一定等价于软件世界的类,但能启发软件设计类的设计,比如其中的某些类名和内容还是非常相似的。从这一方面讲,oo设计和语言能够缩小软件架构和我们所设想的领域模型之间的差异,即实现低表示差距(lower representational gap)

1. 基于UML表示类

除了在交互图中显示对象协作的动态视图外,还可以用设计类图来有效地表示类定义的静态视图,这样可以描述类的属性和方法。

image

当我们绘制交互图时,在此动态对象建模的创造性设计过程中会产生一组类及其方法,他们的对应关系如下图。

这也就是为什么定义交互图是那么重要。

image

因此,类图的定义能够从交互图中产生,虽然这表明一种线性的顺序,即先绘制交互图,再绘制类图。但是在实践中,尤其是应用了并行建模的敏捷建模实践后,这些互补的动态视图和静态视图是并行创建的。

下面是UML绘制类图场景的使用方式,更多可以参考

image

image

2. 认识职责与职责驱动

在 RDD 中,我们认为“软件对象具有职责”,这个定义很符合人在社会群体中分工协作的方式,软件也是人编写的,所以根据职责思考设计的软件系统符合人的行为习惯,同时更易于理解和管理。看亚当史密斯提出的劳动分工的重要性,软件对象亦是如此。

image

理解职责是顺利进行面向对象设计的关键,决定方法归属于哪个对象和对象之间如何交互,其意义重大,应谨慎从事。掌握OOD涉及一套柔性原则,自由度很大,这正是OOD的复杂所在。但OOD并不是魔术,其模式是可以命名(重要!)、解释和应用的。例子可以帮助我们,实践可以帮助我们。

Uml与设计原则,由于UML只是一种标准的、可视化建模语言,了解它的细节并不能教会你如何用对象思想来思考,而对象思想正是最重要的主题。UML有时候被描述成一种“设计工具”,但是这并不完全正确……最关键的软件开发工具是受过良好设计原则训练的思维,而不是UML或任何其他技术。

在绘图(和编写代码)活动当中,我们要运用各种OO设计原则,如GRASP和GoF(四人帮)设计模式。OO设计建模总的来说,基于职责驱动设计(RDD)所代表的内在含义是考虑怎样给协作中的对象分配职责。

RDD中,我们认为软件对象具有职责,即对其所作所为的抽象。UML把职责定义为“类元的契约或义务”。就对象的角色而言,职责与对象的义务和行为相关。职责分为以下两种类型:行为和认知。

image

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

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

RDD也包括了协作的思想,职责借助于方法来实现,该方法既可以单独动作,也可以与其他方法和对象协作。

例如,Sale类可以定义一个或多个方法来获取其总额,比如命名为getTotal方法。为了完成该职责,Sale可能与其他对象协作,例如向每个SalesLineItem对象发送getSubtotal消息以获取其小计金额。

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

GRASP对一些基本的职责分配原则进行了命名和描述,因此掌握这些原则有助于支持RDD。

3. GRASP:给对象分配职责

掌握基本对象设计和职责分配需要详细的原则和推理,对这些原则和推理进行命名和解释是可能的。GRASP原则或模式是一种学习工具,它能帮助你理解基本对象设计,并且以一种系统的、合理的、可以解释的方式来运用设计推理。对这种设计原则进行理解和使用的基础是分配职责的模式。一旦理解了这些模式,我们就拥有了讨论设计的丰富、共享的词汇。因为模式的名称能简洁地表达出复杂的设计概念,一个简短的句子也能表达出许多设计信息。例如以下这个句子:“我建议从抽象工厂模式生成策略模式来支持防止变异模式和关于<X>的低耦合”

设计对象交互和职责分配是对象设计的核心。这些设计决策对对象软件系统是否清晰、是否具有扩展性和可维护性具有重大影响,同时也对构件复用的程度和质量具有影响。职责分配可以遵循一定的原则,GRASP模式总结了面向对象设计最常用的原则。

GRASP是通用职责分配软件模式(General Responsibility Assignment Software Patterns)的缩写。之所以选择这个名称,是为了表明掌握(grasping)这些原则对于成功设计面向对象软件的重要性。 GRASP 原则是对其他原则和设计模式的归纳,设计模式有成百上千种,光是记住 GoF 23 种设计模式就已经很困难了,更别提还要记住每种模式的细节,因此需要对设计模式进行有效的归类,GRASP 中的原则描述了模式的本质。这样除了能够有助于加速对详细设计模式的学习之外,而且对发现其根本的基本主题(防止变异、多态、间接性等)更为有效,它能够帮助我们透过大量细节发现应用设计技术的本质。

image

什么是模式,模式 = 原则 + 惯用方案。经验的OO开发者(以及其他的软件开发者)建立了既有通用原则又有惯用方案的指令系统来指导他们编制软件。如果以结构化形式对这些问题、解决方案和命名进行描述使其系统化,那么这些原则和习惯用法就可以称为模式。简单地讲,好的模式是成对的问题/解决方案,并且具有广为人知的名称,它能用于新的语境中,同时对新情况下的应用、权衡、实现、变化等给出了建议。

“模式”的真实含义是长期重复的事物。设计模式的要点并不是要表达新的设计思想。恰恰相反,优秀模式的意图是将已有的经过验证的知识、惯用法和原则汇编起来;磨砺的越多、越悠久、使用得越广泛,这样的模式就越优秀。

因此,GRASP模式陈述的并不是新思想,它们只是将为广泛使用的基本原则命名并其汇总起来。对于OO设计专家而言,GRASP模式(其思想而非名称)应作为其基础和熟练掌握的原则。这是最关键的!

模式具有确切的名称,软件开发是一个年轻领域。年轻领域中的原则缺乏大家广泛认可的名称,这为沟通和培训带来了困难。模式具有名称,例如信息专家和抽象工厂。对模式、设计思想或原则命名具有以下好处:

  • 它支持将概念条理化地组织为我们的理解和记忆。

  • 它便于沟通。模式被命名并且广泛发布后(我们都同意使用这个名字),我们就可以在讨论复杂设计思想时使用简语(或简图),这可以发挥抽象的优势

职责、GRASP和UML图之间的联系,你可以想一想,在编写代码或建模时,如何给对象分配职责。在UML之中,绘制交互图是考虑这些职责(实现为方法)的时机。

image

因此,当我们在绘制UML交互图时,就是在决定职责的分配。GRASP中的基本原则,以指导在分配职责时可做的选择。这样,当绘制UML交互图以及编写代码时,你就可以运用GRASP原则了

3.1 创建者(Creator)

在OO设计中,你必须考虑的首要问题之一是:由谁创建对象X?这是行为职责

image

创建者模式指导我们分配那些与创建对象有关的职责,这是很常见的任务。创建者模式的基本意图是寻找在任何情况下都与被创建对象具有连接的创建者。如此选择是为了保持低耦合。

组合聚集部分,容器容纳内容,记录者进行记录,所有这些都是类图中类之间极为常见的关系。创建者模式建议,封装的容器或记录器类是创建其所容纳或记录的事物的很好的候选者。当然,这只是一个准则。注意,我们在考虑创建者模式时提到了组合(composition)的概念,组合对象是创建其组成部分的良好候选者。

有时,可以通过寻找具有初始化数据的类来确定创建者,这些数据将在创建过程中传递给被创建者。这实际上就是专家模式的例子。初始化数据在创建期间是通过某种初始化方法(如带有参数的Java构造器)来传递的。例如,假定在创建Payment实例时,需要使用Sale的总额对其进行初始化。因为Sale知道其总额,所以它是Payment的候选创建者。

对象的创建常常具有相当的复杂性,例如为了性能而使用回收的实例,基于某些外部特性值有条件地创建一个或一族类的实例,等等。在这些情况下,最好的方法是把创建职责委派给称为具体工厂(Concrete Factory)或抽象工厂(Abstract Factory)的辅助类,而不是使用创建者模式所建议的类。

image

优点,支持低耦合,这意味着它具有较低的维护依赖性和较高的复用性。这种方法可能不会增加耦合性,因为其所创建的类对于创建者类而言已经是可见的,正是由于存在已有的关联,因此它成为创建者。

相关模式或原则:低耦合,具体工厂和抽象工厂。

3.2 信息专家(Information Expert)

image

一个设计模型也许要定义数百或数千个软件类,一个应用程序也许需要实现数百或数千个职责。在对象设计中,当定义好对象之间的交互后,我们就可以对软件类的职责分配做出选择。如果选择得好,系统就会易于理解、维护和扩展,而我们的选择也能为未来的应用提供更多复用构件的机会。

如下例子:确定总额要哪些信息?其实就是销售的所有SalesLineItem实例及其小计之和。从领域模型,我们知道Sale实例包含了上述信息,按照信息专家建议的准则,Sale是适合这一职责的对象类,它是适合这项工作的信息专家。

image

image

更进一步,为了确定商品的小计,我们需要哪些信息呢?答案是SalesLineItem.quantity、和ProductDescription.price。SalesLineItem知道其数量和与其关联的ProductDescription。因此,根据专家模式,应该由SalesLineItem确定小计,它就是信息专家。为了实现获知并回答小计的职责,SalesLineItem必须知道产品价格。ProductDescription是回答价格的信息专家,因此SaleLineItem向它发送询问产品

信息专家经常用于职责分配,这是对象设计中不断使用的基本指导原则。专家并不意味着模糊或奇特的想法,它表达了一种“直觉”,即对象完成与其所具有的信息相关的职责。

注意,完成职责往往需要分布在不同对象类中的信息。这意味着许多“局部”的信息专家要通过协作来完成任务。例如,销售总额问题最终需要三个对象类协作完成。只要信息分布在不同对象上,对象就需要通过消息进行交互来共同完成工作。

信息专家模式(与对象技术中的其他事物一样)是对真实世界的模拟。我们一般把职责分配给那些具有完成任务所必需的信息的个体。例如在企业中,谁应当作出盈利或亏损的结论?答案是有权使用所有必要信息的人,或许是首席财务官。正如因为信息分布在不同的对象上,软件对象之间要互相协作一样,首席财务官也要和其他人协作。公司的首席财务官也许会要求会计师生成关于借贷的报告。

信息专家模式也不一定全是对真实世界的模拟,专家模式通常导致这样一种设计,软件对象所做的操作通常是作用于它们在真实世界中所代表的非生命体的那些操作;Peter Coad称之为“DIY”(Do It Myself)策略。例如,在真实世界中,不借助电子装置的帮助,销售本身无法告诉你它的总额,销售是一种非生命体,销售的总额是由某人计算出来的。但是在面向对象的软件领域,所有软件对象都是“活的”或“有生命的”,并且它们可以承担职责,完成任务。从根本上说,它们只完成那些与它们所知信息有关的事情。我称其为对象设计的“有生命”原则;

注意,在某些情况下,专家模式建议的方案也许并不合适,通常这是由于耦合与内聚问题所产生的。谁应当负责把Sale存入数据库呢?的确,大多数要保存的信息位于Sale对象中,于是专家会建议将此职责分配给Sale类。那么按照这一决定进行逻辑推理,每个类都应当有能把自身保存到数据库中的服务。但这样会导致内聚、耦合及冗余方面的问题。例如,Sale类现在必须包含与数据库处理相关的逻辑,如与SQL和JDBC(Java数据库连接)相关的处理逻辑。因此,Sale类不仅仅关注“作为销售”的纯应用逻辑。现在由于存在其他职责而降低了它的内聚。这个类必须与其他子系统的数据库服务进行耦合,如和JDBC服务耦合,而不只是与软件对象在领域层的其他对象耦合,所以使耦合度上升。这样也会导致在大量持久性类中重复出现类似的数据库逻辑。

所有这些问题都表明这种做法违反了基本架构原则,即设计要分离主要的系统关注。将应用逻辑置于一处(如领域软件对象),数据库逻辑置于另一处(如单独的持久性服务子系统)等,而不是在同一构件中把不同的系统关注混合起来。支持主要关注的分离可以改善设计中的耦合和内聚。因此,即使按照专家模式,把数据库业务的职责分配给Sale类是合理的,但是却由于其他原因(通常是内聚和耦合),会使我们最终得出不佳的设计。

优点,因为对象使用自身信息来完成任务,所以信息的封装性得以维持。这样就支持了低耦合,进而形成更为健壮的、可维护的系统。低耦合也是一种GRASP模式,将在下一节讨论。行为分布在那些具有所需信息的类之间,因此提倡定义内聚性更强的“轻量级”的类,这样易于理解和维护。该模式通常支持高内聚。

“把职责与数据置于一处”,“知其事,行其责”,“DIY”,“把服务与其属性置于一处”。

3.3 低耦合(Low Coupling)

专家指导我们,由于Board了解所有Square,所以将获知特定Square(具有唯一的名称)的职责分配给Board对象(它拥有这些信息,因此它是信息专家)。但是为什么专家模式给出这样的建议?可在低耦合原则中找到这个问题的答案。

简要地说,耦合(coupling)是元素与其他元素的连接、感知及依赖的程度的度量。如果存在耦合或依赖,那么当被依赖的元素发生变化时,则依赖者也会受到影响。子类与超类是强耦合的。调用对象B的操作的对象A与对象B的服务之间具有耦合作用。

低耦合原则适用于软件开发的许多方面,它实际上是构建软件最重要的目标之一。下面是低耦合模式的定义,从解决方案可知低耦并不是一个明确且可直接使用的方法,他只是强调如果有多个方案的时候,可以他用于进行评估。

image

耦合(Coupling)是对某元素与其他元素之间的连接、感知和依赖程度的度量。具有低(或弱)耦合的元素不会过度依赖于其他元素;“过度”是与语境相关的,但我们必须对此进行检查。这些元素包括类、子系统、系统等。具有高(或强)耦合的类依赖于许多其他的类,这样的类或许不是我们所需要的。有些类会遇到以下问题:由于相关类的变化而导致本体的被迫变化。难以单独地理解。由于使用高耦合类时需要它所依赖的类,因此很难重用。

假设,我们需要创建Payment实例并使它与Sale关联。哪个类应负责此事呢?因为在真实世界领域中,Register记录了Payment,所以创建者模式建议将Register作为创建Payment的候选者。Register实例会把addPayment消息发送给Sale,并把新的Payment作为参数传递给它

image

image

根据职责分配,哪个设计支持低耦合?在这两个例子中,我们都假设Sale最终都必须耦合于Payment。在第一个设计方案中,Register创建Payment,在Register和Payment之间增加耦合;在第2个设计方案中,Sale负责创建Payment,其中没有增加耦合。如果单独地从耦合的角度来看,第2个设计方案是首选,因为保持了总体上的的低耦合。这个例子说明两个不同模式(低耦合和创建者)为何会导致不同方案。

在C++、Java和C#这样的面向对象语言中,从TypeX到TypeY耦合的常见形式包括:

  • TypeX具有引用TypeY的实例或TypeY自身的属性(数据成员或实例变量)。

  • TypeX对象调用TypeY对象的服务。

  • TypeX具有以任何形式引用TypeY的实例或TypeY自身的方法。通常包括类型TypeY的参数或局部变量,或者由消息返回的对象是TypeY的实例。

  • TypeX是TypeY的直接或间接子类。

  • TypeY是接口,而TypeX是此接口的实现。

没有绝对的度量标准来衡量耦合程度的高低。重要的是能够估测当前耦合的程度,并估计增加耦合是否会导致问题,要极力避免产生具有负面影响的高耦合。

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

高耦合对于稳定和普遍使用的元素而言并不是问题。例如,J2EE应用能安全地将自己与Java库(java.util等)耦合,因为Java库是稳定、普遍使用的。

高耦合本身并不是问题所在,问题是与某些方面不稳定的元素之间的高耦合,这些方面包括接口、实现等。

低耦合的优点: 不受其他构件变化的影响,易于单独理解,便于复用。

在更高的目标层次上考虑,为什么期望低耦合呢?其实就是为了我们要减少变化产生的影响,因为低耦合往往能够减少修改软件所需的时间、工作量和缺陷。这只是个简要的回答,但是它对于构建和维护软件而言具有重大意义。

3.4 控制器(Controller)

在SSD分析期间,要首先探讨系统操作,这些是我们系统的主要输入事件,那什么是处理这些事件的第一个对象呢?

image

控制器(Controller)是UI层之上的第一个对象,它负责接收和处理系统操作消息。求助于控制器模式的指导,可以得到一般情况下可接受的、合适的选择。

image

控制器设计中的常见缺陷是分配的职责过多。这时,控制器会具有不良(低)内聚,从而违反了高内聚原则。

  1. 正常情况下,控制器应当把需要完成的工作委派给其他的对象,是一种外观控制器。控制器只是协调或控制这些活动,本身并不完成大量工作。

  2. 当把职责分配给外观控制器会导致低内聚或高耦合的设计时,通常是当外观控制器的职责过多而变得“臃肿”时,就需要考虑使用用例控制器,那么对于每个用例,应用使用不同的控制器。

对于同一用例场景的所有系统事件使用相同的控制器类,在消息处理系统中,可以用命令对象来表示和处理每个消息

3.5 高内聚 (High Cohesion)

image

注意,在左侧的方案中,MonopolyGame对象自己完成全部工作,而在右侧方案中,它为playGame请求对工作进行了委派和协调。

内聚是软件设计中的一种基本品质,内聚可以非正式地用于度量软件元素操作在功能上的相关程度,也用于度量软件元素完成的工作量。比如,有100个方法和2000行源代码(SLOC)的Big对象,要比只有10个方法和200行源代码的Small对象所完成的任务多很多。如果Big对象的100个方法覆盖了众多不同的职责领域(如数据库访问和随机数产生),那么Big对象比Small对象的功能内聚性更低。概括地讲,代码的数量及其相关性都是对象内聚程度的指示器。很明显,不良内聚(低内聚)不只是意味着对象仅依靠本身工作;实际上,具有2000行源代码的低内聚对象或许需要和大量其他对象进行协作。所有的交互也都会趋于产生不良(高)耦合。不良内聚和不良耦合通常是齐头并进的。

高内聚模式的定义如下

image

内聚性较低的类,它们会导致以下问题:难以理解、难以复用、难以维护,且非常脆弱,经常会受到变化的影响

内聚性低的类通常表示大粒度的抽象,或承担了本应委托给其他对象的职责。

假设我们要创建一个(现金)Payment实例,并使其与Sale关联。哪个类可以负责这项工作呢?看2种直观的方案:

image

image

以下是描述不同功能性内聚程度的一些场景:

  1. 非常低的内聚,由一个类单独负责完全不同的功能领域中的大量事物。假设存在一个称为RDB-RPC-Interface的类,它负责与关系数据库交互的所有工作,同时负责处理远程过程的调用。这是两个完全不同的功能领域,并且每个领域都需要大量代码来支持。因此,应该将这些职责分为一组与RDB访问相关的类和一组与RPC支持相关的类。

  2. 低内聚,由一个类单独负责一个功能性领域内的复杂任务。假设存在一个称为RDBInterface的类,它负责与关系数据库的交互的所有工作。这个类的所有方法之间是相互关联的,但是数量过多,并有需要大量的代码来支持,其中或许有成百上千个方法。这个类应当分为一组能分担RDB访问工作的、轻量级的类。

  3. 高内聚,由一个类负责某个功能领域中的相应职责,并与其他类协作完成任务。假设存在称为RDBInterface的类,它只负责与关系数据库的交互的部分。为了检索和存储对象,它要和许多与RDB访问有关的类交互。

  4. 适度内聚。由一个类负责几个不同领域中的轻量级和单独的职责,这些领域在逻辑上与类的概念相关,但彼此之间并不相关。假设存在称为Company的类,它负责了解所有雇员信息和财务信息的所有工作。这两个领域虽然在逻辑上都与公司的概念相关,但彼此之间并没有太紧密的关联。另外,其公共的方法并不多,其代码的总量也不多。根据经验,高内聚的类的方法数目较少、功能性有较强的关联,而且不需要做太多的工作。如果任务规模较大的话,它就与其他对象协作,共同完成这项任务。

高内聚的类优势明显,因为它易于维护、理解和复用。高度相关的功能性与少量的操作相结合,也可以简化维护和改进的工作。细粒度的、高度相关的功能性也可以提高复用的潜力。

高内聚模式(与对象技术中的许多事物一样)是对真实世界的类比。显而易见,如果一个人承担了过多不相关的工作,特别是本应委派给别人的工作,那么此人一定没有很高的工作效率。从某些还没有学会如何分派任务的经理身上可以发现这种情况,因此正承受着低内聚所带来的困难,并且变得“分身乏术”。

内聚和耦合,阴和阳。不良内聚通常会导致不良耦合,反之亦然。我把内聚和耦合称为软件工程中的阴和阳,因为它们是互相依赖的。例如,考虑一个GUI窗口小部件类,它代表并绘制窗口小部件,把数据存入数据库,并调用远程对象服务。这样,它不但完全不是内聚的,而且还耦合了大量(全异)元素。

image

在少数情况下,可以接受较低内聚一种情况是,将一组职责或代码放入一个类或构件中,以使某个人能方便地对其进行维护(尽管这种分组可能也会使维护工作变得很糟糕。但是假设应用中含有嵌入式SQL语句,而根据其他良好的设计原则,这些语句应该被分布到10个类中,如10个“数据库映射器”类。现在,通常仅有1~2个SQL专家知道如何才能最佳地定义和维护这些SQL语句。即使许多面向对象的程序员为此项目工作,但可能只有很少的面向对象程序员会有较强的SQL技能。假设这个SQL专家并不是合格的OO程序员。基于以上情况,软件架构师可以决定,把所有的SQL语句分组到一个类中,即RDBOperations类中,这样可以便于SQL专家在一个位置对所有SQL进行工作。

另一种情况是具有分布式服务器对象的低内聚构件。由于系统开销和性能与远程对象及远程通信相关,因此有时候需要创建数量较少并且规模较大的低内聚服务器对象,以便为大量操作提供接口。这种方法也与称为粗粒度远程接口的模式相关。在这种模式中,远程操作的粒度更粗,以便在远程操作调用中,可以完成或请求更多的工作,这样能够减轻网络的远程调用对于性能的不良影响。举个简单例子,使用一个接收一组数据的远程操作setData来代替具有三个细颗粒操作(setName、setSalary和setHireDate)的远程对象,可以减少远程调用从而获得较好的性能。

高内聚,能够更加轻松、清楚地理解设计。简化了维护和改进工作。通常支持低耦合。由于内聚的类可以用于某个特定的目的,因此细粒度、相关性强的功能的重用性增强。

3.6 多态(Polymorphism)

基于类型的选择——条件变化是程序的一个基本主题。如果使用if-then-else或case语句的条件逻辑来设计程序,那么当出现新的变化时,则需要修改这些case逻辑——通常遍布各处。这种方法很难方便地扩展有新变化的程序,因为可能需要修改程序的多个地方——任何存在条件逻辑的地方。

image

不要测试对象的类型,也不要使用条件逻辑来执行基于类型的不同选择。

image

多态是一个基本的设计原则,用于设计系统如何组织以处理类似的变化。基于多态分配职责的设计能够被简便地扩展以处理新的变化。例如,增加新的具有getTaxes多态方法的计算器适配器将会对现有系统产生很小的影响。

image

何时使用接口进行设计?多态意味着在大部分OO语言中要使用抽象超类或接口。何时应该考虑使用接口呢?普遍的答案是,当你想要支持多态但是又不想约束于特定的类层次结构时,可以使用接口。 如果使用了抽象超类AC而不是接口,那么任何新的多态方案都必须是AC的子类,这对于诸如Java和C#的单根继承语言来说将十分局限。经验的做法是,如果有一个具有抽象超类C1的类层次结构,可以考虑对应于C1中的公共方法特征标记来定义接口I1,然后声明C1来实现接口I1。这样,对于新的多态方案,即使没有避免使用C1子类的直接动机,也能够获得灵活的进化点,用来应对将来未知的情况。

有时,开发者会针对某些未知的可能性变化进行“未来验证”的推测,由此而使用接口和多态来设计系统。如果这种变化点是基于立即或十分可能变化的原因而明确存在的,那么通过多态来增加灵活性一定是合理的。但是有些时候使用多态设计形成的变化点在实际中不一定发生或从未实际发生过,这种不必要的付出很常见。在投入灵活性的改进前,要现实地对待可变性的真实可能性。

多态的优点:易于增加新变化所需的扩展,无需影响客户便能够引入新的实现。

相关模式:防止变异大量流行的GoF设计模式,基于多态讨论其中的适配器(Adapter)、命令(Commond)、组合(Composite)、代理(Proxy)、状态(State)和策略(Strategy)模式。

选择消息,不要询问“什么类型”。

3.7 纯虚构(Pure Fabrication)

面向对象设计有时会被描述为:实现软件类,使其表示真实世界问题领域的概念,以降低表示差异;例如Sale和Customer类。然而,在很多情况下,只对领域层对象分配职责会导致不良内聚或耦合,或者会降低复用潜力。

image

这种类是凭空虚构的,理想状况下,分配给这种虚构物的职责要支持高内聚和低耦合,使这种虚构物清晰或纯粹——因此称为纯虚构。

例如,假设需要在关系数据库中保存Sale的实例。根据信息专家模式,存在一些理由可以将此职责分配给Sale类自身,因为Sale持有需要保存的数据。但是考虑如下含义:这一任务需要相对大量的支持面向数据库的操作,而与实际的销售概念无关,这样会使Sale类变得非内聚。Sale类必须与关系数据库接口(如Java技术中的JDBC)耦合,这样便增加了耦合。而且这种耦合甚至不是针对其他领域对象的,而是针对特定种类的数据库接口。在关系数据库中保存对象是十分普遍的任务,许多类都需要支持这一任务。将此职责置于Sale类中表明了难以复用,或是在其他类中存在大量完成此项工作的冗余。因此,即便就信息专家而言,Sale是将其保存于数据库中的合理候选者,但是这样会导致低内聚、高耦合和难以复用的设计——正是此类困境使得我们需要虚构某些事物。合理的方案是创建一个新类,使其独自负责在某种持久性存储介质(例如关系数据库)中保存对象;对该类命名为PersistentStorage。[2]该类是纯虚构的臆想之物。

image

PersistentStorage,虽然这个概念是可以被理解的,但是我们却无法在领域模型中找到这个名称或“持久性存储”的概念。并且如果开发者询问商店里的业务人员:“你使用持久性存储对象吗”,他们不会理解。他们能够理解诸如“销售”和“支付”的概念。而PersistentStorage并不是领域概念,它只是为了便于软件开发而凭空捏造或虚构的概念。

纯虚构解决了如下设计问题:

  1. 使Sale保持高内聚和低耦合的良好设计。

  2. PersistentStorage类本身是相对内聚的,具有唯一目标,即在持久性存储介质中存储或插入对象。

  3. PersistentStorage类是十分普遍和可复用的对象。在本例中创建纯虚构正是由环境所驱使的——消除了基于专家模式的不良设计,改善了内聚和耦合,增加了潜在的复用性。

与所有GRASP模式一样,纯虚构模式强调的是职责应该置于何处。在本例中,职责由Sale类(源于专家)转移给了纯虚构。

对象的设计可以被广泛地分为两组:

image

诸如Sale等软件类的创建是根据表示解析得来的;这种软件类项涉及或代表领域中的事物。表示解析是对象设计中的常见策略,并支持低表示差异的目标。但是有时,我们需要通过对行为分组或通过算法来分配职责,而无需创建任何名称或目的与现实世界领域概念相关的类。

换言之,纯虚构通常基于相关的功能性进行划分,因此这是一种以功能为中心的或行为的对象。大量现有的面向对象设计模式都是纯虚构的例子:适配器(Adapter)、策略(Strategy)、命令(Command)等。

纯虚构的优点:支持高内聚,因为职责被解析为细粒度的类,这种类只着重于极为特定的一组相关任务。增加了潜在的复用性,因为细粒度纯虚构类的职责可适用于其他应用。

有些对象设计初学者和更熟悉以功能组织和分解软件的人有时会滥用行为解析及纯虚构对象。夸张的是,功能正好变成了对象。创建“功能”或“算法”对象本来并没有错,但是这需要平衡于表示解析设计的能力(例如应用信息专家的能力),这样便能够使诸如Sale等表示类同样具有职责。信息专家所支持的目标是,将职责与这些职责所需信息结合起来赋予同一个对象,以实现对低耦合的支持。如果滥用纯虚构,会导致大量行为对象,其职责与执行职责所需的信息没有结合起来,这样会对耦合产生不良影响。其通常征兆是,对象内的大部分数据被传递给其他对象用以处理。

相关模式和原则:低耦合、高内聚、纯虚构通常会接纳本来是基于专家模式所分配给领域类的职责。所有GoF设计模式例如适配器(Adapter)、命令(Command)、策略(Strategy)等)都是纯虚构。事实上,所有其他设计模式也都是纯虚构。

3.8 间歇性(Indirection)

image

如果耦合不可避免,那我们可以更优雅的面对:

  1. 让耦合发生在非核心重要对象之间(一般领域对象低表示差异的对象是核心对象)

  2. 让耦合发生在一个相对稳定的接口或者协议,将职责分配个一个相对稳定的中介对象

TaxCalculatorAdapter这些对象对于外部税金计算器来说充当了中介的角色。通过多态,它们为内部对象提供了一致的接口,并且隐藏了外部API的变化。通过增加一层中间性和多态,适配器对象保护了内部设计,使其不受外部接口变化的影响

image

“计算机科学中的大多数问题都可以通过增加一层间接性来解决”,这一格言特别适用于面向对象设计。

如同大量现有的设计模式是纯虚构的特例一样,许多设计模式也同样是间接性的特例。适配器(Adapter)、外观(Facade)和观察者(Observer)就是这样的例。此外,许多纯虚构是因为间接性而产生的。间接性的动机通常是为了低耦合,即在其他构件或服务之间加入中介以进行解耦。

间歇性优点,实现了构件之间的低耦合。

相关模式和原则: 防止变异低耦合大量GoF模式,诸如适配器(Adapter)、桥(Bridge)、外观(Facade)、观察者(Observer)和中介(Mediator)[GHJV95]。大量间接性中介都是纯虚构。

3.9 防止变异 (Protected Variation)

image

注意:这里使用的“接口”指的是广泛意义上的访问视图,而不仅仅是诸如Java接口等字面含义。前述的外部税金计算器问题及其使用多态的解决方案能够描述防止变异。

image

其中的不稳定或变化之处是外部税金计算器所具有的不同接口或API。POS系统需要能与大量现有税金计算器系统进行集成,并且还能与现在还不存在但将来可能出现的第三方计算器进行集成。通过增加一层间接性,即接口,并且使用具有不同ITaxCalculatorAdapter实现的多态,这样便实现了对内部系统的保护而避免了外部API的变化所产生的影响。内部对象只与稳定的接口协作,各种适配器实现隐藏了外部系统的变化。

防止变异,这是非常重要和基本的软件设计原则,几乎所有的软件或架构设计技巧都是防止变异的特例,例如数据封装、多态、数据驱动设计、接口、虚拟机、配置文件、操作系统等。

就某种程度而言,从以下方面能够发现开发者或架构师的成熟度:不断地增长更多实现PV机制的知识、选择值得解决的适宜的PV问题、选择恰当的PV解决方案的能力等。早期,人们学习数据封装、接口和多态等核心机制用来实现PV。后来,人们学习的技术包括基于规则的语言、规则解释器、反射和元数据设计、虚拟机等,这些技术都能够用于防止某些变化。

防止变异的核心机制: 数据封装、接口、多态、间接性和标准都是源于PV的。注意:诸如虚拟机和操作系统等构件是实现PV的间接性的复杂例子。

xxa.png

4. 对象可见性设计

可见性(visibility)是对象“看到”或引用其他对象的能力。更广义地说,可见性与范围问题有关:某一资源(例如实例)是否在另一资源的范围之内?考虑可见性的动机是:为了使对象A能够向对象B发送消息,对于A而言,B必须是可见的。实现对象A到对象B的可见性通常有四种方式:

  • 属性可见性(Attribute Visibility)——B是A的属性

  • 参数可见性(Parameter visibility)——B是A中方法的参数

  • 局部可见性(Local visibility)——B是A中方法的局部对象(不是参数)

  • 全局可见性(Global visibility)——B具有某种方式的全局可见性

属性可见性, 当B作为A的属性时,则存在由A到B的属性可见性(attribute visibility)。这是一种相对持久的可见性,因为只要A和B存在,这种可见性就会保持。这也是面向对象系统中可见性的常见形式

image

参数可见性, 当B作为参数传递给A的方法时,存在由A到B的参数可见性(parameter visibility)。这种可见性是相对暂时的,因为它只在方法的范围内存在。

image

局部可见性, 当B被声明为A的方法内的局部对象时,存在由A到B的局部可见性(local visibility)。这种可见性是相对临时的,因为其仅存在于方法的范围之内。局部可见性是参数可见性之后的面向对象系统中第三种常见的可见性形式。实现局部可见性的两种常见方式是:创建新的局部实例并将其分配给局部变量。将方法调用返回的对象分配给局部变量。和参数可见性一样,将本地声明的可见性转换为属性可见性是很常见的。

image

全局可见性, 当B对于A是全局时,存在由A到B的全局可见性(global visibility)。这是一种相对持久的可见性,因为只要A和B的存在,这种可见性就会存在。这是在面向对象系统中最不常见的可见性形式

实现全局可见性的一种方式是将实例分配给全局变量,这在某些语言(如C++)中是可能的,但是有些语言(如Java)不予支持。实现全局可见性的首选方法是使用单实例类(Singleton)模式

在上述各个可见性中,需要仔细审视是否必要,尽量减少耦合提高内聚。

5. 更多对象设计

数据对象,缓存对象、document对象、其他的功能/辅助对象等都不在这里进行体系化展开,后续有机会再干。

五、OOP:选择语言,进行代码实现

至少,Design Class Diagram(DCD)描述了类或接口的名称、超类、操作的特征标记以及类的属性等。这已经足以在OO语言中创建基本类的定义了。如果DCD是使用UML工具绘制的,那么还可以从图形中生成基本的类定义。

在CASE(计算机辅助软件工程)工具的领域里,前向工程(forward engineering)是从图形生成代码,逆向工程(reverse engineering)是从代码生成图形,而双向工程(round-trip engineering)是以上两种工程的闭环——是支持双向生成的工具,并且支持UML图形和代码之间的同步,当任何一方发生变化时,这个工具都能够理想地进行自动和及时的同步。所有UML工具都声称支持这些特性,但是大部分UML工具只能实现其中部分特性。为什么?这是因为多数工具只能完成静态模型:它们可以从代码生成类图,但是无法生成交互图。或者对于前向工程来说,它们可以从类图中生成类的基本定义(例如Java),但是无法从交互图中生成方法体。

image

image

如果什么都能自动生成了,我们还有什么重要的意义和价值呢?

六、Grasp、Solid和GOF关系

大家可能对Grasp、Solid和GOF都听过,感觉每个人都说自己是基础原则或者模式,下面撸一下他们之间的关系,这是我自己的理解,不是官方权威认证,所以说软件是一门哲学,每个人理解都有差异。

GRASP 原则重点聚焦在单个对象职责的分配上,是面向对象的最基础和核心原则。下面这个图针对每个原则的核心思考绘制,没有严格的MECE分类关系,这也就是低耦合和高内聚被称为软件工程中的阴和阳的原因。

image.png

SOLID 在职责分配上更注重可操作性和实践性的指导,也关注类和类的协作和依赖管理。

image.png

GOF设计模式其实是一种经验的总结,是针对特定问题的简洁而优雅的解决方案,非常具有代码级的实操性,是对SOLID更上层的场景化解决方案。

image

华罗庚先生说,读书要从薄到厚,再从厚到薄~ 推荐两本不休的经典著作,这不就是大学时代学的吗?

image

image

image