前言
早就想写一下设计模式的话题,但碍于琐事,便未能如愿,今终如愿。
编程不仅是一门技术,更是一门艺术,任何经努力思考后码出的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++中可以通过设置访问权限来实现,并且尽量避免使用友元函数。
总结
值得注意的是,单一职责原则与接口隔离原则十分相似,但单一职责重点在于对类职责的约束,优先考虑的是职责划分,其次才是细节;而接口隔离主要是隔离对无关接口的依赖。
欢迎朋友们来指出错误。