1、周坤 人生才是大方向面向对象设计原则 1.单一职责原则(SRP): 一个类只能承担一个职责,即就一个类而言,应该仅有一个引起它变化的原因。2.开- 闭原则(OCP): 对扩展开放,对修改关闭3.里氏替换原则(LSP): 子类型必须能够替换基类型4.依赖倒置原则(DIP :dependence inversion): 针对接口编程,不要针对具体编程。5.接口隔离原则(ISP): 使用多个专门的接口比使用单一的接口要好。同一个角色提供宽、窄不同的接口,以对付不同的客户端6.合成/聚集复用原则(CARP:composite/aggregate reuse principle): 即尽量使用合成/聚集
2、复用而不是继承复用。7.迪米特法则(LoD:Law of Demeter):最少知识原则:要求一个类(软件实体) 应该尽可能少的和其它类(软件实体)发生作用1.单一职责原则(SRP):一个类只能承担一个职责,即就一个类而言,应该仅有一个引起它变化的原因。因为每一个职责都是变化的一个轴线,当需求变化时,该变化会表现为类的职责的变化。如果一个类承担了多于一个的职责时,那么引起它变化的原因就有多个。2.开- 闭原则(OCP): 对扩展开放,对修改关闭一个软件实体应该对扩展开放,对修改关闭。就是说,在设计一个模块的时候,应当使这个模块可以在不被修改的情况下被扩展。开-闭原则是面向对象设计可复用的第一块
3、基石。如何实现开-闭原则:1)抽象化是关键可以给出一个或多个抽象类或接口,规定出所有的具体类必须提供的方法的特征(signature)作为系统的抽象层。这个抽象层预见了所有的可能扩展。从抽象层导出的具体类可以修改或扩展系统的行为而不用修改抽象层。2)对可变性封装原则找到一个系统的可变因素把它封装成起来。一个系统的可变性应当被封装成对象,同一种可变性的不同表象意味着同一个继承等级结构中的不同子类。不要把一种可变性和另外一种可变性混在一起,类的继承结构一般不要超过两层,不然就意味着把两种不同的可变性封装在了一起。3.里氏替换原则(LSP): 子类型必须能够替换基类型凡是使用基类型的地方,子类型一定
4、适用,即子类可以替换基类。反之不成立。里氏替换原则是继承复用的基石,只有子类可以替换掉基类,软件单位的功能不受影响时,基类才能真正的被复用。而子类才能在基类的基础上增加新的功能。周坤 人生才是大方向如:Base 是基类,Sub 是 Base 的子类,那么如果有方法 method(Base b),则method(s)一定成立,其中 s 是 Sub 的对象。LSP 原则在设计模式中的体现:策略模式:合成模式:代理模式:4.依赖倒置原则(DIP :dependence inversion): 针对接口编程抽象不应该依赖于具体,具体要依赖于抽象.(针对接口编程,不要针对具体编程。)针对接口编程的意思是
5、说,应该使用抽象类或接口进行变量的类型声明、参数的类型声明、方法的返回类型声明、以及数据类型的转换等。不要针对具体编程的意思是说,不应该使用具体类进行变量的类型声明、参数的类型声明、方法的返回类型声明、以及数据类型的转换等.如:不应该这样声明:Vector employee = new Vector();而应该这样声明 employee:List employee = new Vector();这样的好处是决定将 Vector 改成 ArrayList 时,代码的改动比较少.list 是接口。依赖倒置原则前提是假定所有的具体类都是变化的,有一些具体类可能是相当稳定,不会发生变化的。消费这个具体
6、类的客户端就完全可以使用这个具体类,没必要为此发明一个抽象类型。以抽象方式耦合是依赖倒置原则的关键。5.接口隔离原则(ISP): 使用多个专门的接口比使用单一的接口要好。从客户的角度来说:一个类对另外一个类的依赖性应当是建立在最小的接口上的。如果客户端只需要某一些方法的话,那么就应当向客户端提供这些需要的方法,而不要提供不需要的方法。向客户端提供 public 方法意味着向客户端作出承诺,过多的承诺会给系统的维护造成不必要的负担。定制服务:同一个角色提供宽、窄不同的接口,以对付不同的客户端。这样每一接口仅将客户需要的行为暴露给客户端,而没有将客户不需要的行为放在接口中。适配器模式是接口隔离原则
7、的一个应用。6.合成/聚集复用原则(CARP:composite/aggregate reuse principle):合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部份,新的对象通过向这些对象的委派达到复用已有功能的目的。即尽量使用合成/聚集复用而不是继承复用。周坤 人生才是大方向Has-A:代表一个类是另外一个类的一个组成部分或角色。Is-A:代表一个类是另外一个类的一种。如果两个类是“Has-A” 关系那么应使用合成 /聚集,如果是“Is-A”关系那么可使用继承。7.迪米特法则(LoD:Law of Demeter):最少知识原则又叫最少知识原则、不要和“陌
8、生人” 讲话原则, 要求一个类 (软件实体)应该尽可能少的和其它类(软件实体) 发生作用。不要向间接对象(陌生人)发送消息(讲话)。只应该向以下直接对象(朋友)发送消息:1)this 对象(自身).2)方法的参数对象。3)this 的属性直接引用的对象。4)作为 this 属性的集合中的元素对象。5)在方法中创建的对象。其意图是避免客户与间接对象和对象之间的对象连接产生耦合。利用一个“中间人 ”是“迪米特法则”解决问题的办法。与依赖倒置原则互补使用:引入一个抽象类型”抽象陌生人” 对象,使它成为某人的”朋友” 。某人现在与抽象角色建立了朋友关系,这样的好处是“朋友” 可以随时把“陌生人” 换掉
9、,只要“陌生人”实现了相同的抽象类型,那么某人就无法区分它们。如图迪米特法则的主要用意是控制信息的过载,在将其运用到系统设计中应注意以下几点:1)在类的划分上,应当创建有弱耦合的类。类之间的耦合越弱,就越有利于复用。2)在类的结构设计上,每一个类都应当尽量降低成员的访问权限。一个类不应当 public 自己的属性,而应当提供取值和赋值的方法让外界间接访问自己的属性。3)在类的设计上,只要有可能,一个类应当设计成不变类。4)在对其它对象的引用上,一个类对其它对象的引用应该降到最低。5)限制局部变量的有效范围,有需要一个变量的时候才声明它,可以有效的限制局部变量的有效范围。二.其它一些原则1.关于
10、抽象类只要有可能,不要从具体类继承。如图,在一个以继承关系形成的等级结构里面,树叶节点应该是具体类,而树枝节点应该是抽象类或接口。抽象类应该拥有尽可能多的共同代码。周坤 人生才是大方向抽象类应该拥有尽可能少的数据。在一个继承的等级结构中,共同的代码应该尽可能的往等级结构的上方移动。把重复的代码从子类移到超类中,可以提高代码的复用率。一个对象从超类继承过来的代码,在不使用时不会造成对资源的浪费。一个对象的数据,无论是否使用都会占用资源,因此数据应该尽量放到具体类或等级结构的低端。2.命令查询原则(command-query separation principle):任何方法都可能是如下情况之一
11、:1)执行动作(更新,调整.)的命令方法,这种方法通常具有改变对象状态等副作用,并且是 void(没有返回值的)。2) 向调用者返回数据的查询,这种方法没有副作用,不会永久性地改变任何对象的状态。一个方法不能同时属于以上两种类型。所以查询或者 getter 方法不会作任何修改,而命令也不会有任何返回值。举个反面例子:Missile m = new Missibe();/看上去对我无妨!String name = m.getName();.public class Missile.public String getName()launch();/发射导弹return name;.面向对象设计原则
12、的总结正如牛顿三大定律在经典力学中的位置一样,“开-闭”原则(Open-Closed Principle)是面向对象的可复用设计(Object Oriented Design 或 OOD)的基石。周坤 人生才是大方向其他设计原则(里氏代换原则、依赖倒转原则、合成/聚合复用原则、迪米特法则、接口隔离原则)是实现“开-闭”原则的手段和工具。一、“开-闭”原则(Open-Closed Principle,OCP)1.1“开 -闭”原则的定义及优点1)定义:一个软件实体应当对扩展开放,对修改关闭( Software entities should be open for extension,but c
13、losed for modification.)。即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。2)满足“开-闭”原则的系统的优点a)通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性。b)已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。c)这样的系统同时满足了可复用性与可维护性。1.2 如何实现“ 开-闭”原则在面向对象设计中,不允许更改的是系统的抽象层,而允许扩展的是系统的实现层。换言之,定义一个一劳永逸的抽象设计层,允许尽可能多的行为在实现层被实现。解决问题关键在
14、于抽象化,抽象化是面向对象设计的第一个核心本质。 对一个事物抽象化,实质上是在概括归纳总结它的本质。抽象让我们抓住最最重要的东西,从更高一层去思考。这降低了思考的复杂度,我们不用同时考虑那么多的东西。换言之,我们封装了事物的本质,看不到任何细节。在面向对象编程中,通过抽象类及接口,规定了具体类的特征作为抽象层,相对稳定,不需更改,从而满足“对修改关闭”;而从抽象类导出的具体类可以改变系统的行为,从而满足“对扩展开放”。对实体进行扩展时,不必改动软件的源代码或者二进制代码。关键在于抽象。1.3 对可变性的封装原则“开-闭”原则也就是“对可变性的封装原则”(Principle of Encapsu
15、lation of Variation ,EVP)。即找到一个系统的可变因素,将之封装起来。换言之,在你的设计中什么可能会发生变化,应使之成为抽象层而封装,而不是什么会导致设计改变才封装。周坤 人生才是大方向“对可变性的封装原则”意味着:a)一种可变性不应当散落在代码的许多角落,而应当被封装到一个对象里面。同一可变性的不同表象意味着同一个继承等级结构中的具体子类。因此,此处可以期待继承关系的出现。继承是封装变化的方法,而不仅仅是从一般的对象生成特殊的对象。b)一种可变性不应当与另一种可变性混合在一起。作者认为类图的继承结构如果超过两层,很可能意味着两种不同的可变性混合在了一起。使用“可变性封装
16、原则”来进行设计可以使系统遵守“开-闭”原则。即使无法百分之百的做到“开-闭”原则,但朝这个方向努力,可以显著改善一个系统的结构。二、里氏代换原则(Liskov Substitution Principle, LSP)2.1 概念定义:如果对每一个类型为 T1 的对象 O1,都有类型为 T2 的对象 O2,使得以 T1 定义的所有程序 P 在所有的对象 O1 都代换为 O2 时,程序 P 的行为没有变化,那么类型 T2是类型 T1 的子类型。即,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类。而且它觉察不出基类对象和子类对象的区别。也就是说,在软件里面, 把基类都替换成它的子类,程
17、序的行为没有变化。反过来的代换不成立,如果一个软件实体使用的是一个子类的话,那么它不一定适用于基类。任何基类可以出现的地方,子类一定可以出现。基于契约的设计、抽象出公共部分作为抽象基类的设计。2.2 里氏代换原则与“ 开-闭”原则的关系实现 “开-闭”原则的关键步骤是抽象化。基类与子类之间的继承关系就是抽象化的体现。因此里氏代换原则是对实现抽象化的具体步骤的规范。违反里氏代换原则意味着违反了“开-闭”原则,反之未必。三、 依赖倒转原则(dependence inversion principle, DIP)3.1 概念周坤 人生才是大方向依赖倒转原则就是要依赖于抽象,不要依赖于实现。(Abst
18、ractions should not depend upon details. Details should depend upon abstractions.)要针对接口编程,不要针对实现编程。(Program to an interface, not an implementation.)也就是说应当使用接口和抽象类进行变量类型声明、参数类型声明、方法返还类型说明,以及数据类型的转换等。而不要用具体类进行变量的类型声明、参数类型声明、方法返还类型说明,以及数据类型的转换等。要保证做到这一点,一个具体类应当只实现接口和抽象类中声明过的方法,而不要给出多余的方法。传统的过程性系统的设计办法倾
19、向于使高层次的模块依赖于低层次的模块,抽象层次依赖于具体层次。倒转原则就是把这个错误的依赖关系倒转过来。面向对象设计的重要原则是创建抽象化,并且从抽象化导出具体化,具体化给出不同的实现。继承关系就是一种从抽象化到具体化的导出。抽象层包含的应该是应用系统的商务逻辑和宏观的、对整个系统来说重要的战略性决定,是必然性的体现。具体层次含有的是一些次要的与实现有关的算法和逻辑,以及战术性的决定,带有相当大的偶然性选择。具体层次的代码是经常变动的,不能避免出现错误。从复用的角度来说,高层次的模块是应当复用的,而且是复用的重点,因为它含有一个应用系统最重要的宏观商务逻辑,是较为稳定的。而在传统的过程性设计中
20、,复用则侧重于具体层次模块的复用。依赖倒转原则则是对传统的过程性设计方法的“倒转”,是高层次模块复用及其可维护性的有效规范。特例:对象的创建过程是违背“开闭”原则以及依赖倒转原则的,但通过工厂模式,能很好地解决对象创建过程中的依赖倒转问题。3.2 关系“开-闭”原则与依赖倒转原则是目标和手段的关系。如果说开闭原则是目标,依赖倒转原则是到达“开闭“原则的手段。如果要达到最好的“开闭“原则,就要尽量的遵守依赖倒转原则,依赖倒转原则是对“抽象化“的最好规范。里氏代换原则是依赖倒转原则的基础,依赖倒转原则是里氏代换原则的重要补充。3.3 耦合(或者依赖)关系的种类:零耦合(Nil Coupling)关
21、系:两个类没有耦合关系周坤 人生才是大方向具体耦合(Concrete Coupling)关系:发生在两个具体的(可实例化的)类之间,经由一个类对另一个具体类的直接引用造成。抽象耦合(Abstract Coupling)关系:发生在一个具体类和一个抽象类(或接口)之间,使两个必须发生关系的类之间存有最大的灵活性。3.3.1 如何把握耦合我们应该尽可能的避免实现继承,原因如下:1 失去灵活性,使用具体类会给底层的修改带来麻烦。2 耦合问题,耦合是指两个实体相互依赖于对方的一个量度。程序员每天都在(有意识地或者无意识地)做出影响耦合的决定:类耦合、API 耦合、应用程序耦合等等。在一个用扩展的继承实
22、现系统中,派生类是非常紧密的与基类耦合,而且这种紧密的连接可能是被不期望的。如 B extends A ,当 B 不全用 A 中的所有 methods 时,这时候,B 调用的方法可能会产生错误!我们必须客观的评价耦合度,系统之间不可能总是松耦合的,那样肯定什么也做不了。3.3.2 我们决定耦合的程度的依据何在呢 ?简单的说,就是根据需求的稳定性,来决定耦合的程度。对于稳定性高的需求,不容易发生变化的需求,我们完全可以把各类设计成紧耦合的(我们虽然讨论类之间的耦合度,但其实功能块、模块、包之间的耦合度也是一样的),因为这样可以提高效率,而且我们还可以使用一些更好的技术来提高效率或简化代码,例如
23、c# 中的内部类技术。可是,如果需求极有可能变化,我们就需要充分的考虑类之间的耦合问题,我们可以想出各种各样的办法来降低耦合程度,但是归纳起来,不外乎增加抽象的层次来隔离不同的类,这个抽象层次可以是抽象的类、具体的类,也可以是接口,或是一组的类。我们可以用一句话来概括降低耦合度的思想:“针对接口编程,而不是针对实现编程。在我们进行编码的时候,都会留下我们的指纹,如 public 的多少,代码的格式等等。我们可以耦合度量评估重新构建代码的风险。因为重新构建实际上是维护编码的一种形式,维护中遇到的那些麻烦事在重新构建时同样会遇到。我们知道在重新构建之后,最常见的随机 bug 大部分都是不当耦合造成
24、的 。如果不稳定因素越大,它的耦合度也就越大。某类的不稳定因素=依赖的类个数/被依赖的类个数依赖的类个数 在编译此类的时被编译的其它类的个数总和周坤 人生才是大方向3.3.3 怎样将大系统拆分成小系统解决这个问题的一个思路是将许多类集合成一个更高层次的单位,形成一个高内聚、低耦合的类的集合,这是我们设计过程中应该着重考虑的问题!耦合的目标是维护依赖的单向性,有时我们也会需要使用坏的耦合。在这种情况下,应当小心记录下原因,以帮助日后该代码的用户了解使用耦合真正的原因。3.4 怎样做到依赖倒转?以抽象方式耦合是依赖倒转原则的关键。抽象耦合关系总要涉及具体类从抽象类继承,并且需要保证在任何引用到基类
25、的地方都可以改换成其子类,因此,里氏代换原则是依赖倒转原则的基础。在抽象层次上的耦合虽然有灵活性,但也带来了额外的复杂性,如果一个具体类发生变化的可能性非常小,那么抽象耦合能发挥的好处便十分有限,这时可以用具体耦合反而会更好。层次化:所有结构良好的面向对象构架都具有清晰的层次定义,每个层次通过一个定义良好的、受控的接口向外提供一组内聚的服务。依赖于抽象:建议不依赖于具体类,即程序中所有的依赖关系都应该终止于抽象类或者接口。尽量做到:1、任何变量都不应该持有一个指向具体类的指针或者引用。2、任何类都不应该从具体类派生。3、任何方法都不应该覆写它的任何基类中的已经实现的方法。3.5 依赖倒转原则的
26、优缺点依赖倒转原则虽然很强大,但却最不容易实现。因为依赖倒转的缘故,对象的创建很可能要使用对象工厂,以避免对具体类的直接引用,此原则的使用可能还会导致产生大量的类,对不熟悉面向对象技术的工程师来说,维护这样的系统需要较好地理解面向对象设计。依赖倒转原则假定所有的具体类都是会变化的,这也不总是正确。有一些具体类可能是相当稳定,不会变化的,使用这个具体类实例的应用完全可以依赖于这个具体类型,而不必为此创建一个抽象类型。四、合成/聚合复用原则(Composite/Aggregate Reuse Principle或 CARP)4.1 概念周坤 人生才是大方向定义:在一个新的对象里面使用一些已有的对象
27、,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用这些对象的目的。应首先使用合成/聚合,合成/ 聚合则使系统灵活,其次才考虑继承,达到复用的目的。而使用继承时,要严格遵循里氏代换原则。有效地使用继承会有助于对问题的理解,降低复杂度,而滥用继承会增加系统构建、维护时的难度及系统的复杂度。如果两个类是“Has-a”关系应使用合成、聚合,如果是“Is-a”关系可使用继承。“Is-A“是严格的分类学意义上定义,意思是一个类是另一个类的“一种“。而“Has-A“则不同,它表示某一个角色具有某一项责任。4.2 什么是合成?什么是聚合?合成(Composition)和聚合(Aggregation
28、 )都是关联( Association)的特殊种类。聚合表示整体和部分的关系,表示“拥有” 。如奔驰 S360 汽车,对奔驰 S360 引擎、奔驰 S360 轮胎的关系是聚合关系,离开了奔驰 S360 汽车,引擎、轮胎就失去了存在的意义。在设计中, 聚合不应该频繁出现,这样会增大设计的耦合度。合成则是一种更强的“拥有”,部分和整体的生命周期一样。合成的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个合成关系的成分对象是不能与另一个合成关系共享的。换句话说,合成是值的聚合(Aggregation by Value),而一般说的聚合是引用的聚合(Aggregation by Reference)。明白了合成和聚合关系,再来理解合成/聚合原则应该就清楚了,要避免在系统设计中出现,一个类的继承层次超过 3 层,则需考虑重构代码,或者重新设计结构。当然最好的办法就是考虑使用合成/聚合原则。4.3 通过合成/ 聚合的优缺点优点:1) 新对象存取成分对象的唯一方法是通过成分对象的接口。2) 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。3) 这种复用支持包装。4) 这种复用所需的依赖较少。5) 每一个新的类可以将焦点集中在一个任务上。
Copyright © 2018-2021 Wenke99.com All rights reserved
工信部备案号:浙ICP备20026746号-2
公安局备案号:浙公网安备33038302330469号
本站为C2C交文档易平台,即用户上传的文档直接卖给下载用户,本站只是网络服务中间平台,所有原创文档下载所得归上传人所有,若您发现上传作品侵犯了您的权利,请立刻联系网站客服并提供证据,平台将在3个工作日内予以改正。