【设计模式学习笔记】设计模式七大原则介绍(基于C++实现)
一、开放封闭原则
OCP,Open For Extension Closed For Modification Principle,简称开闭原则。开闭原则是指软件实体是可以扩展的,但是不可修改。也就是说,模块和函数是对扩展(提供方)开放的,对修改(使用方)关闭的,对于一个新的需求,对软件的改动应该是通过增加代码来实现的,而不是通过改动代码实现的。开闭原则是面向对象的核心,是最基础、最重要的设计原则,开发过程中,应该把可能会频繁变动的部分抽象出来,当需要变动时,只需要去实现抽象即可,也就是面向抽象编程。对于C++类来说,对类的改动是通过增加代码实现的,而不是修改代码实现的,通过虚基类的继承和虚函数的实现来完成一个类功能的扩充,这也是多态在设计模式中重要地位的体现。
举例来说,假如我们要创建一个迪迦奥特曼类,迪迦奥特曼有三种形态,最简单的方式就是在一个类中实现,每次都在类中增删查改
//第一层次:直接修改类来实现增加功能
class TigaUltraman1
{
public:
void RedForm() //红色形态
{
cout << "红色形态的迪迦奥特曼" << endl;
}
void BlueForm() //蓝色形态
{
cout << "蓝色形态的迪迦奥特曼" << endl;
}
void CompreForm() //综合形态
{
cout << "综合形态的迪迦奥特曼" << endl;
}
};
int main()
{
//1. 直接修改迪迦奥特曼类来增加不同形态
TigaUltraman1* u1 = new TigaUltraman1;
u1->RedForm();
u1->BlueForm();
u1->CompreForm();
delete u1;
cout << endl;
}
那么这样的话显然不满足开闭原则,实际上应该定义一个抽象类,这个抽象类只提供一个统一的接口,当需要增加功能时只需要继承这个抽象类,并实现抽象方法即可。
//第二层次,创建一个抽象类,通过继承实现形态扩充
class TigaUltraman //迪迦奥特曼抽象类
{
public:
virtual void uForm() = 0;
};
class RedTigaUltraman : public TigaUltraman
{
public:
virtual void uForm()
{
cout << "红色形态的迪迦奥特曼" << endl;
}
};
class BlueTigaUltraman : public TigaUltraman
{
public:
virtual void uForm()
{
cout << "蓝色形态的迪迦奥特曼" << endl;
}
};
class CompreTigaUltraman : public TigaUltraman
{
public:
virtual void uForm()
{
cout << "综合形态的迪迦奥特曼" << endl;
}
};
int main()
{
//2. 使用继承
TigaUltraman* u2 = NULL;
u2 = new RedTigaUltraman;
u2->uForm();
delete u2;
u2 = new BlueTigaUltraman;
u2->uForm();
delete u2;
u2 = new CompreTigaUltraman;
u2->uForm();
delete u2;
cout << endl;
}
更进一步,我们可以提供一个接口,直接调用接口,把各种实现类的对象传递给抽象类的指针并产生多态。即使是子类的子类也可以传给抽象类产生多态。
//第三层次:使用一个统一接口
void get_form(TigaUltraman* u)
{
u->uForm();
}
//增加功能,进化版的综合形态
class EvolutionCompreTigaUltraman : public CompreTigaUltraman
{
public:
virtual void uForm()
{
cout << "进化版的综合形态的迪迦奥特曼" << endl;
}
};
int main()
{
//3. 使用统一接口
u2 = new RedTigaUltraman;
get_form(u2);
delete u2;
BlueTigaUltraman* u3 = new BlueTigaUltraman;
get_form(u3);
delete u3;
CompreTigaUltraman u4;
get_form(&u4);
cout << endl;
u2 = new EvolutionCompreTigaUltraman;
get_form(u2);
delete u2;
cout << endl;
}
二、单一职责原则
SRP,Single Responsibility Principle,单一职责原则。对类来说,类的职责应该是单一的,一个类只能对外提供一种功能。换句话说,变动这个类的理由或动机只能有一个,如果第二个改动类的理由,就不是单一职责。单一职责相当于降低了各种职责的耦合度,如果一个类负责多个职责,那么改动类的某一职责时,可能会影响到类行使其他职责的能力。
三、依赖倒置原则
DIP,Dependence Inversion Principle,依赖倒置原则。抽象不应该依赖于细节,细节应该依赖于抽象。换句话说,依赖于抽象接口,而不是依赖于具体的类的实现,也就是面向抽象接口编程。依赖倒置原则是面向对象编程的标志,在具体软件设计时,上层模块不应该依赖于底层模块,底层模块更不应该依赖上层模块,而是上层模块和底层模块都向中间靠拢,共同依赖于二者中间的抽象接口层。整个软件程序设计的依赖关系应该终止于抽象接口层,上层和底层互不关心,甚至使用什么编程语言都不关心。抽象接口层提供一个标准或者协议,它对上提供访问的接口,对下提供实现的标准,抽象接口层本身不执行任何操作,具体的功能由它的实现去完成。
举例来说,假如我们要组装一台电脑,现在要选择硬盘、内存、屏幕。那么电脑类要集成硬盘、内存、屏幕这些组件,但是我们希望电脑类和组件类之间不应该是相互依赖的关系,我们就可以直接给出一套接口,各个组件厂商只要实现这些抽象接口就可以装入我们的电脑中。
//各个组件的抽象类
class DiskInterface
{
public:
virtual void infomation() = 0;
};
class MemoryInterface
{
public:
virtual void infomation() = 0;
};
class ScreenInterface
{
public:
virtual void infomation() = 0;
};
电脑类定义如下
class Computer
{
public:
Computer(DiskInterface* disk, MemoryInterface* memory, ScreenInterface* screen)
{
this->disk = disk;
this->memory = memory;
this->screen = screen;
}
public:
void get_information()
{
this->disk->infomation();
this->memory->infomation();
this->screen->infomation();
}
private:
DiskInterface* disk; //使用指针而不能使用变量
MemoryInterface* memory;
ScreenInterface* screen;
};
各个厂商根据组件的抽象类去实现,来入围电脑类
//各厂商直接继承抽象类,来实现
class InterDisk : public DiskInterface
{
public:
virtual void infomation()
{
cout << "因特尔硬盘" << endl;
}
};
class WDMemory : public MemoryInterface
{
public:
virtual void infomation()
{
cout << "西部数据的内存条" << endl;
}
};
class HPScreen : public ScreenInterface
{
public:
virtual void infomation()
{
cout << "惠普的屏幕" << endl;
}
};
这样组件类和电脑类都依赖于抽象接口层,两者都向接口层靠近,这就是面向接口编程,也就是我们的依赖倒置原则。我们直接把各个组件的实现类定义对象并传到电脑类中即可
{
InterDisk* idisk = new InterDisk;
WDMemory* wdmem = new WDMemory;
HPScreen* hpscr = new HPScreen;
SamScreen* samscr = new SamScreen;
Computer* c1 = new Computer(idisk, wdmem, hpscr); //使用惠普的屏幕
c1->get_information();
delete c1;
delete samscr;
delete hpscr;
delete wdmem;
delete idisk;
}
假如后来,三星也想入围这个电脑,那么三星直接去实现屏幕的抽象类即可
class SamScreen : public ScreenInterface
{
public:
virtual void infomation()
{
cout << "三星的屏幕" << endl;
}
};
我们再直接把三星的屏幕传入电脑类即可
{
Computer* c2 = new Computer(idisk, wdmem, samscr); //使用三星屏幕
c2->get_information();
}
这就是面向接口编程。
四、接口隔离原则
ISP,Interface Segegation Principle,接口隔离原则。一个接口对外只提供一种功能,不同功能应该通过不同的接口提供,而不是把多种功能都封装到一个接口中,否则的话,可能有的客户只需要功能A不需要功能B,但是提供A功能的接口内还封装了功能B,这就造成了客户被迫依赖某种他们不需要的功能。
五、里氏替换原则
LSP,Liskov Substitution Principle,里氏替换原则。任何地方出现的抽象类,都可以使用该抽象类的实现类来代替,这其实类似于C++中的类型兼容性原则,所有基类出现的地方都可以用子类对象来代替。实际上,继承增强了类与类之间的耦合性,在继承中应该尽量不要重写(覆盖)基类中的非抽象方法,子类可以有自己的方法,但是不能重新定义或修改基类的方法,否则如果基类中发生改变,所有的子类都可能受影响,应该尽量使用组合或者聚合而不是继承。其实,准确来说,应该是尽量不要继承可实例化的类(非抽象类),而应该是从抽象类中继承。
class parent
{
public:
virtual void function1() = 0;
void function2()
{
cout << "parent function 2" << endl;
}
};
class child1 : public parent
{
public:
virtual void function1()
{
cout << "function 1" << endl;
}
void function2()
{
cout << "child function 2" << endl;
}
};
举例来说,应该去实现function1()这样的抽象方法,而不能去覆盖基类的方法function2()。当子类覆盖或实现基类的方法时,方法的前置条件(形参)要比基类方法的输入参数宽松;当子类实现父类的抽象方法时,方法的后置条件(方法返回值)要比基类更严格。
六、合成复用原则
CARP,Composite/Aggregate Reuse Principle,优先使用对象组合而不是继承原则。使用继承的时候,基类的变化可能会影响到子类,而如果使用组合/聚合就可以降低这种依赖关系。组合/聚合降低了类与类之间的耦合性,一个类的变化对另一个类的影响相对较小,如果要使用继承,必须要遵守里氏替换原则。
七、迪米特法则
LOD,Law of Demeter,迪米特法则,也叫做最少知道原则(The Least Knowledge Principle)。一个类对于其它类知道的越少越好,只和朋友说话,不要和陌生人说话,这里的朋友是指作为成员变量、方法输入输出参数的类(朋友类),如果是出现在一个方法内部的类就不是朋友类。迪米特法则降低了耦合度,各个模块之间通过一个接口来实现调用,而模块之间不需要知道对方的内部实现逻辑,并且一个模块的内部改变也不会影响到另一个模块(黑盒原理)。如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。
朋友圈确定:
- 当前对象本身;
- 以参量形式传入到当前对象方法中的对象;
- 当前对象的实例变量直接引用的对象;
- 当前对象的实例变量如果是一个聚集,那么聚集中的元素都是朋友;
- 当前对象所创建的对象;
满足条件之一就是朋友,否则就是陌生人。