More Related Content Similar to 软件设计原则、模式与应用 (20) 软件设计原则、模式与应用2. 一 、引言
1、在敏捷开发中,所涉及的原则分为:
(1)开发原则
共12条。这些原则是从“实践”的角度给出的。
用于项目的组织
(2)设计原则
共11条。这些原则是从设计的角度给出的原则。
用于开发人员的行为指导。
其中• 为了解决所谓的软件“腐化”问题,提出了5条。
• 为包的设计(集成技术),给出了6条。
3. 2、原则提出的基本思想源
软件之美,在于:
1)、它的功能
对于用户来说,通过直观、简单的界面呈现出恰当特征的
程序就是美的;
2)、它的内部结构
对于软件设计者来说,通过简单、直观的划分,使其具有
最小耦合的内部结构就是美的;
3)、团队创造它的过程
对于开发人员和管理者来说,每周都会取得重大进展,并
生产出无缺陷代码的具有活力的团队过程就是美的;
5. 3、敏捷开发(主要包括敏捷实践和敏捷设计)
敏捷联盟的宣言,具体表达了敏捷开发的基本思想。
敏捷联盟宣言
我们正在通过亲身实践,并通过帮助他人的实践,揭示更好的软件
开发方法。这些工作使我们认识到:
• 个体和交互 胜过 过程和工具
• 可以工作的软件 胜过 面面俱到的文档
• 客户合作 胜过 合同谈判
• 响应变化 胜过 遵循计划
虽然右边各项也有价值,但左边的各项具有更大的价值。
Kent Beck James Grenning Robert Martin
Mike Beedle Jim Highsmith Steve Mellor
Arie van Bennekun Andrew Hunt Ken Schwaber
Alistair Cockburn Ron Jeffries Jeff Sutherland
Ward Cunningham Jon Kern Dave Thomas
Martin Fowler Brian Marick
7. 2) 敏捷开发思想的来源:
“ 满足工程设计标准的唯一文档是源代码清单”。
-Jack Reeves 1992 ,“C++ Journal”
在这篇文章中, Jack Reeves对C++的流行,思考了一个问
题,即什么是真正软件设计。按着他的观点:
(1)“ 满足工程设计标准的唯一文档是源代码清单”。
他通过仔细审视软件开发的生命周期,编译器和连接器
是实现软件构建的基本工具。其廉价程度是令人难以置信
的。并且,随着计算机速度的加快,软件构建将变得更加
廉价。 —这是软件业和传统制造业之间的一个重要区别!
(设计团队—设计文档;制造团队——构建产品。
设计与制造独立;人员技能不同。)
8. (2)“设计实际软件的昂贵程度 是令人难以置信的, 之所以
如此,是因为软件是非常复杂的,并且软件项目的几乎
所有步骤 都是设计的一部分”。
A、 编程是一种设计活动,并且在编码显得有意义时,就
应立即进行编码。
其中:
可以使用一些支持高层设计的结构图表、类图、状态图
和PDL等工具和表示法,提供必要的帮助,但它们都不
是真正意义上的软件设计(不符合严格的工程设计标准),
因此必须创建真正的软件设计,并使用某种编程语言来
完成 之。
14. (3)粘固性(Immobility):是指设计中包含了对其它系统有
用的部分,但要想把这些部分分离出来所需要的努力
和风险是巨大的,即设计难于复用。
(4)粘滞性(Viscosity):具有以下2种表现形式:
软件粘滞性:是指当面临一个改动时,那些可以保持系
统设计的方法比那些破坏设计的“生硬”方法更难应用,
即难于做正确的事情。
环境粘滞性是指环境的迟钝和低效。例如编译时间长。-
也是难于做正确的事情。
15. (5)不必要的复杂性(Needless Complexity):是指设计中包
含了当前没有用的成分,即过分设计。
危害:这种设计,一方面使软件变得复杂,另一方面使软件
难于理解。
(6)不必要的重复( Needless Repetition):是指滥用“剪切”
和 “粘贴”鼠标操作。 “剪切”和“粘贴”操作也许是有用的
文本编辑操作,但却是灾难性的代码编辑操作:
往往是开发人员忽略了抽象。从而使系统不易理解;
软件中的重复代码,使系统的改动变得更加困难,
不易系统的维护。
18. 4)、防止软件腐化的基本途径
—依赖变化来获取活力
(1)团队几乎不进行预先(up-front)设计,因此不需要一
个成熟的初始设计;-“自低向上”设计
(2)团队使用许多单元测试和验收测试,支持系统的设计
尽可能的干净、简单,使设计保持灵活性和易于理解性;
-测试驱动的设计
(3)灵活、持续地改进设计,以便使每次迭代结束时所生成
的系统具有满足那次迭代需求的设计。
-不断地实施重构
20. (一)单一职责原则(SRP)
只有佛自己应当承担起公布玄妙秘密的职责。
-E.Cobham Brewer,1810-1897
英语典故字典,1898
(1)、内容:
就一个类而言,应该仅有一个引起 它变化的原因。
这条原则被称为内聚性原则。
一般地,内聚性是指一个模块组成元素之间的功能相关性。
在本条原则中,把内聚性和引起一个模块或类的改变的
作用联系起来。
21. 为了理解这一原则,首先要回答什么是职责?
在SRP中,把职责定义为:引起变化的理由
(a reason of change)。
根据这一定义,显然可知:如果能够想到多个(>1)动机
来改变一个类,那么这个类就具有多于一个的职责。
实践中,人们很习惯以“组”的形式来考虑类的职责。例如:
一个接口“调制解调器”有四个功能:
interface modem
{ public void dial (string pno);
public void hangup ( );
public void send (char c);
public void recv ( );
}
22. 根据职责的定义,可以认为该接口有两个职责:
连接处理(public void dial (string pno);
public void hangup ( );)
数据通信(public void send (char c);
public void recv ( );)
这一接口“调制解调器”显然违背了SRP原则!
(2)问题:是否将这2个职责分离?
这取决于应用程序变化的方式:
如果应用程序的变化会影响连接函数的声明(signature),
那么调用send和recv就必须重新编译,因此应分离这两种职责。
如果应用程序的变化总是导致这两个职责同时变化,那么
就不必分离这两种职责。
23. (3)、如何实现 耦合职责的分离?
就上一个例子而言,可以把2个职责分离为:
《interface》 《interface》
Data Channal Connection
+send(:char) +dial(pno:String)
+recv( ):char +hangup( )
Modem
Implementation
其中,可以把ModemImpiementation类看作是一个杂凑物
(kludge),通过分离它们的接口,解除耦合的概念(“连接”
“通信”)。这样,使所有对该接口的依赖,都与
ModemImpiementation类无关。除了main外,谁也不知道它
的存在。
24. (4)、一种常见的违反该原则的情况—持久化问题
如左图所示:
其中,类Employee包含了业务 Employee
Persistence
规则和持久性控制。这2个职责 +CalculatePay
Subsystem +Store
在多数情况下不应该合在一起。
因为业务规则往往会不断的变化,
并且变化的原因也各不相同。
(5)如何处理职责耦合:
第一,测试驱动的开发实践,往往可以避免这种情况发生,
即这一实践会迫使对类的多种职责进行分离。-主动式
第二,如果出现了这种情况,可以使用FAÇADE和 PROXY
模式对设计进行重构,分离多种职责。 -被动式
27. 2) 测试先于设计的示例
一个简单的游戏程序,名为-“Hunt the Wumpus”。
该游戏的内容为:
玩家(WumpusGame)在洞穴中移动,在没有被Wumpus吃掉
前,设法杀死它。
其中:1)该洞穴由一系列通过过道相连的房间组成;
2)每个房间都有向东、向西、向南向北的通道;
3)玩家通过告诉计算机要行进的方向而四处移动,以便
实现其自己的目标-设法杀死Wumpus。
28. 在编写WumpusGame之前,首先编写了一个测试程序testMove :
public void testMove()
{
WumpusGame g=new WumpusGame() ;
g.connect(4,5,”E”);
g.setPlayerRoom(4);
g.east();
assertEquals(5, g.getPlayerRoom( ));
}
注:在编写WumpusGame之前,先在其测试中陈述你的意图。这种方法一般
称为“有意图的编程(intentional programming)”。可以简单、清晰地给程
序指出一个好的结构。
29. 通过这一测试可以看出:
(1)没有引入“Room”类!仅使用整数表示房间。
作者认为:就游戏而言,连接(从一处到另一处)概念
比“房间”概念更重要。这就是一个意图。
这样,在早期阶段就阐明了一个设计决策。
(2)通过该测试,可以使编程人员了解该游戏程序是如何工
作的。
(3)根据这一简单的规格说明,
可以很容易地实现WumpusGame中的已经命名的方法,
可以很容易命名其它3个方向并实现之。
30. 3)测试可以促使模块之间的分离
例如,一个薪水支付应用,在没有编码前,简单、快速地划了一个UML
图:
CheckWriter Employee
Payroll
+writerCheck( ) +caculatePay( )
+postPayment( )
Employee
Database
+getEmployee( )
其中, +putEmployee( )
类Payroll使用EmployeeDatabase获得Employee对象,并要求
Employee来计算自己的薪水。
接着,它把计算结果传给CheckWriter对象,产生一张支票。
最后,它在Employee对象中记录支付信息,并把Employee
对象写回到数据库中。
31. 现在,要编写 规定Payroll对象行为的测试。其中需要
考虑的问题有:
使用什么样的数据库?
Payroll对象需要从哪些数据库中读取数据?
在测试前,需要把什么数据加载到数据库中?
如何检验打印出来的支票的正确性?
……
对于以上这些问题,可以使用MOCK OBJECT模式解决之。即
在Payroll类和它的所有协作者之间插入接口,创建实现这些
接口的测试桩。如下图所示:
32. Mock Mock
CheckWriter PayrollTest Employee
《interface》 《interface》
CheckWriter Payroll Employee
+writerCheck( ) 《interface》 +caculatePay( )
+postPayment( )
Employee
Database
+getEmployee( )
+putEmployee( )
Mock
Employee
Database
33. 从上图可以看出,
现在Payroll类使用接口和CheckWriter、 Employee 、
EmployeeDatabase 进行通讯,创建了实现这些接口的Mock
Objects。
PayrollTest对象对这些Mock Objects进行查询,以检验
Payroll对象是否正确地对它们进行了管理。
34. 这一测试的设计思想是:
创建适当的Mock Objects,并把它们传递给Payroll对象,
告诉Payroll对象为所有雇员支付薪水,接着要求Mock Objects
去检查所有已开支票的正确性以及所有已支付信息的正确性。
显然,这一测试所检查的都是Payroll应该使用正确的数据调
用正确的函数。它既没有真正地去检查支票的打印,也没有真
正地去检查真实数据库的正确刷新。
相反,它检查了Payroll类应该具有与它独立情况下同样的行
为。其中,不引入MockEmployee类,而是直接使用Employee类,
这是可以的,但使 Employee类显得复复杂了一些。
35. 程序:
Public void testpayroll()
{
MockEmployeeDatabase db=new MockEmployeeDatabase();
MockCheckWriter w=new MockCheckWriter();
Payroll p= new Payroll(db,w);
p.payEmployees();
assert(w.checksWereWritenCorrectly();
assert(db.paymentWerePostedCorrectly();
}
38. 结论
(1)单元测试和验收测试都是一种文档形式-是可编译的、
可 执行的;因此它是准确和可靠的。
(2)编写测试的语言是明确的,即程序员能够阅读单元测试,
客户可以阅读验收测试。
(3)测试套件越简单,就会频繁地运行之。测试运行得越多,
就会越快地发现那些与测试不符的问题。
(4)测试最重要的好处是对体系结构的影响。因为对于一个
模 块或一个应用而言,越是具有可测性,其耦合关系就
越弱。
40. (4)该模式所具有的结构:例如
DB
ProductData +store(ProductData) Application
+getProductData(sku)
+deleteProductData (sku)
Java.sql
Connection Statememt Driver
Manager
Prepared
ResultSet Statememt SQLException
41. 其中,
DB是一个FAÇADE类(呈表类),特定于 productData,
提供了一个非常简单 的 接口。Application不必了解Java.sql
的细节,把Java.sql包所有的复杂性隐藏在一个非常简单的
接口中.
对Java.sql包的使用,DB类向该包施加了许多策略.例如
如 何初始化和关闭数据库连接,如何将ProductData的成员
变量转换为数据库字段,如何构造合适的查询和命令来
操纵数据库.
使用FAÇADE模式,意味着开发人员接受了有关DB的
约定,即用户(Application)的任意部分的代码都必须通过
这样的DB来访问Java.sql包-受限的方式,不能越过
FAÇADE类而直接访问Java.sql。
注意:基于这一约定,使DB成为Java.sql包的唯一代理
(broker).
43. 3)PROXY模式结构及工作原理
如下所示: (以购物车系统为例)
《interface 1
Proxy模式的静态结构 》 Product
3 2
Product 《delegates Product
DB
DB Proxy 》 Implementation
每个要被代理的对象(“产品”)被分为3个部分:
第一部分是一个接口,它声明了客户端要调用的所有方法;
第二部分是一个类,该类在不涉及数据库逻辑的情况下实
现了该接口中的方法;
第三部分是一个知晓数据库的代理。
45. 工作过程:
客户向一个它认为是product、但实际上是productDBProxy
的对象发送getPrice消息。
ProductDBProxy 从数据库中创建ProductImplementation,
然后把getPrice方法委托给它。
(2)Proxy模式的优点:客户和ProductImplementation都不知
道所发生的事情。数据库在这两者都不知道的情况下被插
入到应用程序中。因此,该模式的最大好处是,可以实现
一些重要关系的分离(separation of concerns)。
就这一例子而言,实现了业务规则和数据库的分离。这是
当前最流行的一种保持业务规则和实现机制分离的方法。
47. (二)开放-封闭原则(The Open-Closed Principle,OCP)
1)、内容:软件实体(类、模块、函数等)应该是可扩展
的,但是不可修改的。
(1)“对于扩展是开放的”(open for extention)
这意味着模块的行为是可以扩展的。换言之,可以改变模
块的功能。
(2)“对于改变是封闭的”(closed for modification)
这意味着对模块行为改变时,不必改动模块的源代码或
二进制代码。模块的二进制可执行版本,无论是可链接的
库、DLL或Java的.jar文件,都无需改动。
48. 2)、实现这一原则的基本思想、机制和结构
(1)基本思想: 关键的是抽象!
(2)机制:继承
不允许修改的模块,通常被认为是具有“固定”行为的模
块。抽象基类以及可能的派生类,就能够描述一组任意
可能行为的抽象体。
应用模块可以操作一个抽象体。由于该模块依赖一个“固
定”的抽象体,因此
对于更改来说,可以认为该抽象体是封闭的;
但通过该抽象体的派生,可以扩展该模块的行为,
这样可以认为该抽象体是开放的。
49. ( 3 ) 实现该原则(OCP)的结构
例如: Client Server
由于, Client 类和Server类都是具体类, Client类是既不开放的
又不封闭的类
通过委托方式,实现开放-封闭原则(OCP)的结构:
《interface》
Client Client Interface 该接口体现了抽象!
Server
STRATEGY模式:既开放又封闭的Client
50. 其中,
ClientInterface类是一个拥有抽象成员的抽象类。 Client
类使用这个抽象类,但Client 类的对象却使用Server类的派
生类的对象。
(1)封闭性的体现:如果希望Client 对象使用一个不同的
服务器类,那么只需从ClientInterface类派生一个新的类,
而无需对Client 类进行任何改动。
(2)开放性的体现:如果Client 类需要实现一些功能,可
以使用ClientInterface的抽象接口来描述这些功能。
ClientInterface的子类型可以以任何方式来实现这个接口。
这样,就可以通过创建ClientInterface的新的子类型的方式
来扩展、更改Client 中指定的行为。
51. 附3:
STRATEGY模式
1) 所要解决的问题:
STRATEGY模式可以用来 分离通用算法,并允许高层
算法独立于它的具体实现得以复用.
2) 途径:
STRATEGY模式是使用委托来解决问题,并且
STRATEGY模式还允许具体实现细节独立于高层的算法
而得以复用。(不过需要付出一些额外的复杂性、内存和运行时
间为代价.)
52. 3) 基本结构
为了使用委托来实现通用算法与具体实现的分离,该模式倒置
了通用算法和具体实现之间的依赖关系。例如:
《interface》
Application Application
+init
Runner
+idle
+run +cleanup
+done:boolean
ftocStrategy
具体地说:
(1)不是把通用算法(+run)放在一个抽象基类中,而是放在名为
ApplicationRunner的具体类中。
(2)把通用算法必须要调用的抽象方法(例如: +init ,+idle等 )定义在名为
Applicatinn的接口中。
(3)从这个接口派生出 ftocStrategy,并把它传给ApplicationRunner。
之后, ApplicationRunner就可把具体工作委托给该接口来完成。
53. 该例的程序可以是:
ApplicationRunner.java
Public class ApplicationRunner
{
private Application itsApplication=null;
public ApplicationRunner(Application app)
{ itsApplication=app;}
Public void run() 通用算法(+run)
{ itsApplication.init();
While(!itsApplication.done())
itsApplication.idle()
itsApplication.cleanup }
}
54. Application.java
Public interface Application
{
public void init();
public void idle(); 通用算法必须要调用的抽象方法
public void cleanup();
public boolean done();
}
55. ftocstrategy.java
import java.io.*;
Public class ftocstrategy implements Application
{ 从接口Application派生出
ftocStrategy,
private InputStreamReader isr;
private BufferedReader br;
private boolean isDone=false;
public static void main(Sring[] args) throws Exception
{(new ApplicationRunner(new ftosStrategy())).run();}
把ftocStrategy传给ApplicationRunner。
public void init()
{ isr=new InputStreamReader(System.in);
br=new BufferedReader(isr); } Init的实现
56. public void idle() Idle的实现
{ Sring fahrString=readLineAndReturnNullIfError();
if ( fahrString==null fahrString.length()=0) isDone=thue;
else { double fahr=Double.parseDouble(fahrString);
double celcius=5.0/9.0*(fahr-32);
System.out.println(“F=”+fahr+”, c=”+celcius); }
}
public void cleanup() Cleanup的实现
{ System.out.println( “ftoc exit”); }
public boolean done()
{ return isDone; }
58. 通过抽象基类的方式,实现OCP的另一结
构: Policy
+PolicyFunction()
-ServiceFunction()
Implementation
-ServiceFunction()
Template Method模式:既开放又封闭的基类
其中,Policy类具有一组实现了某种策略的共有函数。这些策略函数使用
一些抽象接口描述了一些要完成的功能。但在这个结构中,这些抽象接口
是Policy 类的一部分。(它们在C++中表现为纯虚函数,在Java中表现为
抽象方法。)
Policy类中的这些函数在Policy的子类型中予以实现。这样,可以通过从
Policy类派生出新类的方式,对Policy中指定的行为进行扩展或更改。
(体现了开放性和封闭性!)
59. 附4:
(1) TEMPLATE METHOD模式
该模式把所有通用代码放入一个抽象基类的实现方法中,而
将所有实现细节都交付给该基类的抽象方法。如下所示:
基类A
方法:F { 通用算法 }
方法1(抽象方法):{实现细节}
方法2(抽象方法):{实现细节}
……
60. (2)TEMPLATE METHOD模式和STRATEGY模式的比较
TEMPLATE METHOD模式和STRATEGY模式都可以
用来把一个功能的通用部分和实现细节清晰地分离开来。
两个模式是满足OCP原则最常用的方法。其途径是,遵循
倒置依赖原则(DIP),使通用算法不依赖具体的实现,并
使通用算法和具体实现都依赖抽象.
2种模式的区别:继承和委托
尽管TEMPLATE METHOD模式和STRATEGY模式所要解决的问题是
类似的,而且可以互换使用.
但 TEMPLATE METHOD模式使用继承来解决问题,
而STRATEGY模式是使用委托来解决问题.
63. 1、Liskov替换原则的内容:
子类型必须能够替换掉它们的基类型。
这一原则是Barbara Liskov在1988年首先提出的。
“这里需要如下替换原则:若对每个类型S的对象o1, 都存在
一个类型T的对象o2, 使得在所有针对编写的程序P中,用o1,替
换o2后,程序P行为功能不变,则S是T的子类型。 ”
若违反这一原则,会出现什么后果?例如:
假定有一个函数f,它的参数指向某个基类B的“指针”或“引
用”。同样,假定有B的某个派生类D,如果把D的对象作为B
类型传递给f,就会导致f出现错误的行为。那么D就违反了
LSP。显然,D对于f来说是脆弱的。
64. 2、例子
违反 LSP,常常以违反OCP的方式使用运行时的类型辨别(RTTI)而
导致的。这种方式往往是显式地使用一个if 语句或if/else,来确定一个对象
的类型,以便选择该类型的正确行为。考虑以下程序:
struct Point {double x,y;};
struct Shape { enum ShapeType { square,circle } itsType;
Shape(ShapeType t):itsType{} };
struct Circle:public Shape
{ Circle():Shape(Circle) {};
void Draw() const;
Point itsCenter;
double itsRadius; };
65. struct Square:public Shape
{ Sqaure():Shape(Sqaure) {};
void Draw() const;
Point itsTopLeft;
double itsSide; };
void DrawShape(const Shape& s)
运行时的类型辨别 { if (s.itsType==Shape::square)
static_cast<const Square&>(s).Draw();
else if (s.itsType==Shape::circle)
static_cast<const Circle&>(s).Draw(); }
68. 结论:
1、LSP是导致违背OCP的主要原因之一。
正是子类型的可替换性,才使使用基类的模块在无须改
变的情况下可以予以扩展。
2、这种可替换性必须是开发人员可以隐式依赖的东西。因此
如果没有显示地强制基类类型的契约,那么代码就必须良
好地并显式地表达出这一点。
3、“is-A”的含义过于宽泛,以至于不能作为子类型的定义。
子类型的正确定义是“可替换性的”,这里的可替换性可以
通过显式或隐式的契约予以定义。
69. (四)依赖倒置原则( DIP)
1、内容:
a. 高层模块不应该依赖低层模块。二者都应该依赖抽象。
b. 抽象不应该依赖细节。细节应该依赖抽象。
该原则是框架设计的核心原则
2、层次化
Booch曾经说过:“所有结构良好的面向对象构架都具有清
晰的层次定义,每个层次通过一个定义良好的、受控的接口向
外提供一组内聚的服务。”
70. 如果对以上这句话只是简单地予以理解,就有可能会出现以
下结构:
Policy Layer
Mechanism Layer
Utility Layer
这一结构存在一个潜伏的错误特征:
(1)Policy layer对于其下的任一改动,包括对 Utility Layer
的改动,都是敏感的。
(2)这种依赖是可传递的。
关于这一结构的评价:这一结构是不好的!
71. 就面向对象技术而言,层次化的合适模型应该是:
Policy
•每个较高层次为它所
《interface》 需要的服务声明一个抽
Policy
Policy Service
Layer 象接口,较低层次实现
Interface
这些抽象接口;
•每个较高层次都通过
Mechanism
抽象接口使用下一层。
《interface》
Mechanism Mechanism Service 这样
Layer Interface 1)高层就不依赖低
层,而低层则依赖高
Utility 层;
2)不仅解除了其中的
Utility 传递依赖关系,而且还
Layer 解除了高层与其它层的
依赖关系。
72. 倒置的接口所有权问题
这就是著名的Hollywood 原则“Don’t call us, we’ll call you.”
即低层模块实现了在高层模块中声明并被高层模块调用的
接口。
依赖于抽象
这是解释DIP规则的一个启发式规则。该规则建议不应该依赖
具体类-即程序中的所有依赖关系都应该终止于抽象类或接口。
根据这个启发式规则,可知:
• 任何变量都不应该持有一个指向具体类的指针或引用;
• 任何类都不应该从具体类派生;
• 任何方法都不应该覆写它的任何基类中已实现的方法。
74. 3、违反依赖倒置原则的例子:
Lamp 这是一个以Button控制Lamp
Button
+poll()
+turnOn() 的系统。其中,Button类直接
+ turnOff() 依赖Lamp对象,这个设计不能
其对应的Java : 让Button控制其它设备。
public class Button 该设计方案违反了DIP,即应
{ 用程序的高层策略没有与低层
provide Lamp itsLamp; 策略相分离,自然就使抽象依
赖于具体细节。
public void poll()
什么是高层策略?它是应用
{ if (/* some condition */ )
的抽象,是那些不随具体细节
itsLamp.turnOn();} 而改变的“真理”。它是系统内
部的系统-隐喻(metaphore).
}
75. 通过倒置对Lamp的依赖关系,可以形成以下设计:
《interface》
Button
ButtonServer
+poll()
+turnOn()
+ turnOff()
Lamp
其中,由接口ButtonServer提供一些抽象方法,Button可以使用这些方
法对有关设备进行控制。由Lamp来实现 接口ButtonServer。
这样的设计就具有很好的灵活性。但问题是:Lamp不可能还受其他类的
控制。(原因:由于Lamp实现了接口ButtonServer.)
77. (五)接口隔离原则( ISP )
1、内容:
不应强迫客户(client)依赖它们不用的方法。
解决的问题:ISP原则用来处理“胖接口”所带来的缺点。
何谓胖接口?如果类的接口不是内聚的,即接口可以分解
为多组方法,每一组方法服务于一组不同客户程序,则称
该接口是胖接口。
2、胖接口所到来的问题--接口污染
考虑一个安全系统。其中有一些Door对象,可以加锁和解
锁,并且知道自己所处的状态(开/关)。
78. class Door
{ public:
virtual void Lock()=0;
virtual void UnLock()=0;
virtual bool IsDoorOpen()=0; }
显然,这是一个抽象类。客户程序可以使用符合Door的那些接
口对象,而不依赖Door的特定实现。
现在考虑这样一个实现-TimedDoor.其中,如果门开着的时间
过长,就会发出警报。为此, TimedDoor对象需要和一个名为
Timer的对象进行交互。即如果希望得到超时时间,就可以调用
Timer的Register函数,该函数有2个参数:一个是超时时间,另
一个是TimerClient对象的指针,该对象的TimeOut函数会在达
到超时时予以调用。
83. 2)使用委托 分离接口
创建一个由TimerClient 所派生的对象,并把该对象的请求委
托给TimedDoor.
其中, 当TimedDoor
Timer 《Interface》 想要向Timer对象注册
0..* Timer Client Door 一个超时请求时,它就
+Timeout
创建一个
DoorTimerAdapter,并
把它注册给Timer.
Door Timer
Adapter TimedDoor 当Timer对象发送
+Timeout() +DoorTimeOut Timeout消息给
DoorTimerAdapter时,
《creates》 DoorTimerAdapter把
这个消息委托给
TimedDoor.
84. 对使用委托 分离接口这一方案的分析:
1) 该方案遵循了接口分离原则(ISP),避免了Door的客户
程序之间的耦合.
2) 即使对Timer的进行了修改,也不会影响Door的使用者.
其中 一例中Timer的定义如下:
Class Timer
{ public:
void Register(int timeout,int timeoutID,TimeCclient* client);
};
Class TimerClient
{ public:
virtual void TimeOut( int timeoutID)=0;
};
85. 3) Timed Door也不必具有和 TimerClient一样的接口;
4) DoorTimerAdapter会将TimerClient接口转换为TimedDoor
接口.
因此,这是一个非常通用的解决方案。
5) 该方案尽管是一种通用的,但不是很优雅的.即每次想去注
册一个超时请求时,都要创建一个新的对象.这对于那些对
内存和运行时间要求高的系统而言,例如嵌入式实时系统,
就显得不够理想。
86. 附5:Adapter模式
问题的提出:设计一个运行于台灯的软件 Light
Switch +turnOn
+ turnOff
其中,Switch 对象不断地了解开关的状态,并可以向Light
发送一个相应的 turnOn 和 turnOff 消息。
这一设计违反了2个设计原则:
依赖倒置原则(DIP): Switch 依赖了具体类Light 。
开放封闭原则(OCP):在任何需要Switch 的地方都需要附带
Light,从而不易扩展Switch,使之去管理除Light之外的其它对象。
87. 为了解决以上问题,可以使用ABSTRACT SERVER模式和
ADAPTER模式
1)ABSTRACT SERVER模式
ABSTRACT SERVER模式,是在Switch和Light之间引入一个接
口,使Switch能够控制任何实现这个接口的设备。如下所示
:
《interface
》
Switch Switchable
+turnOn
+ turnOff
Light
+turnOn
+turnOff
90. 2)ADAPTER模式
问题:以上的解决方案可能会违反单一职责原则(SRP)。
即如果从第三方购买了Light,而没有源代码怎么办?或如果想让
Switch控制其它一些类,但却不能由Switchable派生怎么办?
此时,可以使用ADAPTER模式. 由Switchable 派生出一个适
配器,并委托给Light。这样 Switch就可以控制任何可以 “
打开”和“关闭”的对象。如下所示:
《interface
Switch 》
Switchable
+turnOn
+ turnOff
Light Adapter Light
《delegates
+turnOn +turnOn
+turnOff 》
+turnOff
91. 其中:
1) Switch控制的对象可以不具有和Switchable 中一样的
turnOn和turnOff方法。适配器会适配到对象的接口。
2)LightAdapyer类被称为对象形式的适配器。还有一种称为类
形式的适配器,如下所示:
《interface
Switch 》
Switchable
Light
+turnOn +turnOn
注:这种形式的适配器要比对 + turnOff +turnOff
象形式的适配器高效一些,且
容易使用,但付出的代价是: Light Adapter
使用了高耦合的继承关系。 +turnOn
+turnOff
92. 3)使用多继承 分离接口
下图给出使用多继承并达到遵循ISP的方案:
Timer 《Interface》
0..* Timer Client Door
其中,在这个模型中,
TimedDoor同时继承 +Timeout
了Door和TimerClient.
这样,尽管这2个基类的
客户程序都可以使用
TimedDoor
TimedDoor,但在实际
上却都不再依赖 +DoorTimeOut
TimedDoor.从而,它们
就通过分离的接口使 比较: 就这一问题的2),3)方案而言,
用同一对象. 只有当Door Timer Adapter对象所做的转换是必须
的,或不同时候回需要不同转换时,才会选择2)中给出
的方案.
95. 第一条原则 复用发布等价原则(REP)
复用的粒度就是发布的粒度。
注:
1)如果一个包中的软件是用来复用的,那么就不能包含那些
不是为了复用而设计的软件。一个包中的软件,要么都是
可复用的,要么都不是可复用的。
2)考虑复用软件的人,希望一个包中的所有类,对于同一类
用户都是可复用的,不希望一个用户发现包中所包含的类
一些对他是所需要的,而另一些对他是不适合的。
96. 第2条原则 共同复用原则(CRP)
一个包中的所有类应该是共同复用的。如果复用了包中的一
个类,那么就要复用包中的所有类。
注:
原因:类很少会孤立的复用,一般需要与该类所描述的抽
象相关的一些类进行协作。-它们应该在同一包中。
结论:没有紧密联系的类,不应该在同一包中。
97. 第3条原则 共同封闭原则(CCP)
包中的所有类对于同一类性质的变化应该是共同封闭的。
一个变化若对一个包产生影响,则将对该包中的所有类产生
影响,而对其它包不产生任何影响。
注:
1)这是单一职责原则对于包的规定。这条原则规定了一个包
不应该包含多个引起变化的原因。
2)这条原则通过把对于一些被确定的变化种类开放的类共同
组织在同一包中,可以增强可维护性;可以增强进行有策
略的封闭,使所设计的系统对我们经历过的最常见的变化
做到封闭。
101. 下面给出一个包的无环依赖结构:
MyApplication
1) 这是一个有向无环
图(DAG)
Message Task MyTasks 2) 当负责MyDialogs的
团队发布一个新的
Window Window 版本时,按逆向便
Database 可知道,受影响的
包为MyTasks和
Tasks MyDialogs MyApplication。因
此负责这2个包的人
就要决定何时与
Window MyDialogs的新版本
进行集成。
3)当MyDialogs发布时,完全不会影响其它包。
4)当发布整个系统时,首先编译、测试和发布Window包,接之
是Message Window和Database,再后来是MyTasks,最后是
MyApplication。
102. 消除依赖环的方法:
MyDialogs MyApplication
第一种方法:使用倒置依赖原则
如果出现以下情况: X Y
即新的需求迫使需要更改MyDialogs中的一个类X,使之使用
MyApplication中的一个类。这样就产生了2个包之间的一个依
赖关系环。
使用倒置依赖原则,可以创建一个具有MyDialogs需要的接
口的抽象基类,然后把该抽象基类放进MyDialogs中,并使
MyApplication中的类Y从其继承。这就倒置了它们之间的依赖
关系。如下所示
MyDialogs MyApplication
《interface》
X Y
x Server
104. 第5条原则 稳定依赖原则(SDP)
朝着稳定的目标进行依赖。
1)问题与解决
(1)要使设计是可维护的,某种程度的易变性是必要的。
达到这一目标,可以使用共同封闭原则(CCP),创建对
某些变化类型敏感的包,使这样的包是可变的。
但对于这样的包,就不应让一个难以改变的包依赖于它。
否则可变的包也难以改变。
(2)对于这样易于改变的包,其他人只要创建一个对它的
依赖,就可以使它变得难以更改,这就是软件的反常特性。
问题解决:遵循稳定依赖原则(SDP),可以确保那些打算
易于更改的模块,不会被那些比它们难以更改的
模块所依赖。
105. 稳定性
韦伯斯特认为:如果某物“不容易移动”,则认为它是稳定的。
例如:树立的硬币和放在地面上的桌子。
按着这一说法,稳定性与更改它所付出的工作量是有关的。
那么,就软件来讲,难以更改的因素很多,如规模、复杂性、
清晰程度等,但不考虑这些因素,使一个软件包难以“移动”的
可行方法是让许多软件包都依赖它。-往往需要大量的工作量
如下图所示: X包就是一个稳定的包,其中:
1)有3个包依赖它。
称X对这3个包负有责任。
2)X不依赖任何包,因此所有
X 外部影响都不会使其改变。
称X是无依赖的。
107. 稳定性度量
一种度量“位置”稳定性的方法是,计算入、出包的依赖关系
数目
Ce
I= ————
Ca + Ce
其中,• Ca 输入耦合度:指处于该包外部并依赖于该包内的
类的数目
• Ce 输出耦合度:指处于该包内部并依赖于该包外的
类的数目
• I :不稳定性。取值范围为[0,1]。
I=0 表示该包具有最大的稳定性
I=1 表示该包具有最大的不稳定性
108. 例如:
pa pb
q r s
pc pd
t u v
其中,pc外部有3个类依赖pc内的类,所以Ca = 3。此外,pc外部
有一个类被pc内的类依赖,所以Ce =1。I=1/4。
1)在C++中,这些依赖关系是通过#include语句表示的。
2)在Java中,可以通过计算import语句以及类的修饰名的
数目来计算度量I。
109. 注: 1)SDP规定一个包的I值应该大于它所依赖包的I值(即I值
应该顺着依赖的方向减少)。
2)该条原则并非要求所有的包都是稳定的。
如果一个系统的所有包都是最大程度稳定的,那么该系
统就是不能改变的。实际上,我们希望一些包是稳定的
而另一些包是不稳定的。
I=1 instable instable I=1 stable
instable Flexible
I=0 I=0
理想的包配置 违反了SDP
110. stable stable
U U
Uinterface Flexible
Flexible
《interface C
C
》 IU
产生违反SDP的原因 使用DIP,修正稳定性
1)创建一个接口类IU,并把它放入Uinterface中。确保接
口IU中声明了U要使用的所有方法。
2)让C从该接口继承。以解除stable对Flexible的依赖,并
使这2个包都依赖Uinterface。
结果:1) Uinterface非常稳定。(I=0),而Flexible仍保
持必须的不稳定性(I=1)
2)现在,所有依赖都顺着I减少的方向。
111. 第6条原则 稳定抽象原则(SAP)
包的抽象程度应该和其稳定程度一致。
1)解决的问题
一般来讲,应该把封装系统高层设计的软件放在稳定的包
中(I=0)。但如何使高稳定(I=0)的包又是足够灵活,可
以经受得住变化?
这就是提出本原则的动机!
2)基本思想
把包的稳定性和抽象性联系起来,规定:
(1)一个稳定的包应该是抽象的,即应该包含一些抽象类,
这样的稳定性就可以进行扩展,既是灵活的,又没有过分
限制设计;
(2)不稳定的包应该是具体的,其内部的具体代码易于更改
112. 与DIP原则的关系
(1)SAP和SDP结合在一起,形成了关于包的依赖倒置原则
(DIP)原则。
SAP规定,依赖应该朝着稳定的方向进行;
而SDP规定,稳定性意味着抽象性。
说到底,依赖应该朝着抽象的方向进行。
(2)DIP是一个处理类的原则,类没有灰度(the shades of
grey)问题,即类要么是抽象的,要么不是。而SAP和
SDP的结合,允许一个包是部分抽象的,部分是稳定的。
(注:一个抽象类是一个至少具有一个纯接口的类,并且不能被
实例化。)
113. 抽象性度量
一种度量抽象性程度的方法是:
Na
A= ——
Nc
其中,Na:包中抽象类的数目
Nc:包中类的总数
A:抽象性
可见,A的取值范围为0到1。0意味着包中没有任何抽象类;
1意味着包中只包含抽象类;
114. 抽象性与稳定性之间的关系
(0,
1)
A
(0, I (1,
0) 0)
可见,1)最稳定、最抽象的包位于(0,1)处;
2)最不稳定、最不抽象的包位于(1,0)处。
肯定地说,不能强制所有的包都位于(0,1)处,那么在A/I
图中,那些位置是合适的?
显然:1)(0,0)附近区域的包,是高度稳定且具体的包。
-这样的包不是灵活的,无法对它进行扩展,也很难
116. 显然
1)位于主序列上的包既不是太抽象,因为它具有稳定性;
又不是太不稳定,因为它具有抽象性。既不是无用的,
又不是令人痛苦的 ; 就抽象性而言,它被其他包所依
赖; 就具体性而言,它又依赖于其他包。
2)包的最佳位置是2个端点处。但这样的包在整个项目中
一般均不大于50%,其他的包在该线段的附近就是很好
的。
117. 到主序列的距离
|A+I-1|
距离 D = ————
2
取值范围为[0,-0.707]
规范化距离 D’ = |A+I-1|
取值范围为[0,1]
0 表示一个包正处在主序列上
1表示一个包到主序列的距离最远
注:实践中,使用D’要比D更方便一些。
118. 使用“到主序列的距离”这一概念的意义
1)可以全面分析一个设计和主序列的相符程度。其步骤是;
首先,计算每个包的D;
然后,对所有D值不在0附近的包进行复查和调整。
--这一分析有助于设计者确定 • 哪些包更容易维护
• 哪些包对变化更不敏感。
2)可以对设计进行统计分析。其步骤是:
计算设计中所有包D的均值和方差。显然,期望该设计的
均值和方差均接近于0。其中,方差可用来建立“控制限制”,
以标识那些与所有其他包显得“特别”的包。
A (0, 无用区域
1)• • •
•
Z=1 • • • • • Z=2
••• • • • •
•
Z=2 • • • • ••
• • • • •• Z=1
• I
120. 结论
1)依赖性度量的作用:
可以测量一个设计是否为“好”的依赖,以及抽象结构模式
的匹配程度。
2)使用:
• 该度量不是万能的,它只是一种度量方法。
• 该度量可能只对某些应用是合适的,而对另一些是不合
适的。可以使用其他更好的度量方法,测量一个设计的质
量。