【设计模式学习笔记】设计模式七大原则介绍(基于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)。一个类对于其它类知道的越少越好,只和朋友说话,不要和陌生人说话,这里的朋友是指作为成员变量、方法输入输出参数的类(朋友类),如果是出现在一个方法内部的类就不是朋友类。迪米特法则降低了耦合度,各个模块之间通过一个接口来实现调用,而模块之间不需要知道对方的内部实现逻辑,并且一个模块的内部改变也不会影响到另一个模块(黑盒原理)。如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。

朋友圈确定:

  1. 当前对象本身;
  2. 以参量形式传入到当前对象方法中的对象;
  3. 当前对象的实例变量直接引用的对象;
  4. 当前对象的实例变量如果是一个聚集,那么聚集中的元素都是朋友;
  5. 当前对象所创建的对象;

满足条件之一就是朋友,否则就是陌生人。

posted @ 2022-04-14 00:15  Mindtechnist  阅读(11)  评论(0编辑  收藏  举报  来源