牛骨文教育服务平台(让学习变的简单)

连载:面向对象葵花宝典:思想、技巧与实践(26) - 类模型三板斧

类模型设计其实就是程咬金打天下 -- 三板斧 而已 :)

第一斧(照猫画虎):领域类映射

面向对象类设计首先要解决的一个问题是:类从哪里来 ?

有的人可能会认为,要发挥想象力、创造力。。。。。等各种“力”——这种方法的主要问题是:我们不是在进行纯粹的艺术创造,而是要最终满足客户需求,而不能天马行空。

有的人可能会想到,参考其它的系统吧,把类似系统拿过来改吧改吧 ——这种方法的主要问题是:如果没有其它类似系统给你参考呢 ?
还有的人干脆就说:拍脑袋吧,凭感觉吧 —— 这种方法的主要问题是:猴子能敲出莎士比亚全集么 ?

看起来以上方法都不太可行,那究竟如何才能从哪里找到我们需要的类呢?

相信绝大部分认真看书的同学都会灵光一闪:领域模型。
我们将上一章中的领域模型图拿出来,重新再看一下:

相信不用我多说,绝大部分同学一眼就能看出:哇塞,这不就是类么?

确实是这样的,领域模型中的“领域类”,是设计模型中“软件类”最好的来源。通过“领域类”来启发我们设计最初的“软件类”,具有如下几个明显的优点
1)软件类来自领域类,领域类来自用例,用例来自客户,这样一环扣一环,软件类的正确性得到了保证,不用担心拍脑袋带来的问题;
2)领域类到软件类的转换非常简单,不需要天才的创新,或者丰富的想象力,只要掌握基本的面向对象的知识就能完成,菜鸟也能做设计;
3)不需要参考其它系统,不用担心没有参照物时无法设计的问题;

从领域类到软件类的转换操作非常简单,基本上就是一个照猫画虎的过程。
【类筛选】
虽然我们说从领域类到软件类是一个照猫画虎的过程,但并不意味着将领域类全盘拷贝过来即可。主要的原因在于“软件类”是软件系统内部的一个概念,而领域类是业务领域的概念,并不是每个领域类最终都会体现在软件系统中。

以POS机的领域类为例,领域类“顾客”不需要转换为软件类,因为顾客是POS机业务领域的一个重要参与者,但并不是POS机内部需要实现的一个实体,在POS机业务中,顾客甚至都不是和POS机直接交互的实体,站在POS软件系统的角度来说,顾客和POS机其实没有任何关系。

对于屏幕、键盘、扫描仪这些输入输出设备,一般情况下我们认为它们是POS机系统硬件的一部分,而并不是POS机软件系统的一部分。但假如POS机有一个需求是既支持图形界面输出,又支持字符界面输出,那么POS的软件系统就需要处理这种和屏幕相关的需求了,此时屏幕就是POS机软件系统的一部分了,需要将领域类转换为软件类。为了简单处理,接下来的分析中,输入输出设备不做转换。

经过筛选后,剩下的领域类就需要都转换为软件类,具体如下:
收银员、商品、交易、小票、支付、信用卡、会员卡、现金、购物卡。

【名称映射】
筛选完成后,我们开始讲领域类转换为软件类,转换的方法很简单,首先不管三七二十一,将每个领域类都用一个软件类与对应,名称都保持一样即可。

有的同学可能担心这样设计是否会不符合面向对象设计的要求,是否会导致设计质量不高。。。。。。等等,其实这种担心是多余的,因为我们后续还有很多工作要做,目前做的只是一个开始工作。

【属性映射】
通过名称映射的方法得到软件类后,接下来就是要设计类的属性了。由于领域类中也已经有了属性,因此我们也只需简单的照搬过来即可。

【提炼方法】
软件类的属性设计完成后,接下来就需要设计软件类的方法了。但这次我们就没有那么好的运气了,因为领域类中并没有方法!因此我们不能通过简单映射的方法来获取方法,必须采取其它手段。

和类的设计一样,类方法的设计同样不能采取“创造力、参考其它系统、拍脑袋”等方式来完成,为了确保正确性,类的方法设计也同样应该能够从已有的模型中推导出来。

由于已经明确领域模型中没有方法了,因此就不能从领域模型中得到软件类的方法,剩余只有一个“用例模型”了,因此我们锁定“用例模型”,看看如何从中找到我们所需要的方法。

其实方法也很简单,概括一下就是:找动词
你可能不敢相信自己的眼睛,这么简单,那几乎初中生都会做设计啊,找动词谁不会呢?

然而不管你信不信,这一步确实是这么简单,当然,如果面向对象设计只是到此为止,那确实初中生也是可以做的,但实际上这只是面向对象类设计的开始步骤而已,后面的工作还多着了,所以完全不用担心初中生来抢你的饭碗。

我们以POS机为例,来看看如何通过“找动词”这种技巧来找到软件类的方法。
如下是POS机的用例,我们将相关动词都加粗显示:

【用例名称】

买单

【场景】

Who:顾客、收银员

Where:商店的收银台

When:营业时间

【用例描述】

1. 顾客携带选择好的商品到收银台;

(这一步没有异常)

2. 收银员逐一扫描商品条形码,系统根据条形码查询商品信息;

2.1 扫描仪坏了,必须支持手工输入条形码;

2.2 商品的条形码无法扫描,必须支持手工输入条形码;

2.3 条形码能够扫描,但查询不到信息,需要收银员和顾客沟通,放弃购买此产品

3. 扫描完毕,系统显示商品总额,收银员告诉顾客商品总额;

(这一步没有异常)

4. 顾客将钱交给收银员;

4.1 顾客的钱不够,顾客和收银员沟通,删除某商品;

4.2 顾客的钱不够,顾客和收银员沟通,删除某类商品中的一个或几个(例如买了5包烟,去掉两包)

4.3 顾客觉得某个商品价格太高,要求删除某商品;

4-A:顾客使用信用卡支付

4-A.1 信用卡支付流程(请读者自行思考完善,可以写在这里,如果太多,也可以另外写一个子用例)

4-B:顾客使用购物卡支付

        4-B.1 购物卡支付流程

4-C:顾客使用会员卡积分支付

        4-C.1 会员卡积分支付流程

5. 收银员清点钱数,输入收到的款额,系统给出找零的数目;

(这一步没有异常)

6. 收银员将找零的钱还给顾客,并打印小票;

7. 买单完成,顾客携带商品和小票离开

【用例价值】

顾客买完单以后,就可以携带商品离开,而超市也将得到收入;

【约束和限制】

1. POS机必须符合国标XXX;

2. 键盘使用中文,因为收银员都是中国人;

3. 一次买单数额不能超过99999RMB;

4. POS机要非常稳定,至少一天内不要出现故障;

标识出所有的动词后,还需要进一步的工作:
【筛选】
并不是所有的动词都一定是软件类的方法,我们需要将这些动词识别出来并排除在后续设计范围之外。
例如:
“顾客携带选择好的商品到收银台”:这里的“携带”是顾客的一个动作,而顾客并不是我们的软件类;
“收银员告诉顾客商品总额”:这里的“告诉”确实是收银员的一个动作,而且“收银员”确实也是我们的软件类,但这里也要排除“告诉”,因为“告诉”这个动作和POS系统并没有关系,只是业务流程中的一个步骤而已。
其它需要排除的动词还有:“需要收银员和顾客沟通”、“顾客将钱交给收银员”、“收银员清点钱数”、“收银员将找零的钱还给顾客”、“顾客携带商品和小票离开”
【提炼】
筛选完不需要的动词后,剩下的就是我们需要的动词了,但此时并不能简单的将所有动词拿出来直接扔给某个软件类就行了,我们还需要进行一些加工。

继续以POS机为例:
“收银员逐一扫描商品条形码”:这里的“扫描”看起来是“收银员”的一个动作,而且“收银员”确实也是我们的软件类,但其实深究一下,“扫描”这个动词并不能分配给“收银员”这个软件类,因为真正执行“扫描”功能的是“扫描仪”,收银员只是拿着扫描仪扫描商品,并不是收银员自己去读取商品条形码;类似的动词还有“必须支持手工输入条形码”,也不能算作“收银员”的功能。

那我们为什么不排除这两个动词呢?秘密就在于我们要从这两个动词提炼出软件类的方法。稍作分析,我们就可以发现,无论是“扫描条形码”,还是“手工输入条形码”,其实最终的目的都是“添加本次交易的商品”,因此我们可以提炼出“增加交易商品”的动词。

还有一种提炼的方法需要从已有的动词中推断出来,例如:“扫描完毕,系统显示商品总额”,这里只提到了“显示”这个动词,但相信大部分人都能一眼看出,“显示”之前肯定要“计算”,不然显示出来的值从哪里来呢?

有的朋友可能会疑惑,为什么不在用例的时候就写清楚呢?例如:扫描完毕,系统计算商品总额,然后系统显示商品总额。这样不就一目了然的看出来了么?

理想情况下这种想法当然没错,但现实往往没有那么美好,写用例的产品人员可能经验不足,也可能表达能力有限,还有可能比较马虎,或者遗漏了。。。。。。总之会有很多异常情况,因此设计人员必须具备这样的推断和判断能力。

经过这一步骤后,我们获得的动词如下:
 增加商品
 计算商品总额
 显示商品总额
 删除商品
 现金支付
 信用卡支付
 购物卡支付
 会员卡积分支付
* 打印小票

当然,以上列出来的动词并不是就一定是100%的标准答案,不同的人来进行分析和设计,可能略有不同,但总体应该比较相似,毕竟业务是一样的,而业务需求就是设计最强的约束。

【分配】
识别出有效的动词后,最有一步就是分配了,即:将从用例中提炼出来的动词,分配给已经有了属性的软件类。这种分配操作很多时候都是按图索骥,特别是对于有领域经验的人来说,基本上凭直觉就能基本分配正确。

当然,如果你的经验并不是很丰富,那么还是老老实实的一个一个来分析吧。

以POS机为例:

  • 增加商品:很明显应该分配给“交易”类
  • 计算商品总额:分配给“交易”类
     显示商品总额:分配给“交易”类
     删除商品:分配给“交易”类
     现金支付:分配给“现金”类
     信用卡支付:分配给“信用卡”类
     购物卡支付:分配给“购物卡”类
     会员卡积分支付:分配给“会员卡”类
    * 打印小票:这个动词的分配存在一定的灵活性,有的人可能认为应该分配给“交易”类,因为打印小票可以认为是“交易”流程中的一个步骤;有的人可能认为应该分配给“小票”类,因为打印小票可以认为是“小票”类的一个基本功能。其实两者都有一定道理,如果没有其它更有力的选择因素,我建议根据个人经验选择一个即可,这里我们选择分配给“小票”。
    分配完成后,我们可以看到“交易”、“小票”、“信用卡”、“购物卡”、“会员卡”、“现金”都已经有方法了。

当然,对于有经验的人来说,以上步骤完全可以在脑海中就迅速完成了,而并不会这样一步一步的演示给别人看,所以看起来就像变戏法一样,不知怎么就设计出来了很多的软件类。

经过上面的处理步骤后,我们得到如下的类图:

与领域模型相比,部分领域类被剔除了,留下来的领域类映射成软件类后,又增加了方法。虽然还不完善,但软件类的是越来越有型,越来越清晰了。

第二斧(精雕细琢):应用设计原则和设计模式
完成了从领域类到软件类的映射后,类出来了,属性也出来了,方法也有了,看起来设计已经大功告成了。
事实上也确实有很多人基本上做到这一步就开始动手编码了,而且经过一番拼搏,最后发布的系统也能用。

但相信很多人都会有这个疑问:这样做就够了么,这样设计是否是好的设计呢?
要回答这个问题,我们首先要明确:什么叫做“好”的设计呢 ?

到目前为止,我们已经有了一个类的设计模型,而且如果按照这个模型去实现的话,最终应该也是能够满足用户的需求,毕竟我们这个类模型是按照“需求模型 -> 领域模型 -> 类模型”这样一路推导过来的,不会出现大的偏差。

那么,满足了用户需求的设计就是好的设计么?
相信有经验的朋友都会知道答案:“满足用户需求”只是设计的一个最基本要求,而不是一个“好设计”的评判标准。

既然如此,那么到底什么才是好的设计呢,是否有明确的标准来进行评价呢?
幸运的是,面向对象领域经过几十年的发展,确实已经发展出了很多成熟的指导思想和方法,用于评价和指导如何才能做好面向对象的设计。其中最具代表性的就是“设计原则”和“设计模式”

【设计原则】
当我们谈到面向对象领域的设计原则的时候,我们其实都是在谈论罗伯特.C.马丁(Robert C. Martin ,又叫Bob大叔)的SOLID原则。

这也难怪,Bod大叔实在是太牛了,面向对象领域的设计原则几乎被他全部包揽了,加上他在他的畅销书《敏捷软件开发:原则、模式与实践》中详细的将这些原则集中一 一阐述,面向对象领域设计原则的权威非他莫属。毫不夸张的说,Bob大叔的威名和在面向对象领域中的地位,和设计模式的“四人帮”是不相上下的。

虽然很多资料都将SOLID原则和敏捷开发、测试驱动开发等方法绑定在一起,但我觉得只要是面向对象设计,不管是瀑布流程、、CMM流程、RUP流程、还是敏捷开发流程,都应该应用设计原则以提高设计质量。

参考wiki百科,SOLID设计原则简单介绍如下:
SOLID实际上是取5个设计原则的首字母拼起来的一个助记单词。具体的设计原则如下(详细的设计原则,我们会在后面详细阐述,这里不再详细展开):

首字母英文简写英文名称中文名称说明
SSRPSingle Responsibility Principle单一职责原则对象应该只具备单一职责
OOCPOpen/Close Principle开放/封闭原则认为“软件体应该是对于扩展开放的,但是对于修改封闭的”的概念。
LLSPLiskov Substitution PrincipleLiskov替换原则认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念
IISPInterface Segregation Principle接口隔离原则多个特定客户端接口要好于一个宽泛用途的接口
DDIPDependency Inversion Principle依赖反转原则依赖于抽象而不是一个实例

(wiki百科链接如下:
http://zh.wikipedia.org/wiki/SOLID_(面向对象设计))

前面我们简单的八卦了一下,现在回归正题:设计原则有什么用?
其实和所有的原则一样,设计原则也是一个判断标准,说通俗点,设计原则就像是木匠手中的尺子,尺子是用来衡量木材的长短的,而设计原则就是衡量类设计的“尺子”:量一量,看长了还是短了,还是正好,长了就裁短一些,短了就加长一些。经过如此衡量并调整,最终就能够得到我们希望的设计作品。

当然,和木匠的尺子稍有不同,木匠不用尺子就做不出能用的家具,但我们不用设计原则的话,其实还是能够做出满足需求的系统的。

既然这样,我们为什么一定要用设计原则呢?ARTHUR J.RIEL在《OOD启思录》一书中针对这个问题给出了非常形象的解释:
你不必严格遵守这些原则,违背它也不会被处以宗教刑罚。但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。”-------ARTHUR J.RIEL,《OOD启思录》

也就是说,如果违背了这些设计原则,就可能有危险,但究竟是什么危险呢,警铃要警告我们什么呢,是火灾、水灾、地震、陷阱、还是有狮子、老虎。。。。。?

要回答这个问题,还需要回到面向对象的本源:我们在第一章解释为什么要面向对象的时候提到了面向对象的核心思想是“可扩展性”,这其实就是我们应用设计原则的根本目的:保证可扩展性。如果我们不遵守这些设计原则,警铃就会响起,提醒我们:你的设计可扩展性会有问题!

除了设计原则外,后面要讲到的设计模式,其本质也是为了提高可扩展性。这也是为什么我们通过领域类映射得到了很多软件类之后,还需要不辞辛劳的继续应用设计原则和设计模式的主要原因,本质上都是为了提高设计的可扩展性。

SOLID设计原则的各个子原则详细介绍会在后面详细介绍,这里我们简单的以POS机为例,看看如何应用设计原则。

仔细观察我们通过领域类映射得到的软件类,可以发现一个很明显不符合SOLID原则中的DIP原则的地方,即:“交易类”直接依赖“会员卡”、“购物卡”、“信用卡”、“现金”4个子类,这样的实现不符合DIP原则,当需要增加新的支付方式时,“交易类”也需要跟着修改。

既然不满足DIP设计原则,那么我们就按照DIP原则的要求,提取出一个支付的父类来,即:“交易类”依赖“支付类”,“会员卡”、“购物卡”、“信用卡”、“现金”都继承“支付”类。具体实现如下:

可以看到,应用DIP设计原则之后,我们又多出了一个“支付”的类,这个类原来在领域模型中是没有的,而是我们在设计阶段“创造”出来的。

对于其它各个类,我们都可以依次使用设计原则进行判断,当发现不符合设计原则的设计时,就采取增加、删除、合并、拆分等手段,使我们的设计逐步改进,最终达到符合设计原则的目的。

【设计模式】
相比设计原则来说,设计模式更加普及和流行,当我们谈到设计方法的时候,大部分人肯定都会想到设计模式,设计模式如此深入人心,几乎到了言必谈设计模式的地步。

和设计原则类似,当我们谈论设计模式的时候,我们其实都是在谈论GOF(Gang of Four,中文翻译为“四人帮”)在经典名作《设计模式  --可复用面向对象软件的基础》一书中提到的设计模式。

通俗的讲,设计模式是用于指导我们如何做出更好的设计方案,而前面提到的设计原则,其作用也是这样的。那么,设计原则和设计模式,我们该如何选择?

有的朋友可能会以为这两个是二选一的关系,要么用设计原则,要么用设计模式。这种理解是错误的,设计原则和设计模式并不是竞争关系,正好相反,它们是互补的关系。

设计原则和设计模式互补体现在:设计原则主要用于指导“类的定义”的设计,而设计模式主要用于指导“类的行为”的设计,更通俗一点的讲:设计原则是类的静态设计原则,设计模式是类的动态设计原则。
一般情况下,我们是采用“先设计原则,后设计模式”的方法来操作的。

设计模式的相关内容会在后文详细介绍,这里我们以POS机为例,看看如何应用设计模式来优化我们的设计。

通过分析应用设计原则优化后的类,我们发现“信用卡”这个类存在优化的空间,因为国际上存在不同的信用卡,最常见的有中国银联(UnionPay)、Visa、MasterCard这几种,每种信用卡在支付的时候需要接入不同的机构,其接入方式和协议肯定都是有一定差异的。为了封装这种差异以支持后续更好的扩展,我们应用设计模式的Bridge模式,提取出“信用卡处理”这个类,这个类的主要处理“连接、认证、扣款”这样的职责。UnionPay、Visa、MasterCard都继承“信用卡处理”这个类。具体如下:

第三斧(照本宣科):拆分辅助类
经过前面的设计步骤之后,面向对象类的设计工作已经完成,我们输出了完整的类模型,看起来已经可以开始动手编码了,你是否舒了一口气,看着自己的设计作品,不由得产生了一种自豪感呢?

确实值得自豪,毕竟我们一步一个脚印,从最初仅仅存在于客户脑袋中的需求,逐步的推导、演变、设计出了能够付诸实施的类模型了。但在最终实施之前,还有一点小小的动作要完成,这就是我们的拆分辅助类操作。

拆分辅助类的主要目的是为了使我们的类在编码的时候能够满足一些框架或者规范的要求。比如说常见的MVC模式,将一个业务拆分成Control、Model、View三个元素;J2EE模式中,将对象分为PO、BO、VO、DTO等众多对象。

之所以说这是一点小小的动作,是因为这个动作确实很简单,只要将我们设计出来的类,按照规范要求,一 一对应分拆即可。

以POS机为例,假如我们的框架要求提供DAO对象,负责数据库的相关操作,则“购物卡”类就应该拆分为两个:“购物卡”、“购物卡DAO”,其中“购物卡”用于负责提供“支付”功能给“交易”类调用,“购物卡DAO”用于负责从数据库读取购物卡信息,修改数据库中购物卡余额等操作。

需要注意的是,拆分设计辅助类仅仅是为了满足框架或者规范的要求,本身并不是一个设计的步骤,而是实施的一个步骤,所以我们一般都不需要将拆分的辅助类体现在类模型中,仅仅在编码的时候拆分即可。