OO七大设计原则
一、单一职责原则(Single Responsibility Principle,SRP)
含义: 1、避免相同的职责分散到不同的类中 2、避免一个类承担太多职责 作用: 1、可以减少类之间的耦合 2、提高类的复用性
一个类,只有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。
问题由来
之所以会出现单一职责原则就是因为在软件设计时会出现以下类似场景:
T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。也就是说职责P1和P2被耦合在了一起。
违反SRP原则的重构可采取设计模式:外观模式(Facade)、代理模式(Proxy)或数据访问对象(DAO)。
二、开放封闭原则(Open-Close Principle,OCP)
对扩展是开放的,对修改是封闭的。实现开放封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。
一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。。
对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
只有依赖于抽象。实现开放封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以对修改就是封闭的;而通过面向对象的继承和对多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。这是实施开放封闭原则的基本思路,同时这种机制是建立在两个基本的设计原则的基础上,这就是Liskov替换原则和合成/聚合复用原则。
例如.把一个类的功能抽象出来.形成一个抽象接口.然后对该接口编程.这样当需要扩展时只要从该接口派生一个
违反OCP的例子
此例子是书上的例子,比较有代表性,使用的是C语言,并且没有遵循OCP的设计原则。
void DrawAllShape(ShapePointer list[], int n)
{
int i;
for (i = 0; i < n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*)s);
break;
case circle:
DrawSquare((struct Circle*)s);
break;
default:
break;
}
}
这个例子符合OCP么?只需要看它是如何应对变化的。
假如我们需要新增一个三角形,就必须在这段代码的switch语句中新增case。这样就导致我们修改了源代码,也就违反了OCP所说的对修改封闭。
这个例子所带来的危害也是显而易见的:
新增一种类型,都需要新增switch(或者if/else)。
如果switch(if/else)过于复杂,那么要理解和新增新的形状就非常困难了。
对于形状的类型,也需要对ShapeType enum进行修改,这样就会到这所有用到ShapeType enum的地方,都进行重新编译。也就是新增一个类型,所有类型
相关的代码都要重新被编译一次。
class Shape
{
public:
virtual void Draw() const = 0;
}
class Square: public Shape
{
public:
virtual void Draw() const;
}
class Circle: public Shape
{
public:
virtual void Draw() const;
}
void DrawAllShapes(vector<Shape*>& list)
{
vector<Shape*>::iterator I;
for (i = list.begin(): i != list.end(); i++)
{
(*i)->Draw();
}
}
这段代码究竟是不是符合OCP的原则,我们新增一个新的类型来试试。
假如需要增加一个三角形,我们只需要增加一个继承自Shape的Triangle类型。在使用
DrawAllShapes的地方,将Triangle的对象存入list即可。从这里可以看出,我们并未对上述的代码逻辑进行修改。而是从Shape扩展出Triangle的类型。
也就实现了对修改封闭,对扩展开放的原则。
三、里氏替换原则(Liskov Substitution Principle,LSP)
1、里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。(容易理解:只要有父类出现的地方,都可以用子类来替代)2、LSP主要是针对继承的设计原则 里氏替换原则包含以下4层含义:
* 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象(已实现)方法。
public class C {
public int func(int a, int b){
return a+b;
}
}
public class C1 extends C{
@Override
public int func(int a, int b) {
return a-b;
}
}
public class Client{
public static void main(String[] args) {
C c = new C1();
System.out.println("2+1=" + c.func(2, 1));
}
}
运行结果:2+1=1
上面的运行结果明显是错误的。类C1继承C,后来需要增加新功能,类C1并没有新写一个方法,而是直接重写了父类C的func方法,违背里氏替换原则,引用父类的地方并不能透明的使用子类的对象,导致运行结果出错。
* 子类中可以增加自己特有的方法。
* 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
代码示例
import java.util.HashMap;
public class Father {
public void func(HashMap m){
System.out.println("执行父类...");
}
}
import java.util.Map;
public class Son extends Father{
public void func(Map m){//方法的形参比父类的更宽松
System.out.println("执行子类...");
}
}
import java.util.HashMap;
public class Client{
public static void main(String[] args) {
Father f = new Son();//引用基类的地方能透明地使用其子类的对象。
HashMap h = new HashMap();
f.func(h);
}
}
运行结果:执行父类…
我们应当主意,子类并非重写了父类的方法,而是重载了父类的方法。因为子类和父类的方法的输入参数是不同的。子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap类型时,只会执行父类的方法,不会执行fAD类的重载方法。这符合里氏替换原则。
* 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
下述代码由于子类型无法替换父类型,导致DrawShape函数需要知道Shap的所有子类型。违反LSP后导致违反OCP。
四、依赖倒置原则(Dependence Inversion Principle,DIP)
含义: 1、上层模块不应该依赖于下层模块,它们共同依赖于一个抽象(父类不能依赖子类,它们都要依赖抽象类) 2、抽象不能依赖于具体,具体应该要依赖于抽象
高层模块不应该依赖底层模块,都应该依赖于抽象;抽象不应该依赖于具体,具体依赖于抽象如何遵守:
1、每个较高的层次类都为它所需要的服务提出一个接口声明,较低层次类实现这个接口
2、每个高层类都通过该抽象接口使用服务
五、接口隔离原则(Interface Segregation Principle,ISP)
1、客户端不应该依赖它不需要的接口
那依赖什么呢?依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除,那就需要对接口进行细化,保证其纯洁性。
2、类间的依赖关系应该建立在最小的接口上
它要求是最小的接口,也是要求接口细化,接口纯洁。
我们把两种定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口。再通俗的说就是接口尽量细化,同时接口中的方法尽量少。
原理:使用多个专门的接口,而不使用单一的总接口,即类不应该依赖那些它不需要的接口。
模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来
应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。在面对对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便。
很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。
接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
---------------------
六、合成/聚合复用原则CARP(Composite/Aggregate Reuse Principle)
在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过这些向对象的委派达到复用已有功能的目的.这个设计原则有另一个简短的表述:要尽量使用合成/聚合,尽量不要使用继承.
只有当以下的条件全部被满足时,才应当使用继承关系。
1. 子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分“Has-A”和“Is-A”.只有“Is-A”关系才符合继承关系,“Has-A”关系应当使用聚合来描述。
2 .永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。
3 .子类具有扩展超类的责任,而不是具有置换掉或注销掉超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。
超类:被继承的类一般称为“超类”或“父类”,继承的类称为“子类”
“Is-A”是严格的分类学意义上的定义,意思是一个类是另一个类的“一种”。
“Has-A”是表示某一个角色具有某一项责任。
继承是“Is-A”关系。合成/聚合是“Has-A”关系。
什么是合成?
. 合成表示一种强的拥有关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样,打个比方:人有两个胳膊,胳膊和人就是部分和整体的关系,人去世了,那么胳膊也就没用了,也就是说胳膊和人的生命周期是相同的 体现的是一种contains-a的关系
. 合成关系用实心的菱形+实线来表示
什么是聚合?
. 聚合表示一种弱的拥有关系,体现的是A对象可以包含B对象,但是B对象并不是A对象的一部分,打个比方:人是群居动物,所以每个人属于一个人群,一个人群可以有多个人,所以人群和人是聚合的关系 体现的是整体与部分、拥有的关系,即has-a的关系
. 聚合关系用空心的菱形+实线来表示
合成/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
Is-A继承关系:“表示类与类之间的继承关系、接口与接口之间的继承的关系以及类对接口实现的关系”
Has-A合成关系:“是关联关系的一种,是整体和部分(通常为一个私有的变量)之间的关系,并且代表的整体对象负责构建和销毁代表部分对象,代表部分的对象不能共享”。
Use-A依赖关系:“是类与类(通常为函数的参数)之间的连接,依赖总是单向的”。
七、迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP)。
迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。于是就提出了迪米特法则。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。
---------------------
一个对象应该对其他对象有最少的了解。
一个对象应该对其他对象了解最少迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用性才可以提高。
就是如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用.