设计模式的SOLID原则和创建式设计模式 一
设计模式的作用
如果说算法的作用是让我们写出高效的程序,那么设计模式的目的就是让我们写出好的,扩展性强的,符合规范的代码。
SOLID设计原则
在软件设计中,要遵循一些原则。SOLID五大原则在软件设计中非常常见。
S 单一职责原则
每个模块都负责单一的职责,若一个模块负责多个原则,则应该考虑将这个模块划分
事实上,这个原则看上去很简单,但在软件设计中一个模块是否负责单一职责是很难区分的,可能会随着适用场景的改变而改变。
举个例子:
class Person{
int PersonID;
string name;
int age;
int sex;
string city;
string address;
};
当用于只管理维护用户信息,不用于其他用途的场景时,该类是符合单一职责的。
但是当业务场景中加上了一个配送的场景,此时就不符合单一职责了,应该将Person的地址信息给拆分开来。
O 开闭原则
对扩展开放,对修改封闭
开闭原则用于判断自己的代码是否有扩展性,如果要需要增加一个新的功能时,需要改变已有的实现的话,那么就属于违反开闭原则。如何判断改变已有的实现,有几个层级,如子系统层级,类层级,方法层级,在业务较简单的实现在方法层级符合开闭原则就够了,若是业务复杂的话,则需要考虑在类层级上符合开闭原则。
L 里氏替换原则。
当让一个子类继承父类时,我们需要遵循这样一个原则:子类能够替代父类所在的任何一个场景,如果不能做到,那么就违背了里氏替换原则。
举几个符合里式替换原则的例子,父类为鸟,子类为麻雀。因为麻雀就是鸟,这样的父子类就是符合里氏替换原则的。
class bird{
public:
bird(){
cout<<"create a bird"<<endl;
}
virtual ~bird(){
cout<<"destruct a bird"<<endl;
}
};
class sparrow : public bird{
public:
sparrow(){
cout<<"create a sparrow"<<endl;
}
virtual ~sparrow(){
cout<<"destruct a sparrow"<<endl;
}
};
I 接口隔离原则
每个类所用到的接口集应该是最小的。
举一个正确的例子和一个错误的例子就很容易理解
class Interface{
virtual void f1() = 0;
virtual void f2() = 0;
virtual void f3() = 0;
virtual void f4() = 0;
virtual void f5() = 0;
};
class A:public Interface{
public:
void f1(){
cout<<"A call f1()"<<endl;
}
void f2(){
cout<<"A call f2()"<<endl;
}
void f3(){
cout<<"A call f3()"<<endl;
}
void f4(){
}
void f5(){
}
};
class B:public Interface{
public:
void f1(){
cout<<"B call f1()"<<endl;
}
void f4(){
cout<<"B call f4()"<<endl;
}
void f5(){
cout<<"B call f5()"<<endl;
}
void f2(){
}
void f3(){
}
};
在这个例子中,A类只需要用到方法f1,f2,f3,,B类只需要用到f1,f4,f5,它们都用到了接口Interface,但是由于接口中定义了5个方法,所以A类不得不实现它用不到的f4,f5方式,这样就造成了代码的冗余,此时我们应该将接口划分成两个,一个专门用来定义A所要用到的方法,一个专门用来定义B中用到的方法。
D
依赖倒置原则
高层模块不应该依赖底层模块,两者都应该依赖抽象。
这是一个很好的保证扩展性的策略。如果在系统中发现高层模块依赖底层模块的实现,我们可以考虑在这个底层模块中加一层抽象,以此来隔离低层模块的变化。
同样举个例子来说明。
在订餐系统中有一个order类,order类可以预定不同种类的食物,这时order类需要依赖各种各样的食物类来实现,为了不违背依赖倒置原则,我们可以在其中加一个抽象的food类,使得order和各种各样的事物类都依赖于这个food类,这样就遵循了依赖倒置原则,就算之后增加了新的事物,我们也能只用再加一个新的依赖于food类的具体食物就行,这样就将变化封装起来了。
创建式设计模式
创建式相关的设计模式主要为了解决对象创建的工作。分别包括单例模式,工厂模式,原型模式,建造者模式
单例模式的目的是为了保证每个类只能创建一个实例,实现方法有
恶汉式单例模式,懒汉式单例模式(不安全),加锁型单例模式(安全),双检查锁单例模式(会有reorder问题)
直接上代码
//author: Solitude
//date: 2021-09-28
//purpose: descripe the design pattren about creating objects
#include<iostream>
#include<mutex>
using namespace std;
//懒汉式单例模式
//C++没有实现java中的静态代码块概念,所以懒汉式单例模式得需要使用特殊的方式编写
//本段代码只用于表意 无法实际运行
//缺点是如果用不上实例对象却用到了类中的某个方法时,会导致浪费内存
#if 0
class Singleton1{
private:
Singleton1(){}
~Singleton1(){}
static Singleton1* single;
static
{
//initialize the single
}
public:
Singleton1* getInstance(){
return single;
}
};
#endif
//饿汉式单例模式
//缺点是线程不安全
class Singleton2{
private:
Singleton2(){}
~Singleton2(){}
Singleton2* single;
public:
Singleton2* getInstance(){
if(single == nullptr){
//initialize the single
//...
}
return single;
}
};
//饿汉式单例模式(加锁)
//能够实现线程安全,缺点是锁粒度较大,效率不高
class Singleton3{
private:
Singleton3(){
//initialize the single
}
~Singleton3(){}
Singleton3* single;
std::mutex mtx;
public:
Singleton3* getInstance(){
mtx.lock();
if(single == nullptr){
Singleton3();
}
mtx.unlock();
return single;
}
};
//懒汉式的double check模式,效率较高
//但是有个reorder问题
//reorder问题描述
//构造函数一般分为3个步骤 1.申请内存 2.调用构造函数初始化内存 3.返回
//有些编译器对构造函数做了初始化 将第2步延后执行,申请内存之后就直接返回
//这样可能会有一个另外的类得到未初始化的对象就直接使用,导致报错
//这个问题无法靠程序解决,只有依靠编译器支持
class Singleton4{
private:
Singleton4(){
//initialize the single
}
~Singleton4(){}
Singleton4* single;
std::mutex mtx;
public:
Singleton4* getInstance(){
if(single == nullptr){
mtx.lock();
if(single == nullptr){
Singleton4();
}
mtx.unlock();
}
return single
}
};
int main(){
}
工厂模式主要为了解决创建多种相似类别的对象问题。
原型模式主要为了解决得到与当前实例对象相同的对象的问题
建造者模式主要为了应付对象构建过程多样化的问题,解决方案为将对象构建过程与对象本身解耦
这个放在下篇博客中介绍