面向对象(类)的设计原则
转载于: 面向对象设计的七大设计原则详解
类的设计原则有其中:开闭原则、里氏代换原则、迪米特原则(最少知道原则)、单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则
1. 开闭原则(The Open-Closed Principle ,OCP)
软件实体(模块,类,方法等)应该对扩展开放,对修改关闭。
根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。
扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的。
例子:
左边的设计是直接依赖实际的类,不是对扩展开放的。
右边的设计是良好的设计:
Client对于Server提供的接口是封闭的;
Client对于Server的新的接口实现方法的扩展是开放的。
系统设计需要遵循开闭原则的原因
稳定性。开闭原则要求扩展功能不修改原来的代码,这可以让软件系统在变化中保持稳定。
扩展性。开闭原则要求对扩展开放,通过扩展提供新的或改变原有的功能,让软件系统具有灵活的可扩展性。 遵循开闭原则的系统设计,可以让软件系统可复用,并且易于维护。
2. 里氏替换原则(Liskov Substitution Principle ,LSP)
所有引用基类的地方必须能透明地使用其派生类的对象。
类的继承原则:如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,或者说,应该重新设计它们之间的关系。
动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。
里式替换原则的引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能。
具体来说:
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的方法时(重载/重写或实现抽象方法)的后置条件(即方法的输出/返回值)要比父类更严格或相等。
里式替换原则的优点
约束继承泛滥,是开闭原则的一种体现。
加强程序的健壮性,同时变更时也可以做到非常好地提高程序的维护性、扩展性。降低需求变更时引入的风险。
3. 迪米特原则(最少知道原则)(Law of Demeter ,LoD)
迪米特原则(Law of Demeter)又叫最少知道原则(Least Knowledge Principle),可以简单说成:talk only to your immediate friends,只与你直接的朋友们通信,不要跟“陌生人”说话。
对于面向OOD来说,又被解释为下面两种方式:
1)一个软件实体应当尽可能少地与其他实体发生相互作用。
2)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
朋友圈的确定 “朋友”条件:
当前对象本身(this)
以参量形式传入到当前对象方法中的对象
当前对象的实例变量直接引用的对象
当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
当前对象所创建的对象
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
迪米特原则不希望类直接建立直接的接触。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特原则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度。
4. 单一职责原则
永远不要让一个类存在多个改变的理由。
换句话说,如果一个类需要改变,改变它的理由永远只有一个。如果存在多个改变它的理由,就需要重新设计该类。
单一职责原则原则的核心含意是:只能让一个类/接口/方法有且仅有一个职责。
如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,而这种变化将影响到该类不同职责的使用者(不同用户):
一方面,如果一个职责使用了外部类库,则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。
另一方面,某个用户由于某个原因需要修改其中一个职责,另外一个职责的用户也将受到影响,他将不得不重新编译和配置。 这违反了设计的开闭原则,也不是我们所期望的。
5. 接口分隔原则(Interface Segregation Principle ,ISP)
不能强迫用户去依赖那些他们不使用的接口。
换句话说,使用多个专门的接口比使用单一的总接口总要好。
它包含了2层意思:
接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。
接口的依赖(继承)原则:如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b后的接口a也应该遵循上述原则:不应该包含用户不使用的方法。 反之,则说明接口a被b给污染了,应该重新设计它们的关系。
总而言之,接口分隔原则指导我们:
一个类对一个类的依赖应该建立在最小的接口上
建立单一接口,不要建立庞大臃肿的接口
尽量细化接口,接口中的方法尽量少
6. 依赖倒置原则(Dependency Inversion Principle ,DIP)
A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
B. 抽象不应该依赖于细节,细节应该依赖于抽象
C.针对接口编程,不要针对实现编程。
高层模块与低层模块:往往在一个应用程序中,我们有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外有一些高层次的类,这些类封装了某些复杂的逻辑,并且依赖于低层次的类,这些类我们称之为高层模块。
7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)
尽量使用组合/聚合,不要使用类继承。
即在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的。
组合和聚合都是关联的特殊种类。
聚合表示整体和部分的关系,表示“拥有”。组合则是一种更强的“拥有”,部分和整体的生命周期一样。
组合的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个组合关系的成分对象是不能与另一个组合关系共享的。
组合是值的聚合(Aggregation by Value),而一般说的聚合是引用的聚合(Aggregation by Reference)。
在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。