面向对象 的七大设计原则

面向对象 的七大设计原则 面向对象的七大设计原则文章目录面向对象的七大设计原则简述七大原则之间的关系一、开闭原则The Open-Closed Principle OCP概念理解系统设计需要遵循开闭原则的原因开闭原则的实现方法一个符合开闭原则的设计开闭原则的相对性二、 里氏替换原则Liskov Substitution Principle LSP概念理解里式替换原则的优点重构违反LSP的设计三、 迪米特原则最少知道原则Law of Demeter LoD概念理解迪米特原则的优缺点违反迪米特原则的设计与重构使用迪米特原则时要考虑的四、单一职责原则为什么一个类不能有多于一个以上的职责职责的划分使用单一职责原则的理由五、 接口分隔原则Interface Segregation Principle ISP概念理解违反ISP原则的设计与重构接口分隔原则的优点和适度原则单一职责原则和接口分隔原则的区别六、 依赖倒置原则Dependency Inversion Principle DIP概念理解依赖倒置原则的违反例和重构怎么使用依赖倒置原则依赖倒置原则的优点七、 组合/聚合复用原则Composite/Aggregate Reuse Principle CARP概念理解什么时候才应该使用继承通过组合/聚合复用的优缺点通过继承来进行复用的优缺点简述类的设计原则有七个包括开闭原则、里氏代换原则、迪米特原则最少知道原则、单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则。七大原则之间的关系七大原则之间并不是相互孤立的彼此间存在着一定关联一个可以是另一个原则的加强或是基础。违反其中的某一个可能同时违反了其余的原则。开闭原则是面向对象的可复用设计的基石。其他设计原则是实现开闭原则的手段和工具。一般地可以把这七个原则分成了以下两个部分设计目标开闭原则、里氏代换原则、迪米特原则设计方法单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则一、开闭原则The Open-Closed Principle OCP软件实体模块类方法等应该对扩展开放对修改关闭。概念理解开闭原则是指在进行面向对象设计中设计类或其他程序单位时应该遵循对扩展开放open对修改关闭closed 的设计原则。开闭原则是判断面向对象设计是否正确的最基本的原理之一。根据开闭原则在设计一个软件系统模块类方法的时候应该可以在不修改原有的模块修改关闭的基础上能扩展其功能扩展开放。扩展开放某模块的功能是可扩展的则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。修改关闭某模块被其他模块调用如果该模块的源代码不允许修改则该模块修改关闭的。软件系统的功能上的稳定性持续性要求模块是修改关闭的。通过下边的例子理解什么是扩展开放和修改关闭左边的设计是直接依赖实际的类不是对扩展开放的。右边的设计是良好的设计Client对于Server提供的接口是封闭的Client对于Server的新的接口实现方法的扩展是开放的。系统设计需要遵循开闭原则的原因稳定性。开闭原则要求扩展功能不修改原来的代码这可以让软件系统在变化中保持稳定。扩展性。开闭原则要求对扩展开放通过扩展提供新的或改变原有的功能让软件系统具有灵活的可扩展性。遵循开闭原则的系统设计可以让软件系统可复用并且易于维护。开闭原则的实现方法为了满足开闭原则的对修改关闭原则以及扩展开放原则应该对软件系统中的不变的部分加以抽象在面向对象的设计中可以把这些不变的部分加以抽象成不变的接口这些不变的接口可以应对未来的扩展接口的最小功能设计原则。根据这个原则原有的接口要么可以应对未来的扩展不足的部分可以通过定义新的接口来实现模块之间的调用通过抽象接口进行这样即使实现层发生变化也无需修改调用方的代码。接口可以被复用但接口的实现却不一定能被复用。接口是稳定的关闭的但接口的实现是可变的开放的。可以通过对接口的不同实现以及类的继承行为等为系统增加新的或改变系统原来的功能实现软件系统的柔性扩展。好处提高系统的可复用性和可维护性。简单地说软件系统是否有良好的接口抽象设计是判断软件系统是否满足开闭原则的一种重要的判断基准。现在多把开闭原则等同于面向接口的软件设计。一个符合开闭原则的设计需求创建一系列多边形。首先下面是不满足开闭原则的设计方法Shape.henumShapeType{ isCircle, isSquare}; typedef struct Shape { enumShapeType type } shape;一键获取完整项目代码1234Circle.htypedef struct Circle { enumShapeType type; double radius; Point center; } circle; void drawCircle( circle* );一键获取完整项目代码123456Square.htypedef struct Square { enumShapeType type; double side; Point topleft; } square; void drawSquare( square* );一键获取完整项目代码123456drawShapes.cpp#include Shape.h #include Circle.h #include Square.h void drawShapes( shape* list[], intn ) { int i; for( int i0; in; i ) { shape* s list[i]; switch( s-type ) { case isSquare: drawSquare( (square*)s ); break; case isCircle: drawCircle( (circle*)s ); break; } } }一键获取完整项目代码1234567891011121314151617该设计不是对扩展开放的当增加一个新的图形时Shape不是扩展的需要修改源码来增加枚举类型drawShapes不是封闭的当其被其他模块调用时如果要增加一个新的图形需要修改switch/case此外该设计逻辑复杂总的来说是一个僵化的、脆弱的、具有很高的牢固性的设计。用开闭原则重构该设计如下图此时在该设计中新增一个图形只需要实现Shape接口满足对扩展开放也不需要修改drawShapes()方法对修改关闭。开闭原则的相对性软件系统的构建是一个需要不断重构的过程在这个过程中模块的功能抽象模块与模块间的关系都不会从一开始就非常清晰明了所以构建100%满足开闭原则的软件系统是相当困难的这就是开闭原则的相对性。但在设计过程中通过对模块功能的抽象接口定义模块之间的关系的抽象通过接口调用抽象与实现的分离面向接口的程序设计等可以尽量接近满足开闭原则。二、 里氏替换原则Liskov Substitution Principle LSP所有引用基类的地方必须能透明地使用其派生类的对象。概念理解也就是说只有满足以下2个条件的OO设计才可被认为是满足了LSP原则不应该在代码中出现if/else之类对派生类类型进行判断的条件。派生类应当可以替换基类并出现在基类能够出现的任何地方或者说如果我们把代码中使用基类的地方用它的派生类所代替代码还能正常工作。以下代码就违反了LSP定义。if (obj typeof Class1) { do something } else if (obj typeof Class2) { do something else }一键获取完整项目代码12345里氏替换原则(LSP)是使代码符合开闭原则的一个重要保证。同时LSP体现了类的继承原则如果一个派生类的对象可能会在基类出现的地方出现运行错误则该派生类不应该从该基类继承或者说应该重新设计它们之间的关系。动作正确性保证从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。示例里式替换原则为我们是否应该使用继承提供了判断的依据不再是简单地根据两者之间是否有相同之处来说使用继承。里式替换原则的引申意义子类可以扩展父类的功能但不能改变父类原有的功能。具体来说子类可以实现父类的抽象方法但不能覆盖父类的非抽象方法。子类中可以增加自己特有的方法。当子类的方法重载父类的方法时方法的前置条件即方法的输入/入参要比父类方法的输入参数更宽松。当子类的方法实现父类的方法时重载/重写或实现抽象方法的后置条件即方法的输出/返回值要比父类更严格或相等。下面举几个例子帮助更进一步理解LSP例:1Rectangle是矩形Square是正方形Square继承于Rectangle这样一看似乎没有问题。假如已有的系统中存在以下既有的业务逻辑代码void g(Rectangle r) { r.SetWidth(5); r.SetHeight(4); assert(r.GetWidth() * r.GetHeight()) 20); }一键获取完整项目代码123456则对应于扩展类Square在调用既有业务逻辑时Rectangle square new Square(); g(square);一键获取完整项目代码12时会抛出一个异常。这显然违反了LSP原则。说明这样的继承关系在这种业务逻辑下不应该使用。例2鲸鱼和鱼应该属于什么关系从生物学的角度看鲸鱼应该属于哺乳动物而不是鱼类。没错在程序世界中我们可以得出同样的结论。如果让鲸鱼类去继承鱼类就完全违背了Liskov替换原则。因为鱼作为基类很多特性是鲸鱼所不具备的例如通过腮呼吸以及卵生繁殖。那么二者是否具有共性呢 有那就是它们都可以在水中游泳从程序设计的角度来说它们都共同实现了一个支持游泳行为的接口。例:3运动员和自行车例子每个运动员都有一辆自行车如果按照下面设计很显然违反了LSP原则。class Bike { public: void Move( ); void Stop( ); void Repair( ); protected: int ChangeColor(int ); private: int mColor; };class Player : private Bike{public:void StartRace( );void EndRace( );protected:int CurStrength ( );private:int mMaxStrength;int mAge;};一键获取完整项目代码1234567891011121314151617181920212223里式替换原则的优点约束继承泛滥是开闭原则的一种体现。加强程序的健壮性同时变更时也可以做到非常好地提高程序的维护性、扩展性。降低需求变更时引入的风险。重构违反LSP的设计如果两个具体的类AB之间的关系违反了LSP 的设计假设是从B到A的继承关系那么根据具体的情况可以在下面的两种重构方案中选择一种创建一个新的抽象类C作为两个具体类的基类将AB的共同行为移动到C中来解决问题。从B到A的继承关系改为关联关系。对于矩形和正方形例子可以构造一个抽象的四边形类把矩形和正方形共同的行为放到这个四边形类里面让矩形和正方形都是它的派生类问题就OK了。对于矩形和正方形取width 和height 是它们共同的行为但是给width 和height 赋值两者行为不同因此这个抽象的四边形的类只有取值方法没有赋值方法。对于运动员和自行车例子可以采用关联关系来重构class Player { public: void StartRace( ); void EndRace( ); protected: int CurStrength ( ); private: int mMaxStrength; int mAge; Bike * abike; };一键获取完整项目代码123456789101112在进行设计的时候我们尽量从抽象类继承而不是从具体类继承。如果从继承等级树来看所有叶子节点应当是具体类而所有的树枝节点应当是抽象类或者接口。当然这只是一个一般性的指导原则使用的时候还要具体情况具体分析。在很多情况下在设计初期我们类之间的关系不是很明确LSP则给了我们一个判断和设计类之间关系的基准需不需要继承以及怎样设计继承关系。三、 迪米特原则最少知道原则Law of Demeter LoD迪米特原则Law of Demeter又叫最少知道原则Least Knowledge Principle可以简单说成talk only to your immediate friends只与你直接的朋友们通信不要跟“陌生人”说话。概念理解对于面向OOD来说又被解释为下面两种方式1一个软件实体应当尽可能少地与其他实体发生相互作用。2每一个软件单位对其他的单位都只有最少的知识而且局限于那些与本单位密切相关的软件单位。朋友圈的确定“朋友”条件当前对象本身this以参量形式传入到当前对象方法中的对象当前对象的实例变量直接引用的对象当前对象的实例变量如果是一个聚集那么聚集中的元素也都是朋友当前对象所创建的对象任何一个对象如果满足上面的条件之一就是当前对象的“朋友”否则就是“陌生人”。迪米特原则的优缺点迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖因此很容易使得系统的功能模块功能独立相互之间不存在或很少有依赖关系。迪米特原则不希望类直接建立直接的接触。如果真的有需要建立联系也希望能通过它的友元类来转达。因此应用迪米特原则有可能造成的一个后果就是系统中存在大量的中介类这些类之所以存在完全是为了传递类之间的相互调用关系这在一定程度上增加了系统的复杂度。例如购房者要购买楼盘A、B、C中的楼他不必直接到楼盘去买楼而是可以通过一个售楼处去了解情况这样就减少了购房者与楼盘之间的耦合如图所示。违反迪米特原则的设计与重构下面的代码在方法体内部依赖了其他类这严重违反迪米特原则class Teacher { public: void command(GroupLeader groupLeader) { listStudent listStudents new listStudent; for (int i 0; i 20; i) { listStudents.add(new Student()); } groupLeader.countStudents(listStudents); } }一键获取完整项目代码12345678910方法是类的一个行为类竟然不知道自己的行为与其他类产生了依赖关系Teacher类中依赖了Student类然而Student类并不在Teacher类的朋友圈中一旦Student类被修改了Teacher类是根本不知道的这是不允许的。正确的做法是class Teacher { public: void command(GroupLeader groupLeader) { groupLeader.countStudents(); } }class GroupLeader {private:listStudent listStudents;public:GroupLeader(listStudent _listStudents) {this.listStudents _listStudents;}void countStudents() {cout“女生数量是” listStudents.size() endl;}}一键获取完整项目代码123456789101112131415161718使用迪米特原则时要考虑的朋友间也是有距离的一个类公开的public属性或方法越多修改时涉及的面也就越大变更引起的风险扩散也就越大。因此为了保持朋友类间的距离在设计时需要反复衡量是否还可以再减少public方法和属性是否可以修改为private等。注意迪米特原则要求类“羞涩”一点尽量不要对外公布太多的public方法和非静态的public变量尽量内敛多使用private、protected等访问权限。是自己的就是自己的如果一个方法放在本类中既不增加类间关系也对本类不产生负面影响就放置在本类中。四、单一职责原则永远不要让一个类存在多个改变的理由。换句话说如果一个类需要改变改变它的理由永远只有一个。如果存在多个改变它的理由就需要重新设计该类。单一职责原则原则的核心含意是只能让一个类/接口/方法有且仅有一个职责。为什么一个类不能有多于一个以上的职责如果一个类具有一个以上的职责那么就会有多个不同的原因引起该类变化而这种变化将影响到该类不同职责的使用者不同用户一方面如果一个职责使用了外部类库则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。另一方面某个用户由于某个原因需要修改其中一个职责另外一个职责的用户也将受到影响他将不得不重新编译和配置。这违反了设计的开闭原则也不是我们所期望的。职责的划分既然一个类不能有多个职责那么怎么划分职责呢Robert.CMartin给出了一个著名的定义所谓一个类的一个职责是指引起该类变化的一个原因。如果你能想到一个类存在多个使其改变的原因那么这个类就存在多个职责。SRP违反例class Modem { void dial(String pno); //拨号 void hangup(); //挂断 void send(char c); //发送数据 char recv(); //接收数据 };一键获取完整项目代码123456乍一看这是一个没有任何问题的接口设计。但事实上这个接口包含了2个职责第一个是连接管理dialhangup另一个是数据通信sendrecv。很多情况下这2个职责没有任何共通的部分它们因为不同的理由而改变被不同部分的程序调用。所以它违反了SRP原则。下面的类图将它的2个不同职责分成2个不同的接口这样至少可以让客户端应用程序使用具有单一职责的接口让 ModemImplementation实现这两个接口。我们注意到ModemImplementation又组合了2个职责这不是我们希望的但有时这又是必须的。通常由于某些原因迫使我们不得不绑定多个职责到一个类中但我们至少可以通过接口的分割来分离应用程序关心的概念。事实上这个例子一个更好的设计应该是这样的如图例如考虑下图的设计。Retangle类具有两个方法如图。一个方法把矩形绘制在屏幕上另一个方法计算矩形的面积。有两个不同的Application使用Rectangle类如上图。一个是计算几何面积的Rectangle类会在几何形状计算方面给予它帮助。另一Application实质上是绘制一个在舞台上显示的矩形。这一设计违反了单一职责原则。Rectangle类具有了两个职责第一个职责是提供一个矩形形状几何数据模型第二个职责是把矩形显示在屏幕上。对于SRP的违反导致了一些严重的问题。首先我们必须在计算几何应用程序中包含核心显示对象的模块。其次如果绘制矩形Application发生改变也可能导致计算矩形面积Application发生改变导致不必要的重新编译和不可预测的失败。一个较好的设计是把这两个职责分离到下图所示的两个完全不同的类中。这个设计把Rectangle类中进行计算的部分移到GeometryRectangle类中。现在矩形绘制方式的改变不会对计算矩形面积的应用产生影响了。使用单一职责原则的理由单一职责原则从职责改变理由的侧面上为我们对类接口的抽象的颗粒度建立了判断基准在为系统设计类接口的时候应该保证它们的单一职责性。降低了类的复杂度、提高类的可读性提高系统的可维护性、降低变更引起的风险五、 接口分隔原则Interface Segregation Principle ISP不能强迫用户去依赖那些他们不使用的接口。概念理解换句话说使用多个专门的接口比使用单一的总接口总要好。它包含了2层意思接口的设计原则接口的设计应该遵循最小接口原则不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到则说明该接口过胖应该将其分割成几个功能专一的接口。接口的依赖继承原则如果一个接口a继承另一个接口b则接口a相当于继承了接口b的方法那么继承了接口b后的接口a也应该遵循上述原则不应该包含用户不使用的方法。 反之则说明接口a被b给污染了应该重新设计它们的关系。如果用户被迫依赖他们不使用的接口当接口发生改变时他们也不得不跟着改变。换而言之一个用户依赖了未使用但被其他用户使用的接口当其他用户修改该接口时依赖该接口的所有用户都将受到影响。这显然违反了开闭原则也不是我们所期望的。总而言之接口分隔原则指导我们一个类对一个类的依赖应该建立在最小的接口上建立单一接口不要建立庞大臃肿的接口尽量细化接口接口中的方法尽量少违反ISP原则的设计与重构下面我们举例说明怎么设计接口或类之间的关系使其不违反ISP原则。假如有一个Door有lockunlock功能另外可以在Door上安装一个Alarm而使其具有报警功能。用户可以选择一般的Door也可以选择具有报警功能的Door。有以下几种设计方法ISP原则的违反例一在Door接口里定义所有的方法。但这样一来依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。违反了ISP原则。ISP原则的违反例二在Alarm接口定义alarm方法在Door接口定义lockunlock方法Door接口继承Alarm接口。跟方法一一样依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。违反了ISP原则。遵循ISP原则的例一通过多重继承实现在Alarm接口定义alarm方法在Door接口定义lockunlock方法。接口之间无继承关系。CommonDoor实现Door接口AlarmDoor有2种实现方案1同时实现Door和Alarm接口。2继承CommonDoor并实现Alarm接口。第2种方案更具有实用性。这样的设计遵循了ISP设计原则。遵循ISP原则的例二通过关联实现在这种方法里AlarmDoor实现了Alarm接口同时把功能lock和unlock委让给CommonDoor对象完成。这种设计遵循了ISP设计原则。接口分隔原则的优点和适度原则接口分隔原则从对接口的使用上为我们对接口抽象的颗粒度建立了判断基准在为系统设计接口的时候使用多个专门的接口代替单一的胖接口。符合高内聚低耦合的设计思想从而使得类具有很好的可读性、可扩展性和可维护性。注意适度原则接口分隔要适度避免产生大量的细小接口。单一职责原则和接口分隔原则的区别单一职责强调的是接口、类、方法的职责是单一的强调职责方法可以多针对程序中实现的细节接口分隔原则主要是约束接口针对抽象、整体框架。六、 依赖倒置原则Dependency Inversion Principle DIPA. 高层模块不应该依赖于低层模块二者都应该依赖于抽象B. 抽象不应该依赖于细节细节应该依赖于抽象 C.针对接口编程不要针对实现编程。概念理解依赖在程序设计中如果一个模块a使用/调用了另一个模块b我们称模块a依赖模块b。高层模块与低层模块往往在一个应用程序中我们有一些低层次的类这些类实现了一些基本的或初级的操作我们称之为低层模块另外有一些高层次的类这些类封装了某些复杂的逻辑并且依赖于低层次的类这些类我们称之为高层模块。依赖倒置Dependency Inversion面向对象程序设计相对于面向过程结构化程序设计而言依赖关系被倒置了。因为传统的结构化程序设计中高层模块总是依赖于低层模块。问题的提出Robert C. Martin氏在原文中给出了“Bad Design”的定义系统很难改变因为每个改变都会影响其他很多部分。当你对某地方做一修改系统的看似无关的其他部分都不工作了。系统很难被另外一个应用重用因为很难将要重用的部分从系统中分离开来。导致“Bad Design”的很大原因是“高层模块”过分依赖“低层模块”。一个良好的设计应该是系统的每一部分都是可替换的。如果“高层模块”过分依赖“低层模块”一方面一旦“低层模块”需要替换或者修改“高层模块”将受到影响另一方面高层模块很难可以重用。问题的解决为了解决上述问题Robert C. Martin氏提出了OO设计的Dependency Inversion Principle (DIP) 原则。DIP给出了一个解决方案在高层模块与低层模块之间引入一个抽象接口层。High Level Classes高层模块 -- Abstraction Layer抽象接口层 -- Low Level Classes低层模块抽象接口是对低层模块的抽象低层模块继承或实现该抽象接口。这样高层模块不直接依赖低层模块而是依赖抽象接口层。抽象接口也不依赖低层模块的实现细节而是低层模块依赖继承或实现抽象接口。类与类之间都通过抽象接口层来建立关系。依赖倒置原则的违反例和重构示例考虑一个控制熔炉调节器的软件。该软件从一个IO通道中读取当前的温度并通过向另一个IO通道发送命令来指示熔炉的开或者关。温度调节器的简单算法const byte THERMONETER0x86; const byte FURNACE0x87; const byte ENGAGE1; const byte DISENGAGE0;void Regulate(double minTemp,double maxTemp){for(;{while (in(THERMONETER) minTemp)wait(1);out(FURNACE,ENGAGE);while (in(THERMONETER) lt; maxTemp) wait(1); out(FURNACE,DISENGAGE); }}一键获取完整项目代码123456789101112131415161718算法的高层意图是清楚的但是实现代码中却夹杂着许多低层细节。这段代码根本不能重用于不同的控制硬件。由于代码很少所以这样做不会造成太大的损害。但是即使是这样使算法失去重用性也是可惜的。我们更愿意倒置这种依赖关系。图中显示了 Regulate 函数接受了两个接口参数。Thermometer 接口可以读取而 Heater 接口可以启动和停止。Regulate 算法需要的就是这些。这就倒置了依赖关系使得高层的调节策略不再依赖于任何温度计或者熔炉的特定细节。该算法具有很好的可重用性。通用的调节器算法void Regulate(Thermometer t, Heater h, double minTemp, double maxTemp) { for(;;) { while (t.Read() minTemp) wait(1); h.Engate();while (t.Read() lt; maxTemp) wait(1); h.Disengage(); }}一键获取完整项目代码1234567891011121314怎么使用依赖倒置原则1. 依赖于抽象任何变量都不应该持有一个指向具体类的指针或引用。如class class1{ class2* cls2 new class2(); } class class2{ ....... }一键获取完整项目代码123456任何类都不应该从具体类派生。2. 设计接口而非设计实现使用继承避免对类的直接绑定抽象类/接口 倾向于较少的变化抽象是关键点它易于修改和扩展不要强制修改那些抽象接口/类例外有些类不可能变化在可以直接使用具体类的情况下不需要插入抽象层如字符串类3. 避免传递依赖避免高层依赖于低层使用继承和抽象类来有效地消除传递依赖依赖倒置原则的优点可以减少类间的耦合性、提高系统稳定性提高代码可读性和可维护性可降低修改程序所造成的风险。七、 组合/聚合复用原则Composite/Aggregate Reuse Principle CARP尽量使用组合/聚合不要使用类继承。概念理解即在一个新的对象里面使用一些已有的对象使之成为新对象的一部分新对象通过向这些对象的委派达到复用已有功能的目的。就是说要尽量的使用合成和聚合而不是继承关系达到复用的目的。组合和聚合都是关联的特殊种类。聚合表示整体和部分的关系表示“拥有”。组合则是一种更强的“拥有”部分和整体的生命周期一样。组合的新的对象完全支配其组成部分包括它们的创建和湮灭等。一个组合关系的成分对象是不能与另一个组合关系共享的。组合是值的聚合Aggregation by Value而一般说的聚合是引用的聚合Aggregation by Reference。在面向对象设计中有两种基本的办法可以实现复用第一种是通过组合/聚合第二种就是通过继承。什么时候才应该使用继承只有当以下的条件全部被满足时才应当使用继承关系1派生类是基类的一个特殊种类而不是基类的一个角色也就是区分Has-A和Is-A。只有Is-A关系才符合继承关系Has-A关系应当用聚合来描述。2永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话就不要使用继承。3派生类具有扩展基类的责任而不是具有置换掉override或注销掉Nullify基类的责任。如果一个派生类需要大量的置换掉基类的行为那么这个类就不应该是这个基类的派生类。4只有在分类学角度上有意义时才可以使用继承。总的来说如果语义上存在着明确的Is-A关系并且这种关系是稳定的、不变的则考虑使用继承如果没有Is-A关系或者这种关系是可变的使用组合。另外一个就是只有两个类满足里氏替换原则的时候才可能是Is-A 关系。也就是说如果两个类是Has-A关系但是设计成了继承那么肯定违反里氏替换原则。错误的使用继承而不是组合/聚合的一个常见原因是错误的把Has-A当成了Is-A 。Is-A代表一个类是另外一个类的一种Has-A代表一个类是另外一个类的一个角色而不是另外一个类的特殊种类。看一个例子如果我们把“人”当成一个类然后把“雇员”“经理”“学生”当成是“人”的派生类。这个的错误在于把 “角色” 的等级结构和 “人” 的等级结构混淆了。“经理”“雇员”“学生”是一个人的角色一个人可以同时拥有上述角色。如果按继承来设计那么如果一个人是雇员的话就不可能是学生这显然不合理。正确的设计是有个抽象类 “角色”“人”可以拥有多个“角色”聚合“雇员”“经理”“学生”是“角色”的派生类。通过组合/聚合复用的优缺点优点1.新对象存取子对象的唯一方法是通过子对象的接口。2.这种复用是黑箱复用因为子对象的内部细节是新对象所看不见的。3.这种复用更好地支持封装性。4.这种复用实现上的相互依赖性比较小。5.每一个新的类可以将焦点集中在一个任务上。6.这种复用可以在运行时间内动态进行新对象可以动态的引用与子对象类型相同的对象。7.作为复用手段可以应用到几乎任何环境中去。缺点: 就是系统中会有较多的对象需要管理。通过继承来进行复用的优缺点优点新的实现较为容易因为基类的大部分功能可以通过继承的关系自动进入派生类。修改和扩展继承而来的实现较为容易。缺点继承复用破坏封装性因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的所以这种复用是透明的复用又称“白箱”复用。如果基类发生改变那么派生类的实现也不得不发生改变。从基类继承而来的实现是静态的不可能在运行时间内发生改变没有足够的灵活性。