浅谈设计模式六大原则


前言

早就想写一下设计模式的话题,但碍于琐事,便未能如愿,今终如愿。
编程不仅是一门技术,更是一门艺术,任何经努力思考后码出的Code,虽达不到如Nginx这等史诗级别框架的艺术高度,但对创作者而言,是甜于甘露的。


一、设计模式是什么?

将一个人的编程能力类比于武侠小说中武者的功夫,那么学会一门编程语言代表着你已经有了成为绝世高手的体魄基础(能敲代码);学习了算法,能让你在比武之时,用更少的内力做更多的事(用更少的、更具有智慧的代码,更高效的达到目的);学习的框架越多,代表你学习的武功种类越多(能做的事情越多);而学习了设计模式,你的项目将会更健壮,层次更清晰(整体架构设计更为灵活,易于拓展)。设计模式是先驱们在实际开发中,经过不断实践得出的软件设计经验。建议初学者,在有一定的面向对象的基础的前提下,再去学习设计模式,这样能避免许多不必要的挫败感。

二、设计模式六大原则

1.开闭原则(总原则)

对拓展开放,但对修改封闭(但一般不可能做到完完全全对修改封闭)。简而言之就是,对于新增的需求,不应该建立在修改原来代码的基础上,而是尽可能在不修改已有代码的前提下新增代码,这就是设计模式的总原则,可用工厂、策略等模式组合来实现该原则,值得注意的是,简单工厂模式违背了该原则。
开闭原则实例:

class Game {
    
     
public:
	std::string virtual play_game() {
    
     return std::string(); }
};

//游戏类型是引起变化的原因 因此我们需要将它抽象出来

class LOL :public Game {
    
    
public:
	std::string play_game()override
	{
    
    
		return std::string("this is LOL!");
	}
};

class DNF :public Game {
    
    
public:
	std::string play_game()override
	{
    
    
		return std::string("this is DNF!");
	}
};

class CF :public Game {
    
    
public:
	std::string play_game()override
	{
    
    
		return std::string("this is CF!");
	}
};

//此处的定义建立工厂的接口
__interface Factory
{
    
    
public:
	Game* Create_Factory();
};

//每个工厂类生成对应的游戏对象

class LOL_Factory :public Factory{
    
    
public:
	Game* Create_Factory(){
    
    
		return new LOL();
	}
};

class CF_Factory :public Factory {
    
    
public:
	Game* Create_Factory() {
    
    
		return new CF();
	}
};

class DNF_Factory :public Factory {
    
    
public:
	Game* Create_Factory() {
    
    
		return new DNF();
	}
};

void test()
{
    
    
	//若要换为DNF游戏 只需将CF_Factory改为DNF_Factory即可
	auto factory = new CF_Factory();
	auto game = factory->Create_Factory();
	std::cout << game->play_game() << std::endl;
	delete game;
	delete factory;
}

此处用了工厂方法模式,对于不同的游戏的玩法进行抽象为接口,并独自实现,每个游戏都添加一个相应工厂,用于生成特定的游戏。

2.单一职责原则

就一个类而言,应该仅有一个引起它变化的原因。简而言之就是,一个类只负责一个职责,提高内聚性。若一个类有多个职责,相当于将多个职责耦合在一起。
接下来看一个多职责的例子:要设计一个管理用户的类(ID做主键,唯一标识一个用户),一般而言,只需用一个接口,包含实现获取、修改用户的信息等信息相关的方法,以及删除、新增用户等管理用户实体的方法,让一个类具体实现即可,实例如下:


class User {
    
    
public:
	//这里不太合规范,为了方便。。。
	//理应在User内部也实现get set方法
	std::string name;
	std::string ID;
	int age;
};

__interface UserManage {
    
    
	//用户相关信息
	void setName(User&user,std::string&name);
	std::string& getName(const User& user)const;
	void setAge(User& user,int age);
	int getAge(const User& user)const;
	void setID(User& user, int id);
	int getID(const User& user)const;
	//针对用户的操作
	bool dltUser(int id);
	bool addUser(User& user);
	bool changeID(User& user);
	
};

class Manager :public UserManage
{
    
    
	/*
	* 具体方法实现略 不想码了。。。
	*/
};

这样做的问题在于,对用户属性的操作与对用户实体的操作放在一起,Manager类能对用户信息进行操作,又能对用户实体操作,业务对象与业务逻辑没有分开,应该将接口一分为二。
具体改进如下:

class User {
    
    
public://这里不太合规范,为了方便。。。
	std::string name;
	std::string ID;
	int age;
};

__interface UserInfor {
    
    
	//用户相关信息
	void setName(User&user,std::string&name);
	std::string& getName(const User& user)const;
	void setAge(User& user,int age);
	int getAge(const User& user)const;
	void setID(User& user, int id);
	int getID(const User& user)const;

};

__interface UserOP {
    
    
	//针对用户的操作
	bool dltUser(int id);
	bool addUser(User& user);
	bool changeID(User& user);
};

//对用户信息进行设置时 可以强转为UserInfor
//对用户实体进行操作时 可以强转为UserOP
__interface UserManage :UserInfor, UserOP {
    
    

};

class Manager :public UserManage
{
    
    
	
};

3.依赖倒转原则

该原则包含两个含义:
1.高层模块不应该依赖于底层模块,两个都应该依赖抽象。
2.抽象不应该依赖细节,细节应该依赖于抽象。
比如项目刚开始时,有一个显示类,一个纸质书本类,此时显示类可看作高层模块,而书本类可以看作底层模块,若要显示纸质书本内容,那么显示类必将依赖于书本是纸质品这个特性,而电子书是显示在屏幕上的,显示纸质书本的接口将不能得到复用,需要新增电子书显示接口,而在未来新增别的特性的书籍时,以前的接口都得不到复用。
解决方案:显示类的显示函数依赖于抽象类或接口,书类的内容函数也依赖于抽象类,不同书类实现具体细节。

//此处也可将接口替换成抽象类
__interface Display {
    
    
public:
	std::string return_text();
};

class EleBook :public Display{
    
    
public:
	std::string return_text()override
	{
    
    
		return std::string("这是电子书巴拉巴拉。。。。。");
	}
};

class NomBook :public Display {
    
    
public:
	std::string return_text()override
	{
    
    
		return std::string("这是纸质书巴拉巴拉,,,,,,");
	}
};

class Reader {
    
    
public:
	void read_book(Display&p)//通过多态屏蔽实参具体类型
	{
    
    
		std::cout << p.return_text() << std::endl;
	}
};

void test()
{
    
    
	std::unique_ptr<EleBook>book(new EleBook());
	std::unique_ptr<Reader>reader(new Reader());
	reader->read_book(*book);
}

4.里氏替换原则

子类型必须能替换掉它们的父类,子类可以扩展父类的功能,但不能改变原有父类的功能,每个子类对应不同的业务含义,使父类作为参数,传递不同的子类完成不同的业务逻辑,注意:子类可以增加功能,但不能改变父类原来的功能(若要改变,则应该让父类与子类继承同一个接口类),即要求子类能以父类的身份出现,并且调用接口与父类调用接口的结果一致。
与之十分相似的情况是C++中父类包含虚函数,子类继承父类并重写虚函数方法,此时父类可以替换子类,使用子类重写的方法,但不能使用子类独有的方法。
这种情况的实例如下:

class Parent {
    
    
	public:
		Parent(){
    
    }
		~Parent(){
    
    }
		virtual void play()
		{
    
    
			std::cout << "Parent playing!" << std::endl;
		}
};

class Son1 :public Parent {
    
    
	public:
		Son1(){
    
    }
		~Son1(){
    
    }
		virtual void play()override
		{
    
    
			std::cout << "Son1 playing!!!!" << std::endl;
		}
		//子类1拓展功能
		void happy_play()
		{
    
    
			std::cout << "Son1 happy Playing!" << std::endl;
		}
};

class Son2 :public Parent {
    
    
	public:
		Son2(){
    
    }
		~Son2(){
    
    }
		virtual void play()override
		{
    
    
			std::cout << "Son2 playing" << std::endl;
		}
		//子类2拓展功能
		void happ_play()
		{
    
    
			std::cout << "Son2 happy playing" << std::endl;
		}

};

void test(Parent *p)
{
    
    
	//可根据不同子类的虚函数表 调用子类的虚函数
	p->play();
	//但此时无法访问子类的拓展功能 只能通过子类对象访问
	try
	{
    
    
		Son1& s1 = dynamic_cast<Son1&>(*p);
		s1.happy_play();
	}
	catch (const std::bad_cast&)
	{
    
    
		Son2& s2 = dynamic_cast<Son2&>(*p);
		s2.happ_play();
	}
}

利用多态,可十分方便的复用代码,并且无需知晓调用对象具体属于哪类。

5.接口隔离原则

建立单一接口,包括两种含义:
1.客户端不应该依赖它不需要的接口;
2.一个类对另一个类的依赖应该建立在最小的接口上。
通俗理解便是:复杂的接口,可分解为多个简单的接口。接口的设计粒度越小,系统的灵活性就越高,但同时系统的复杂度也就越高,开发难度也就越大。这样做的好处是,当一个模块依赖于另外一个模块的接口时,不至于因其它无关接口的改变(比如接口发生了改变等情况),而修改该模块。
比如我需要一块硬盘存储数据,有两种办法,把自己电脑的硬盘拆下来,或者去买一块,前者我需要依赖于专业设备来拆开电脑,若没有专业设备,工作似乎就进行不下去,而我直接买一块硬盘,就能直接用,无需依赖于拆开电脑、装好电脑等无关的操作。
实例如下:

__interface T1 {
    
    
public:
	void method1();
	void method2();
	void method3();
	void method4();
};

//即便S1只需要method1 和 method2 也必须实现它不需要的3,4方法
class S1 :public T1 {
    
    
public:
	void method1()override {
    
    
		std::cout << "S1实现的method1" << std::endl;
	}
	void method2()override {
    
    
		std::cout << "S1实现的method2" << std::endl;
	}
	void method3()override {
    
    
		std::cout << "S1实现的method3" << std::endl;
	}
	void method4()override {
    
    
		std::cout << "S1实现的method4" << std::endl;
	}
};

可以看到,类S1实现了两个它不需要的接口,改进方法是,将接口进行拆分,如下

//改进方法 将T1分为多个interface

__interface T1 {
    
    
public:
	void method1();
};

__interface T2 {
    
    
public:
	void method2();
};

__interface T3 {
    
    
public:
	void method3();
};

__interface T4 {
    
    
public:
	void method4();
};


class S1 :public T1,T2 {
    
    
public:
	void method1()override {
    
    
		std::cout << "S1实现的method1" << std::endl;
	}
	void method2()override {
    
    
		std::cout << "S1实现的method2" << std::endl;
	}
};

class S2 :public T1,T3 {
    
    
public:
	void method1()override {
    
    
		std::cout << "S2实现的method1" << std::endl;
	}
	void method3()override {
    
    
		std::cout << "S2实现的method3" << std::endl;
	}
};

此处S1要实现method1,method2,S2要实现method1,method3,最简单的方法就是拆分为4个接口以保证不会实现多余的无关接口。

6.迪米特原则

又称为最少知道原则,尽量降低类与类之间的耦合,一个对象应该对其它对象有最少的了解。即一个类对自己依赖的类知道的越少越好,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供public方法,不对外泄露任何信息,这一点在C++中可以通过设置访问权限来实现,并且尽量避免使用友元函数。


总结

值得注意的是,单一职责原则与接口隔离原则十分相似,但单一职责重点在于对类职责的约束,优先考虑的是职责划分,其次才是细节;而接口隔离主要是隔离对无关接口的依赖。
欢迎朋友们来指出错误。

猜你喜欢

转载自blog.csdn.net/weixin_45416439/article/details/123891253