深蓝软件,凌宁,微软(中国)有限公司,深蓝软件,凌宁,微软(中国)有限公司,深蓝软件,设计模式(1),导言:面向对象设计原则,设计模式(1)导言:面向对象设计原则,目录,1 面向对象的设计原则,2 设计模式概论,3 单件,4 观察者,目录1 面向对象的设计原则,面向对象的设计原则,1单一职责SRP,2.OCP开闭原则,3.里氏代换LSP,4.依赖倒转DIP,5.接口隔离ISP,6.迪米特法则LOD,7,合成聚合复用原则(CARP),面向对象的设计原则1单一职责SRP,Booch和Rumbaugh的新的“统一”标识符,Booch和Rumbaugh的新的“统一”标识符,单一职责SRP,一个优良的系统设计,强调模块间保持低耦合、高内聚的关系,在面向对象设计中这条规则同样适用,所以面向对象的第一个设计原则就是:单一职责原则(SRP,Single Responsibility Principle)。,单一职责,强调的是职责的分离,在某种程度上对职责的理解,构成了不同类之间耦合关系的设计关键,因此单一职责原则或多或少成为设计过程中一个必须考虑的基础性原则。,1.单一职责原则(SRP),一个类,最好只做一件事,只有一个引起它变化的原因。,例如,在一个Game类中,可能会具有两个不同的职责,一个职责是维护创建当前轮的比赛,另一个职责是计算总比赛得分。根据srp原则,着两个职责应该分离到两个类中,Game类保持维护创建当前轮的比赛,Scorer类负责计算比赛的得分。,如何要把这两个职责分离到单独的类中呢?,如果一个类承担的职责过多,等于把这些职责耦合在了一起。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。,例如,考虑下图的设计。Retangle类具有两方法,如图。一个方法把矩形绘制在屏幕上,另一个方法计算矩形的面积。,单一职责SRP一个优良的系统设计,强调模块间保持低耦合、高内,有两个不同的Application使用Rectangle类,如上图。一个是计算几何面积的,Rectangle类会在几何形状计算方面给予它帮助。另一个Application实质上是绘制一个在舞台上显示的矩形。,Rectangle类具有了两个职责,第一个职责是提供一个矩形形状几何数据模型;第二个职责是把矩形显示在屏幕上。,对于SRP的违反导致了一些严重的问题。首先,我们必须在计算几何应用程序中包含核心显示对象的模块。其次,如果绘制矩形Application发生改变,也可能导致计算矩形面积Application发生改变,导致不必要的重新编译,和不可预测的失败。,有两个不同的Application使用Rectangle类,,一个较好的设计是把这两个职责分离到下图所示的两个完全不同的类中。这个设计把Rectangle类中进行计算的部分一道GeometryRectangle类中。,现在矩形绘制方式的改变不会对计算矩形面积的应用产生影响了。,一个较好的设计是把这两个职责分离到下图所示的两个完全不同的类,1.1 什么是职责,在SRP中,我们把职责定义为“变化的原因”(a reason for change)。如果你能够想到多于一个的动机去改变类,那么这个类就具有多于一个的职责。有时,我们很难注意到这一点。我们习惯于以组的形式去考虑职责。,class Modem,public:,void dial(pno:String):;,void hangup():;,void send(c:Char):;,void recv():;,上述Modem接口,大多数人会认为这个接口看起来非常合理。该接口声明了4个函数确实是Modem所具有的功能。然而,该接口却显示出了两个职责,一个是连接管理(dial+hangup),第二个是数据通信(send+recv)。,这两个职责应该被分离开么?这依赖于应用程序的变化。是按照实际情况决定的。如果应用程序的变化会影响连接管理,那么设计就具有僵化的臭味。因为,调用send和recv的类必须要重新编辑。在这种情况下,这两个职责应该被分离,这样做会避免这两个职责耦合在一起。,另一方面,如果应用程序的变化总是导致这两方面职责同时变化,那么就不必分离他们。实际上,分离他们就会具有不必要的复杂性臭味,1.1 什么是职责,1.2 持久化,上图展示了一种常见的违反SRP的情况,Employee类包含了业务逻辑和对于持久层的控制。这两个职责在大多数情况下决不应该混合在一起。,业务规则往往会频繁的变化,而持久化的方式却不会如此频繁的变化,并且变化的原因也是完全不同的。把业务规则和持久模块绑定在一起的做法是不妥的。当僵化性和脆弱性的臭味变得强烈,那么就应该使用FACADE和PROXY模式对设计进行重构,分离这两个职责。,小结:,SRP是所有原则中最简单的之一,也是最难正确应用的。我们会自然的把职责结合在一起。软件设计要做的许多内容,就是发现职责并把那些职责相互分离。分离的原则也不是教条性的,需要应实际需求而定。,1.2 持久化上图展示了一种常见的违反SRP的情况,Empl,2.OCP开闭原则,“Closed for Modification;Open for Extension”是所有面向对象原则的核心。软件设计本身所追求的目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。其他的设计原则,很多时候是为实现这一目标服务的,例如以Liskov替换原则实现最佳的、正确的继承层次,就能保证不会违反开放封闭原则。,OCP的动机很简单:软件是变化的。不论是优质的设计还是低劣的设计都无法回避这一问题。OCP说明了软件设计应该尽可能地使架构稳定而又容易满足不同的需求。,2.OCP开闭原则“Closed for Modificat,为什么要OCP?,通常,对于开发完的代码都需要多种测试才能够投入使用,这包括:,1 设计人员进行初期的架构设计,2 要经过开发人员的单元测试、集成测试。,3 然后再到测试人员的白盒测试、黑盒测试。,4 最后还要由用户进行一定的测试。,经过漫长的测试,代码才能够投入使用。但是软件产品的维护和升级又是一个永恒的话题,在维护的过程中,你可能要不断地增加一些小功能;在升级的过程中,你要增加一些较大的功能。,这种功能的扩展,就要求我们改变原有的代码。但是,对原代码的修改就会深刻地影响到原来的功能的方方面面:,1 可能对旧代码引入了新的错误,使你不得不对旧代码进行大规模的修改。,2 可能引起你不得不重新构造系统的架构。,3 即使新增的代码对旧代码没有影响,你也不得不对原来的系统做一个全面的测试。,4 经过一段时间,也许你认为以前代码更好,更符合用户需求,所有上述列出来的问题,都是对系统功能进行扩展所不能承受的代价。换句话说,我们设计出来的系统,一定要是扩展性良好的系统。如何才能够设计出扩展性良好的系统呢?这就需要在软件系统设计时遵守开闭原则,为什么要OCP?通常,对于开发完的代码都需要多种测试才能够投,玉帝的智慧,玉帝招安美猴王 的例子,不劳师动众、不破坏天规便是“闭”,收仙有道便是“开”。招安之法便是玉帝天庭的“开一闭”原则,通过给美猴王封一个“弼马温”的官职,便可使现有系统满足变化了的需求,而不必更改天庭的既有秩序,玉帝的智慧玉帝招安美猴王 的例子,如何在OO中引入OCP原则?,把对实体的依赖改为对抽象的依赖就行了。下面的例子说明了这个过程:,05赛季的时候,一辆F1赛车有一台V10引擎。但是到了06赛季,国际汽联修改了规则,一辆F1赛车只能安装一台V8引擎。车队很快投入了新赛车的研发,不幸的是,从工程师那里得到消息,旧车身的设计不能够装进新研发的引擎。我们不得不为新的引擎重新打造车身,于是一辆新的赛车诞生了。但是,麻烦的事接踵而来,国际汽联频频修改规则,搞得设计师在“赛车”上改了又改,最终变得不成样子,只能把它废弃。,如何在OO中引入OCP原则?把对实体的依赖改为对抽象的依赖就,为了能够重用这辆昂贵的赛车,工程师们提出了解决方案:首先,在车身的设计上预留出安装引擎的位置和管线。然后,根据这些设计好的规范设计引擎(或是引擎的适配器)。于是,新的赛车设计方案就这样诞生了。,为了能够重用这辆昂贵的赛车,工程师们提出了解决方案:首先,在,做到开闭原则,就注意以下两点。,1)多使用抽象类,在设计类时,对于拥有共同功能的相似类进行抽象化处理,将公用的功能部分放到抽象类中,所有的操作都调用子类。这样,在需要对系统进行功能扩展时,只需要依据抽象类实现新的子类即可。如图10-1所示,在扩展子类时,不仅可以拥有抽象类的共有属性和共有函数,还可以拥有自定义的属性和函数。,做到开闭原则,就注意以下两点。1)多使用抽象类,2)多使用接口,与抽象类不同,接口只定义子类应该实现的接口函数,而不实现公有的功能。在现在大多数的软件开发中,都会为实现类定义接口,这样在扩展子类时实现该接口。如果要改换原有的实现,只需要改换一个实现类即可。,如图各子类由接口类定义了接口函数,只需要在不同的子类中编写不同的实现即可,当然也可以实现自有的函数。,2)多使用接口 与抽象类不同,接口只定义子类应,Liskov(女程序员)替换原则,在一个软件系统中,子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。,第一个例子:正方形不是长方形,“正方形不是长方形”是一个理解里氏代换原则的最经典的例子。在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统中,让正方形继承自长方形是顺利成章的事情。现在,我们截取该系统的一个代码片段进行分析:,Liskov(女程序员)替换原则 在一个软件系统中,子类应该,正方形不是长方形,长方形类Rectangle:,class Rectangle,double length;double width;,public:double getLength(),return length;void setLength(double height),this.length=length;double getWidth(),return width;void setWidth(double width)this.width=width;,正方形类Square:class Square:public Rectangle,public:,void setWidth(double width),Rectangle:setLength(width);Rectangle:setWidth(width);void setLength(double length),Rectangle:.setLength(length);Rectangle:.setWidth(length);,正方形不是长方形长方形类Rectangle:正方形类Squa,正方形不是长方形,由于正方形的度和宽度必须相等,所以在方法setLength和setWidth中,对长度和宽度赋值相同。类TestRectangle是我们的软件系统中的一个组件,它有一个resize方法要用到基类Rectangle,resize方法的功能是模拟长方形宽度逐步增长的效果:测试类TestRectangle:,class TestRectangle,public:,void resize(Rectangle&objRect),while(objRect.getWidth()=objRect.getLength(),objRect.setWidth(objRect.getWidth()+1);,正方形不是长方形由于正方形的度和宽度必须相等,所以在方法se,正方形不是长方形,我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就