设计模式02-面向对象设计原则.ppt
设计模式(2),湖北汽车工业学院软件教研室,一、类(对象)的职责分配,面向对象的基石是类与对象,如何确定问题域中的类与对象?类与对象是属性和服务的封装,服务体现了类与对象的职责。如何分配类与对象的职责呢?-GRASP(General Responsibility Assignment Software Patterns),通用职责分配软件模式,Information Expert(信息专家)如果某个类拥有完成某个职责所需要的所有信息,那么这个职责就应该分配给这个类来实现。这时,这个类就是相对于这个职责的信息专家。例如:常见的网上商店里的购物车(ShopCar),需要让每种商品(SKU)只在购物车内出现一次,购买相同商品,只需要更新商品的数量即可。如下图:,比较商品是否相同的方法需要放到那里类里来实现呢?分析业务得知需要根据商品的编号(SKUID)来唯一区分商品,而商品编号是唯一存在于商品类里的,所以根据信息专家模式,应该把比较商品是否相同的方法放在商品类里。,Creator(创造者)如果一个类创建了另一个类,那么这两个类之间就有了耦合,产生了依赖关系。依赖或耦合带来的问题就是在以后的维护中会产生连锁反应,但必要的耦合是逃不掉的,我们能做的就是正确地创建耦合关系,不要随便建立类之间的依赖关系。凡符合以下条件的情况,都应该由类来创建类,这时是的创建者:是的聚合是的容器持有初始化的信息(数据)记录的实例频繁使用,例如:因为订单(Order)是商品(SKU)的容器,所以应该由订单来创建商品。如下图:,Low coupling(低耦合)低耦合意思就是要尽可能地减少类之间的连接。下面这些情况会造成类A、B之间的耦合:A是B的属性 A调用B的实例的方法 A的方法中引用了B,例如B是A方法的返回值或参数。A是B的子类,或者A实现了B低耦合降低了因一个类的变化而影响其他类的范围;低耦合使类更容易理解,因为类会变得简单,更内聚。,关于低耦合,有下面一些基本原则:a.Dont Talk to Strangers原则:不需要通信的两个对象之间,不要进行无谓的连接。如果A已经和B有连接,如果分配A的职责给B不合适的话(违反信息专家模式),那么就把B的职责分配给A。两个不同模块的内部类之间不能直接连接。,例如:Creator模式的例子里,实际业务中需要另一个出货人来清点订单(Order)上的商品(SKU),并计算出商品的总价,但是由于订单和商品之间的耦合已经存在了,那么把这个职责分配给订单更合适,这样可以降低耦合,以便降低系统的复杂性。如下图:,High cohesion(高内聚)功能性紧密相关的职责应该放在一个类里,并共同完成有限的功能,那么就是高内聚。这样更有利于类的理解和重用,也便于类的维护。高内聚也可以说是一种隔离,每一个部分(类)都有自己独立的职责和特性,每一个部分内部发生了问题,也不会影响其他部分,因为高内聚的对象之间是隔离开的。例如:一个订单数据存取类(OrderDAO),订单即可以保存为Excel模式,也可以保存到数据库中;那么,不同的职责最好由不同的类来实现,这样才是高内聚的设计,如下图:,这里把两种不同的数据存储功能分别放在两个类里来实现,如果未来保存到Excel的功能发生错误,那么检查OrderDAOExcel类就可以了。这使系统模块化,方便划分任务,比如这两个类就可以分配个不同的人同时进行开发,这样也提高了团队协作和开发进度。,Controller(控制器)用来接收和处理系统事件的职责,一般应该分配给一个能够代表整个系统的类,这样的类通常被命名为“XX处理器”、“XX协调器”或者“XX会话”。关于控制器类,有如下原则:a.系统事件的接收与处理通常由一个高级类来代替。b.一个子系统会有很多控制器类,分别处理不同的事务。,Polymorphism(多态)多态更符合高内聚和低耦合原则例如:设计一个绘图程序,要支持可以画不同类型的图形。先定义一个抽象类Shape,让矩形(Rectangle)、圆形(Round)分别继承这个抽象类,并重写(override)Shape类里的Draw()方法,这样就可以使用同样的接口(Shape抽象类)绘制出不同的图形,如下图:,Pure Fabrication(纯虚构)高内聚低耦合,是系统设计的终极目标,但是内聚和耦合永远都是矛盾对立的。高内聚会拆分出更多数量的类,但是对象之间需要协作来完成任务,这又造成了高耦合。由一个纯虚构的类来协调内聚和耦合,可以在一定程度上解决上述问题。例如:上面多态模式的例子,如果我们的绘图程序需要支持不同的系统,那么因为不同系统的API结构不同,绘图功能也需要不同的实现方式,那么该如何设计更合适呢?如下图:,增加了纯虚构类AbstractShape,不论是哪个系统都可以通过AbstractShape类来绘制图形,我们即没有降低原来的内聚性,也没有增加过多的耦合,,Indirection(间接)“间接”顾名思义,就是这个事不能直接来办,需要绕个弯才行。绕个弯的好处就是,本来直接会连接在一起的对象彼此隔离开了,一个的变动不会影响另一个。例如“两个不同模块的内部类之间不能直接连接”,但是可以通过中间类来间接连接两个不同的模块,这样对于这两个模块来说,它们之间仍然是没有耦合/依赖关系的。,Protected Variations(受保护变化)预先找出不稳定的变化点,使用统一的接口封装起来,如果未来发生变化的时候,可以通过接口扩展新的功能,而不需要去修改原来旧的实现。,二、可复用面向对象设计的基本原则,2.1、开闭原则OCP(Open-Closed Principle):-Software entities(classes,modules,functions,etc)should be open for extension,but closed for modification-软件实体(类,模块,函数等)应当对扩展开放,对修改关闭;,理解:撰写软件模块时,应该让它可以扩展,同时无须修改。可以改变模块的行为,而不改模块的源代码。OCP原则的基础是抽象,只有抽象才可以抓住事物的本质和共性,保证相对的稳定,实现以不变应万变的功能。支持OCP的技术是抽象、继承、多态、接口。符合该法则便意味着最高等级的复用性(reusability)和可维护性(maintainability)。,符合OCP的模块标准:可扩展,即“对扩展是开放的”(Open For Extension)模块的行为可以被扩展,以需要满足新的需求。不可更改,即“对更改是封闭的”(Closed for Modification)模块的源代码是不允许进行改动的。,优越性:通过扩展已有的软件系统,提供新的行为,以满足软件的新需求,使得变化中的软件系统有较好的适应性和灵活性;已有的软件模块,特别是抽象层模块不被修改,这就使得变化中的软件就有一定的稳定性和灵活性。,说明OCP的理想是在不改变源代码的情况下改变模块的功能,这不易实现。如果部分的OCP实现,也可以戏剧性地改善应用程序的结构。如果能控制变动不再传播到现有已经可以运行的程序代码当中,就很理想了。OCP在模式设计中得到广泛的应用。,2.2、如何实现开闭原则,2.2.1、针对接口编程,而不是针对(接口的)实现编程Program To An Interface,Not An Implementation,接口的定义对象声明的每一个方法所指定的方法名、参数和返回值构成了对象的方法签名(signature)。对象方法所定义的所有方法签名的集合被称为该对象的接口(interface),它描述了该对象所能接受的全部请求的集合。在面向对象系统中,对象只通过接口与外部交流。对象接口与其功能实现是分离的,两个有相同接口的对象可以有完全不同的实现。对应程序设计语言中interface/abstract class。,从接口导出的子类有如下特点:子类仅仅添加或重定义操作,而没有隐藏父类的操作。所有的子类都能响应接口中的请求,子类的类型都是接口的子类型。,实施原则不将变量声明为某个特定的具体类的实例对象,而声明为抽象类所定义的接口。当不得不在系统的某个地方实例化具体的类时,可应用创建型模式(Abstract Factory,Builder,Factory Method,Prototype和Singleton)。创建型模式确保系统是采用针对接口的方式书写的,而不是针对实现而书写的。,好处:1)客户无须知道所使用对象的特定类型,只须对象有客户所期望的接口;2)客户无须知道所使用的对象是用什么类来实现的,只须知道定义接口的抽象类。3)降低程序各部分之间的耦合性,使程序模块互换成为可能。,例子,Public interface IManeuverablePublic void left();public void right();Public void forward();public void reverse();public void setSpeed(double speed);Public float getSpeed();,Public class Car implements IManeuverable/Public class Boat implements IManeuverable/IManeuverable Iman=new Car();Iman.setspeed();,2.2.2、优先使用对象组合,而不是类继承,英文原意:Favor object composition over class inheritance.什么是组合(composition)是一种通过创建一个组合了其它对象的对象,从而获得新功能的复用方法。包括聚合(aggregation)和包容(containment)两种特殊形式。聚合标识拥有关系(has a),如运动员与运动队。包容是强烈的拥有关系,拥有者负责被拥有者的生命周期,如汽车与轮胎关系。,组合与继承的比较联系组合以及继承的目的都是为了复用。区别继承是一种通过扩展一个已有对象的实现,从而获得新功能的复用方法;组合使得原有对象成为新对象的一部分,因此新的对象可以调用已有对象的功能。,组合的优点:因为对象只能通过接口访问,所以组合并不破坏封装性;因为对象的实现是基于接口写的只要类型一致,运行时刻可以用一个对象来替代另一个对象,所以实现上存在较少的依赖关系。优先使用对象组合有助于保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。基于对象组合的设计将使系统的行为依赖于对象间的关系而不是被定义在某个类中。,组合的缺点:会有较多的对象需要管理。为了能将多个不同的对象作为组合块来使用,必须仔细地对接口进行定义。,继承的优点:因为大部分功能可以通过继承关系自动进入子类,所以新的实现较为容易。继承使修改或者扩展继承来的实现较为容易。,继承的缺点:因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。父类通常至少定义了部分子类的具体表示,由于子类能揭示其父类的实现细节,所以继承常被认为“破坏了封装性”。子类中的实现与它的父类有紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法只继承抽象类,因为抽象类通常提供较少的实现。,如何应用这条原则理想情况下,不应为获得复用而去创建新的构件。应该尽可能只使用对象组合技术,通过组装已有的构件就能获得需要的功能。使用继承的复用使得创建新的构件要比组装旧的构件来得容易。实践中继承和对象组合常一起使用。,2.2.3、封装变化点,英文encapsulating the concept that varies(GOF)含义:将系统中经常变化的部分封装起来,以和稳定的部分隔离。理解使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。获得最大限度复用的关键在于对新需求和已有需求发生变化时的预见性,这就要求系统要能够封装变化点,以减少系统重新设计的机会。,评价:有助于增加复用性,并降低系统耦合度。基于设计模式的解决方案擅长封装变化点。,三、可复用面向对象设计的一般原则,1、单一职责原则(SRP)2、Liskov 替换原则(LSP)3、依赖倒置原则(DIP)4、接口隔离原则(ISP)5、迪米特原则,3.1 单一职责原则(SRP),SRP(Single-Responsibility Principle)含义:A Calss should have only one reason to changge。一个类应该仅有一个原因导致其变化。,理解如果一个类包含过多的职责,这就意味着会有多个导致其变化的原因。而且,这些变化往往不是孤立的,它们通过多个职责间的耦合关系相互作用,使类产生多个互相依赖的不确定的演化方向,带来维护和重用上的困难。按单一职责分解系统,是否会造成类过多?注意分解的粒度。控制分解的标准是“变化”,把那些多边或易变的部分分离出去。,3.3 Liskov 替换原则(LSP),LSP(Liskov Substitution Principle)含义Subtype must be substitutable for their base type。派生类应该可以替换基类。Function That Use Referennces To Base(Super)Classes Must Be Able To Use Objects Of Derived(Sub)Classes Without Knowing It。使用指向基类(超类)的引用的函数,必须能够在不知道具体派生类(子类)对象类型的情况下使用它们。,理解:如果在用户的某些方法中,使用基类作为参数,则应该允许在方法中传入派生类对象的实例。LisKov代换法则利用了类型继承关系中的向上转型机制(向上转型是安全的)。LSP保证一个子类总是能够被用在其基类可以出现的地方。理氏代换原则是继承复用的基石,只有衍生类可以替换基类,软件单位不受到影响,基类才能被真正复用,而衍生类也才能在基类的基础上增加新的行为。注意:反过来并不成立,一个软件实体如果使用的是一个子类的话,不应定适用于基类。,3.4 依赖倒置原则(DIP),DIP(Dependency Inversion Principle)含义:Abstractions should not depend on details,Details should depend on abstractions。抽象不应该依赖于实现细节,实现细节应该依赖于抽象。理解依赖倒置原则强调依赖接口、抽象方法及抽象类,而不是依赖具体方法或类。,理解结构化设计中,高层模块依赖于底层模块,抽象层依赖于具体层,从而形成由上到下顺序依赖的架构设计,这种结构从顶部主模块开始,然后向下发展出细节部分。存在着弱点:底层模块实现的是具体细节,这就决定了它的多变。如果总是向下依赖于多变的下级模块,必然会把这种变化不定的因素传播到上层,影响系统得稳定性和健壮性。虽然通过增加中间层的努力可以吸收底层变化因素,但中间层并不能改变这种顺序依赖,相反,中间层的加入反而又导致了上层对中间层的依赖,增加了系统的复杂性。,错误的依赖方向,倒置的原则扭转上述错误的依赖关系,抽象层次包含的是应用系统的业务逻辑和宏观的、对整个系统来说重要的战略性决定,是必然的体现,而具体层次则要实现有关的算法和逻辑,以及战术的决定,带有相当大的偶然性,会经常变化。,面向对象的架构中,高层模块不再依赖于含有实现细节的模块,而是依赖抽象的接口。这种设计的好处是:通过抽象接口取代被依赖的中间层,保证了上层结构的稳定。因为底层细节实现模块的变化并不导致接口的改变。也就是说,同一接口允许有不同的实现方法,甚至不同的实现模块。,如何应用这一原则设计中的每一个依赖关系,应该是依赖于接口,或一个抽象类,而不是一个具体类。抽象的东西比较稳定。能使我们有更多的回旋余地。抽象层可以作为上下层之间的枢纽,代表设计中可以伸缩或扩展的地方,而且在变动时不致于被修改,从而达到OCP的要求。在可能的情况下,这个法则应该尽可能遵循,至少组件一定要遵循这个法则。如COM就坚持这个法则。COM组件唯一可以看到的部分就是其抽象的接口。,在设计中常发生依赖具体类的地方是建构对象。在架构设计中的任何地方都可能发生构造对象实例的情况。抽象工厂模式是解决这个问题的一个简单方法。在多层系统的设计中,通过代理模式引入的代理层,可以使得原来相互依赖的层之间实现依赖反转,从而满足层与层之间独立演化的需求。特别在业务层依赖数据层或其他第三方中间层时,这种设计可以很好的消除耦合。,3.5 接口隔离原则(ISP),ISP(Interface Segregation Principle)含义:Clients should not be forced to depend on methods that they do not use.不应该强迫客户程序依赖于它们不用的方法。每一个接口都应该是一种角色,不要干不该干不要强迫客户依赖于它们不用的方法。,理解依据不同的客户端而划分出多个不同的专用接口,比使用单一的总接口好。如果客户程序依赖于那些有它们不使用的方法的类,当其他客户要求这个类改变时,就会影响到这个客户程序,这无意中导致了所有客户程序之间的耦合。由于希望尽可能地避免这种耦合,因此希望分离接口。接口是对客户端的一种开发的承诺,一种通信的协议,一种服务的契约,一旦公布很难收回。,一个反面例子,一个正面例子,关于接口常遇到的问题合并接口:把多个功能相近的不同接口合并成一个大接口。这样看上去总的接口数量减少了,但针对不同的客户端而言,它们却要面向大接口中大量冗余的,用不着的方法,使得接口臃肿,难以理解。预留接口:设计时预留接口,为以后系统的变更和扩展做考虑。结果可能给系统留下了一大块垃圾,徒增系统的维护成本。,接口设计原则以客户端的需求划分接口。即使两个不同类型的客户端需要相同的方法,也要增加接口。通过设计原则LSP、DIP而不是通过预留接口来增加系统的可扩展性。推荐使用抽象接口,给现有对象增加接口而不是修改接口。,3.6 迪米特法则,LoD(Law of Demeter),1987年秋天Ian Holland在美国东北大学为一个叫迪米特的项目做设计时提出。又叫最少知识原则(Least Knowledge Principle)Only talk to your immediate friends只与你直接的朋友们通信 Dont Talk to strangers不要跟“陌生人”说话每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。,狭义的迪米特法则:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另外一个类的某一个方法,可以通过第三者转发这个调用。,Friend类持有一个Stranger类的私有对象,他们是朋友类:publicclassFriend privateStrangerstranger=newStranger();publicvoidoperation2()publicStrangerprovide()returnstranger;,Someone类有一个方法接受一个Friend类型的变量,但直接与Stranger类通信:publicclassSomeone publicvoidoperation1(Friendfriend)Strangerstranger=friend.provide();stranger.operation3();不符合迪米特法则,改造,首先在Friend类里添加一个方法,封装对Stranger类的操作:publicclassFriend privateStrangerstranger=newStranger();publicvoidoperation2()publicStrangerprovide()returnstranger;publicvoidforward()stranger.operation3();,然后,我们重构Someone的operation1方法,让其调用新提供的forward方法:publicclassSomeone publicvoidoperation1(Friendfriend)friend.forward();现在Someone对Stranger的依赖完全通过Friend隔离,这样的结构已经符合狭义迪米特法则了。,狭义迪米特法则一个明显的缺点:会在系统里造出大量的小方法,散落在系统的各个角落。这些小的方法会造成设计师的迷惑和困扰。遵循迪米特法则会使一个系统的局部设计简化,也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。,结合依赖倒转原则,对代码进行如下重构来解决这个问题,首先添加一个抽象的Stranger类,使Someone依赖于抽象的“Stranger”角色,而不是具体实现:publicabstract class AbstractStranger abstractvoidoperation3();然后,让Stranger从该类继承:publicclassStrangerextendsAbstractStranger publicvoidoperation3(),随后,重构Someone使其依赖抽象的Stranger角色:publicclassSomeone publicvoidoperation1(Friendfriend)AbstractStrangerstranger=friend.provide();stranger.operation3();最后,重构Friend的provide方法,使其返回抽象角色:publicclassFriend privateStrangerstranger=newStranger();publicvoidoperation2()publicAbstractStrangerprovide()returnstranger;,现在,AbstractStranger成为Someone的朋友类,而Friend类可以随时替换掉AbstractStranger的实现类,Someone不再需要了解Stranger的内部实现细节。下图是重构后的UML类图:,广义迪米特法则 在类的划分上,应该创建有弱耦合的类;在类的结构设计上,每一个类都应当尽量降低成员的访问权限;在类的设计上,只要有可能,一个类应当设计成不变类;,在对其他类的引用上,一个对象对其它对象的引用应当降到最低;尽量降低类的访问权限;谨慎使用序列化功能;不要暴露类成员,而应该提供相应的访问器(属性)。,本章作业,2.1、面向对象设计的基本原则是什么?如何实施这个原则?2.2、面向对象设计的一般原则有哪些?谈谈你对相关原则的理解。2.3、查阅资料,阐述GRASP模式。,