敏捷软件开发:原则、模式与实践——第20章 咖啡的启示

第20章 咖啡的启示
  这个例子对于教学有很多好处。它短小、易于理解并且展示了如何应用面向对象设计原则去管理依赖和分类关注点。但从另一方面来说,它的短小也意味着这种分离带来的好处可能抵不过其成本。就当做一个设计思路来看吧。

20.1 Mark IV型专用咖啡机
20.1.1 规格说明书

  Mark IV型专用咖啡机一次可以产出12杯咖啡。使用者把过滤器放置在支架上,在其中装入研磨好的咖啡,然后把支架推入其容器中。接着,使用者向滤水器中倒入12杯水并按下冲煮(Brew)按钮。水一直加热到沸腾。不断产生的水蒸气压力使水洒在咖啡粉末上,形成水滴通过过滤器流入到咖啡壶中。咖啡壶由一个保温盘进行长期保温,仅当壶中有咖啡时,保温盘才进行工作。如果在水还在向咖啡粉喷洒时从保温盘上拿走咖啡壶,水流就会停止,这样煮好的咖啡就不会溅在保温盘上。以下是需要监控的硬件设备。

  • 加热器的加热元件。可以开启和关闭。
  • 加热盘的加热元件。可以开启和关闭。
  • 保温盘传感器。它有3个状态:warmerEmpty、potEmpty和potNotEmpty。
  • 加热器传感器,用来判断是否有水。它有两个状态:boilerEmpty、boilerNotEmpty。
  • 冲煮按钮。这个瞬时按钮启动冲煮流程。它有一个指示灯,当冲煮流程结束时亮,表示咖啡已经煮好。
  • 减压阀门,在开启时可以降低加热器中的压力。压力降低会阻止水流向过滤器。该阀门可以开启和关闭。

Mark IV型专用咖啡机的硬件已经设计完成,硬件工程师为我们提供了低层的API,如下:

 

  我们在为一个简单的嵌入式实时系统设计软件。我期望可以给出一组类图、顺序图和状态图。

20.1.2 常见的丑陋方案

  下图展示了最常见的丑陋方案:

这种关联是初学者常犯的一个错误。该关联是基于问题中的一些物理关联而非软件控制行为作出的。咖啡从HotWaterSource流入ContainmentVessel和这两个类之间的关联完全无关。

  如果通向器皿的热水流的开始和停止是有ContainmentVessel通知HotWaterSource进行的,会怎样呢下展示,注意,ContainmentVessel给HotWaterSource发送了start消息。这意味着关联关系是反方向的。ContainmentVessel依赖于HotWaterSource。

当HotWaterSource和ContainmentVessel都准备好,UserInterface对象就应该向HotWaterSource发送start消息,HotWaterSource就开始工作。

  用例2:接收器皿没有准备好

  当接收器皿没有准备好,ContainmentVessel通知HotWaterSource停止传送热水,当准备好后,再通知HotWaterSource再次开启热水流,热水流的终止和恢复如下:

HotWaterSource和ContainmentVessel都可以发送Done消息。

  用例4:咖啡喝完了

  当冲煮结束并且一个空咖啡壶被放在保温盘上时,Mark IV 就会关掉指示灯。

20.1.5 实现抽象模型

  我们创建的3个类都不能知道关于Mark IV的任何消息。这就是依赖倒置原则(DIP)。我们不允许系统中高层的咖啡制作策略依赖于低层的实现。

  使用者按下冲煮按钮

  UserInterface如何知道冲煮按钮被按下了呢必须要调用CoffeeMakerAPI.GeeBrewButtonStatus()函数。我们决定UserInterface类是不能知道CoffeeMakerAPI的。根据DIP,这个调用放在UserInterface的派生类中。

  为什么要创建一个受保护的StartBrewing()方法呢护不再M4UserInterface中直接调用Start()函数呢因很简单,但是很重要。IsReady()测试以及随后对HotWaterSource和ContainmentVessel的start()方法的调用都是高层的策略,都应归属于UserInterface类。

  实现IsReady()方法

  实现Start()方法

  HotWaterSource的Start()方法只是一个抽象方法,M4HotWaterSource会实现该方法调用CoffeeMakerAPI中关闭阀门以及开启加热器的函数。在编写这些函数过程中,我开始不停地写一些类似CoffeeMaker.api.XXX这样的结构感到厌烦,因此我就同时也做了一些重构。

 

  调用M4UserInterface.CheckButton

  系统的控制流是如何运转调用CoffeeMakerAPI.GetBrewButtonStatus()函数的呢择线程还是轮询个决策可以在最后一刻作出。这对设计没有任何影响。最好总是假设消息都是可以异步发送的,就好像存在有独立的线程一样。

  假设我们用轮询的方式:

 

 

  完成咖啡机练习

 

20.1.6 这个设计的好处

  线条把3个抽象类圈了起来。圈中的类没有依赖于任何圈外的类。因此,抽象完全和细节隔离开了。

敏捷软件开发:原则、模式与实践——第20章 咖啡的启示

 

 

20.2 面向对象过度设计
  这个例子对于教学有很多好处。它短小、易于理解并且展示了如何应用面向对象设计原则去管理依赖和分类关注点。但从另一方面来说,它的短小也意味着这种分离带来的好处可能抵不过其成本。

  如果把Mark IV 咖啡机实现为一个有限状态机,我们会发现它有7个状态和18个迁移。我们可以用18行的SMC(the State Machine Compiler,状态机编译器)代码来表示状态机。轮询传感器的简单主循环也就十几行代码,有限状态机要调用动作函数也在几十行代码左右。简而言之,我们可以在一页代码之内实现整个程序。

  如果不算上测试代码,咖啡机的面向对象实现有5页代码。我们无法对这种悬殊做出合理的解释。在大型应用中,依赖管理和关注点分离带来的好处会明显超出面向对象设计的成本。但是在这个例子中,我们可能得出相反的结论。

 

 

 

摘自:《敏捷软件开发:原则、模式与实践(C#版)》Robert C.Martin    Micah Martin 著

相关资源:淘金币抵钱怎么用|淘金币自动领取工具v1.3绿色版.zip_淘金币自动…

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2015年7月25日
下一篇 2015年7月25日

相关推荐