设计模式是在特定环境下人们解决某类重复出现问题的一套成功或者有效的解决方案。
设计模式的基础就是多态。
单一职责原则:类的职责单一,对外只提供一种功能,而引起类变换的原因都应该只有一个。
开闭原则:类的改动是通过增加代码进行的,而不是修改代码。即对扩展开放,对修改关闭,增加功能是通过增加代码实现的,而不是通过修改代码实现的(重点)
里氏替换原则:任何抽象类出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。
依赖倒转原则:依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程,不要对实现编程。高层模块不应该依赖于底层模块,两者都应该依赖于抽象
class AbstructWorker{ public: virtual void doBussiness()=0; } class saveMoney:public AbstructWorker{ public: void doBussiness(){ cout<<"办理存钱业务"<<endl; } } class payMoney:public AbstructWorker{ public: void doBussiness(){ cout<<"办理支付业务"<<endl; } } class transferMoney:public AbstructWorker{ public: void doBussiness(){ cout<<"办理转账业务"<<endl; } } //中层,依赖于抽象 void donewBussiness(AbstructWorker* worker){ worker->dobussiness(); } //高层调用 void test1{ saveMoney* worker=new saveMoney; donewBussiness(worker); }
接口隔离原则:不应该强迫用户的程序依赖他们不需要的接口,一个接口应该只提供一种对外功能,不应该把所有的操作都封装到一个接口中去。
合成复用原则:如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系,对于继承和组合,优先使用组合。
迪米特法则(最少知识原则):一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性,例如在一个程序中各个模块相互调用时,通常会提供一个统一的接口来实现,这样其他模块不需要了解另一个模块的内部实现细节,这样当一个模块的内部发生改变时,不会影响到其他模块的使用(黑盒原理)
简单工厂模式
简单工厂模式下,定义一个工厂类,通过工厂类的对象生成器与传入的参数去创建对象,判断逻辑在工厂内,实现客户端与具体类的解耦,对于某些创建过程比较复杂的类来数不用考虑太多,定义好对象生产器之后定义对象直接传参数就行了
缺点就是简单工厂模式增加新的功能是通过修改源代码实现的,不符合开闭原则,其次就是这个工厂类的职责过重,一旦出问题会影响到很多模块。
#include<iostream> using namespace std; //抽象类 class AbstructFruit { public: virtual void showname() = 0; }; //苹果类 class Apple :public AbstructFruit { virtual void showname() { cout << "我是苹果" << endl; } }; //香蕉类 class Banana :public AbstructFruit { virtual void showname() { cout << "我是香蕉" << endl; } }; //凤梨类 class Pear :public AbstructFruit { virtual void showname() { cout << "我是凤梨" << endl; } }; //简单水果工厂类 class FruitFactory { public: AbstructFruit* createFruit(string name) { if (name == "apple") { return new Apple; } else if (name == "banana") { return new Banana; } else if (name == "pear") { return new Pear; } return nullptr; } }; void test1() { //创建工厂 FruitFactory* factory = new FruitFactory; AbstructFruit* fruit = factory->createFruit("apple"); fruit->showname(); delete fruit; fruit = factory->createFruit("banana"); fruit->showname(); delete fruit; fruit = factory->createFruit("pear"); fruit->showname(); delete fruit; return; } int main() { test1(); return 0; }
适用场景
1、工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
2、客户端只知道传入工程类的参数,对于如何创建对象并不关心。
工厂方法模式
工厂方法模式=简单工厂模式+开闭原则
例如:对于每一个水果都创建一个工厂去创建水果对象,新增水果,只需要添加新的水果工厂即可,不会修改已有的水果工厂,所有的水果工厂通过抽象类水果工厂来管理
优点:
1、不需要记住具体类名,甚至连具体参数都不用记忆
2、实现了对象创建和使用的分离
3、系统的可扩展性也变得非常好,无需修改接口和原类
缺点:
1、系统中的类的个数增加,复杂度和理解度增加
2、增加了系统的抽象性和理解难度
#include<iostream> using namespace std; //抽象水果类 class AbstructFruit { public: virtual void showname() = 0; }; //苹果类 class Apple :public AbstructFruit { virtual void showname() { cout << "我是苹果" << endl; } }; //香蕉类 class Banana :public AbstructFruit { virtual void showname() { cout << "我是香蕉" << endl; } }; //凤梨类 class Pear :public AbstructFruit { virtual void showname() { cout << "我是凤梨" << endl; } }; //抽象工厂 class AbstructFruitFactory{ public: virtual AbstructFruit* CreateFruit()=0; } //苹果工厂 class AppleFactory:public AbstructFruitFactory{ public: virtual AbstructFruit* CreateFruit(){ return new Apple; } } //香蕉工厂 class BananaFactory:public AbstructFruitFactory{ public: virtual AbstructFruit* CreateFruit(){ return new Banana; } } //凤梨工厂 class AppleFactory:public AbstructFruitFactory{ public: virtual AbstructFruit* CreateFruit(){ return new Pear; } } void test2{ AbstructFruitFactory* factory=nullptr; AbstructFruit* fruit=nullptr; //创建苹果工厂 factory=new AppleFactory; fruit=factory->createFruit(); fruit->showname(); }
适用场景
1、客户端不知道它所需要的对象的类,知道工厂就行了。
2、抽象工厂类通过其子类来指定创建哪个对象。
抽象工厂
抽象工厂针对的是产品簇,而不是产品等级结构
优点: 工厂抽象类创建了多个类型的产品,当有需求时,可以创建相关产品子类和子工厂类来获取。
缺点: 扩展新种类产品时困难。抽象工厂模式需要我们在工厂抽象类中提前确定了可能需要的产品种类,以满足不同型号的多种产品的需求。但是如果我们需要的产品种类并没有在工厂抽象类中提前确定,那我们就需要去修改工厂抽象类了,而一旦修改了工厂抽象类,那么所有的工厂子类也需要修改,这样显然扩展不方便。
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
那么我们就必须保证:
(1)该类不能被复制。
(2)该类不能被公开的创造。
那么对于C++来说,它的构造函数,拷贝构造函数和赋值函数都不能被公开调用。
单例模式实现方式
实现步骤:
首先将构造函数私有化,然后在类内定义该类的私有静态指针,然后定义一个静态结构,可以让用户获得定义的单例对象(即静态指针)
单例模式通常有两种模式,分别为懒汉式单例和饿汉式单例。两种模式实现方式分别如下:
(1)懒汉式设计模式实现方式(2种)
a. 静态指针 + 用到时初始化
b. 局部静态变量
(2)饿汉式设计模式(2种)
a. 静态指针 + 类外初始化时new空间实现
b. 直接定义静态对象
懒汉模式实现一:静态指针 + 用到时初始化
特点是延迟加载,比如配置文件,采用懒汉式的方法,配置文件的实例直到用到的时候才会加载,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化。
//碰到多线程,线程不安全 template<typename T> class Singleton { public: static T& getInstance() { if (!value_){ value_ = new Singleton(); } return *value_; } private: Singleton(); ~Singleton(); static T* value_; }; template<typename T> T* Singleton<T>::value_ = NULL;
在单线程中,这样的写法是可以正确使用的,但是在多线程中就不行了,该方法是线程不安全的。
a. 假如线程A和线程B, 这两个线程要访问getInstance函数,线程A进入getInstance函数,并检测if条件,由于是第一次进入,value为空,if条件成立,准备创建对象实例。
b. 但是,线程A有可能被OS的调度器中断而挂起睡眠,而将控制权交给线程B。
c. 线程B同样来到if条件,发现value还是为NULL,因为线程A还没来得及构造它就已经被中断了。此时假设线程B完成了对象的创建,并顺利的返回。
d. 之后线程A被唤醒,继续执行new再次创建对象,这样一来,两个线程就构建两个对象实例,这就破坏了唯一性。 另外,还存在内存泄漏的问题,new出来的东西始终没有释放,下面是懒汉式的一种改进。
//线程安全 emplate<typename T> class Singleton { public: static Singleton& getInstance(){ if (!value_){ value_ = new T(); } return *value_; } private: class CGarbo{ public: ~CGarbo(){ if(Singleton::value_) delete Singleton::value_; } }; static CGarbo Garbo; Singleton(); ~Singleton(); static Singleton* value_; }; template<typename T> T* Singleton<T>::value_ = NULL;
在程序运行结束时,系统会调用Singleton的静态成员Garbo的析构函数,该析构函数会删除单例的唯一实例。使用这种方法释放单例对象有以下特征:
-
a. 在单例类内部定义专有的嵌套类;
-
b. 在单例类内定义私有的专门用于释放的静态成员;
-
c. 利用程序在结束时析构全局变量的特性,选择最终的释放时机。
懒汉模式实现二:局部静态变量
//碰到多线程,线程不安全(线程不安全) template<typename T> class Singleton { public: static T& getInstance(){ static T instance; //局部静态变量 return instance; } private: Singleton(){}; //默认构造函数 Singleton(const Singleton&); //拷贝构造函数 Singleton& operator=(const Singleton&); //赋值运算符重载 };
同样,静态局部变量的实现方式也是线程不安全的。如果存在多个单例对象的析构顺序有依赖时,可能会出现程序崩溃的危险。 对于局部静态对象的也是一样的。因为 static T instance;语句不是一个原子操作,在第一次被调用时会调用Singleton的构造函数,而如果构造函数里如果有多条初始化语句,则初始化动作可以分解为多步操作,就存在多线程竞争的问题。 为什么存在多个单例对象的析构顺序有依赖时,可能会出现程序崩溃的危险?
原因:由于静态成员是在第一次调用函数GetInstance时进行初始化,调用构造函数的,因此构造函数的调用顺序时可以唯一确定了。对于析构函数,我们只知道其调用顺序和构造函数的调用顺序相反,但是如果几个Singleton类的析构函数之间也有依赖关系,而且出现类似单例实例A的析构函数中使用了单例实例B,但是程序析构时是先调用实例B的析构函数,此时在A析构函数中使用B时就可能会崩溃。
//代码实例(线程安全) #include <string> #include <iostream> using namespace std; class Log { public: static Log* GetInstance(){ static Log oLog; //局部静态变量 return &oLog; } void Output(string strLog){ cout<<strLog<<(*m_pInt)<<endl; } private: Log():m_pInt(new int(3)){} ~Log(){ cout<<"~Log"<<endl; delete m_pInt; m_pInt = NULL; } int* m_pInt; }; class Context { public: static Context* GetInstance(){ static Context oContext; //静态局部变量 return &oContext; } ~Context(){ //在析构函数中使用已经被析构的Log静态局部对象 Log::GetInstance()->Output(__FUNCTION__); } void fun(){ Log::GetInstance()->Output(__FUNCTION__); } private: Context(){} Context(const Context& context); }; int main(int argc, char* argv[]){ Context::GetInstance()->fun(); return 0; }
在这个反例中有两个Singleton: Log和Context,Context的fun和析构函数会调用Log来输出一些信息,结果程序Crash掉了
解决方案:对于析构的顺序,我们可以用一个容器来管理它,根据单例之间的依赖关系释放实例,对所有的实例的析构顺序进行排序,之后调用各个单例实例的析构方法,如果出现了循环依赖关系,就给出异常,并输出循环依赖环。
饿汉模式
单例类定义的时候就进行实例化。因为main函数执行之前,全局作用域的类成员静态变量m_Instance已经初始化,故没有多线程的问题。
饿汉模式实现一:静态指针 + 类外初始化时new空间实现
//代码实例(线程安全) class Singleton { protected: Singleton(){} private: static Singleton* p; public: static Singleton* initance(){ return p; } }; Singleton* Singleton::p = new Singleton();
饿汉模式实现二:直接定义静态对象
//代码实例(线程安全) //.h文件 class Singleton { public: static Singleton& GetInstance(){ return m_Instance; } private: Singleton(){} Singleton(const Singleton&); Singleton& operator= (const Singleton&); private: static Singleton m_Instance; }; Singleton Singleton::m_Instance=new Singleton; //类外定义-不要忘记写 //函数调用 Singleton& instance = Singleton::GetInstance();
优点:
实现简单,多线程安全
缺点:
a. 如果存在多个单例对象且这几个单例对象相互依赖,可能会出现程序崩溃的危险。
原因:对编译器来说,静态成员变量的初始化顺序和析构顺序是一个未定义的行为;具体分析在懒汉模式中也讲到了。
b. 在程序开始时,就创建类的实例,如果Singleton对象产生很昂贵,而本身有很少使用,这种方式单从资源利用效率的角度来讲,比懒汉式单例类稍差些。但从反应时间角度来讲,则比懒汉式单例类稍好些。
使用条件:
a. 当肯定不会有构造和析构依赖关系的情况。
b. 想避免频繁加锁时的性能消耗
单例对象释放问题
一般来说单例的内存不必考虑释放问题,因为只有一份,占很少的内存,在程序结束后就可以自动释放了,所以没必要自己释放,如果非要释放,可以使用一个嵌套类,在单例类中定义一个嵌套类成员,在嵌套类的析构函数去释放
代理模式
定义:为其他对象提供一种代理以控制这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用
例如说,我们创建一个系统类,但是我们不想每个人都能任意访问这个系统,我们可以做个代理类来限制对系统类的访问,这个代理类包含系统类的所有类方法,并且在通过权限验证后在代理类中调用系统类的方法。
//代理角色与真实对象的公共接口 class AbstructSystem{ virtual void run()=0; } //真实对象 class mySystem:public AbstructSystem{ virtual void run(){ cout<<"系统启动......<<endl; } } //代理角色 class ProxySystem:public AbstructSystem{ public: ProxySystem(string username,string password){ m_username=username; m_password=password; } void check(){ if(m_username=="root"&&m_password=="1234")flag=true; flag=false; } virtual void run(){ } private: string m_username; string m_password; bool flag; }
外观模式
外观模式就是将复杂的子类系统抽象到同一个的接口进行管理,外界只需要通过此接口与子类系统进行交互,而不必直接与复杂的子类系统进行交互。
class sys1{ public: void run(){ cout<<"系统一启动"<<endl; } } class sys2{ public: void run(){ cout<<"系统二启动"<<endl; } } class sys3{ public: void run(){ cout<<"系统三启动"<<endl; } } //外观类 class facade{ public: facade(){ p1=new sys1; p2=new sys2; p3=new sys3; } void run(){ p1->run(); p2->run(); p3->run(); } private: sys1*p1; sys2*p2; sys3*p3; }
适配器模式
就是将已经写好的接口适配成我们想要的接口
class MyPrint{ public: //两个参数,而for_each的第三个参数为单参数的函数,所以需要使用适配器去粉饰 bool operator()(int v1,int v2){ cout<<v1+v2<<endl; } } class Target{ public: virtual bool operator()()=0; } //适配器 class adapter:public Traget{ public: adapter(int num){ para=num; } virtual bool operator()(int v){ a(v1,para); } private: adapter a; int para; } //MyBind2 adapter MyBand2(int num){ return adapter(num); } void test1{ vector<int>nums; for(int i=0;i<10;i++)nums.push_back(i); for_each(nums.begin(),nums.end(),MyBand2(10)); }
模板方法模式
定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤
例如,对于泡咖啡和泡茶两件事,固定步骤都为:烧水--->冲泡--->倒入杯中--->加辅料
优点:
1、在父类中形式化的定义一个算法,而由它的子类来实现细节的出路,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序
2、模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当的使用继承来实现代码复用
3、可实现一种反向控制结构,通过子类覆盖父类的方法来决定某一特定步骤是否需要执行
4、在模板方法模式中可以通过子类覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则
//抽象模板 class kuangjia{ public: virtual void shaoshui()=0; virtual void chongpao()=0; virtual void daoru()=0; virtual void jialiao()=0; //模板方法 void make(){ shaoshui(); chongpao(); daoru(); jialiao(); } }; //泡茶 class makeTea:public kuangjia{ public: void shaoshui(){ } void chongpao(){ } void daoru(){ } void jialiao(){ } } //泡咖啡 class makeCoffee:public kuangjia{ public: void shaoshui(){ } void chongpao(){ } void daoru(){ } void jialiao(){ } } void test1{ makeTea* tea=new makeTea; tea->make(); makeCoffee* coffee=new makeCoffee; coffee->make(); }
策略模式
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
class weaponstrategy{ public: virtual void useweapon()=0; } class AKstrategy{ void useweapon(){ } } class wifestrategy{ void useweapon(){ } } class people{ public: void setStrategy(weaponstrategy* tmp){ p=tmp; } void throwWeapon(){ p->useWeapon(); } private: weaponstrategy* p }
命令模式
将请求封装成一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
命令模式是一种对象行为型模式,其别名为动作模式或者事务模式。
将不同的请求封装成不同的对象,这些对象有一个共同的接口(抽象类),从而可以将这些请求对象进行排队。
class task{ public: void addmoney(){ cout<<"增加金币"<<endl; } void adddiamon(){ cout<<"增加钻石"<<endl; } void addrank(){ cout<<"增加等级"<<endl; } } //抽象类 class AbstructCommand{ public: virtual void execuit()=0; } //请求对象 class AddMoney:public AbstructCommand{ public: AddMoney(task* tmp){ t=tmp; } void execuit(){ tmp->addmoney(); } private: task* t; } class AddDiamon:public AbstructCommand{ public: AddDiamon(task* tmp){ t=tmp; } void execuit(){ tmp->adddiamon(); } private: task* t; } class AddRank:public AbstructCommand{ public: AddRank(task* tmp){ t=tmp; } void execuit(){ tmp->adddiamon(); } private: task* t; } //服务器类,以一个队列将请求对象排队 class Server{ public: void addrequest(AbstructCommand*command){ que.push(command); } void startHandle(){ while(!que.empty){ que.front()->execuit(); que.pop(); } } private: queue<AbstructCommand*>que; }
观察者模式
观察者模式是用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。
在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者。
//观察者抽象类 class Abstructobservation{ public: virtual void update()=0;//观察者要根据观察目标改变状态 } //具体观察者 class obser1:public Abstructobservation{ obstr1(){ cout<<"观察者1正在观察"<<endl; } virtual void update(){ cout<<"观察者1停止观察"<<endl; } } class obser2:public Abstructobservation{ obstr1(){ cout<<"观察者2正在观察"<<endl; } virtual void update(){ cout<<"观察者2停止观察"<<endl; } } class obser3:public Abstructobservation{ obstr1(){ cout<<"观察者3正在观察"<<endl; } virtual void update(){ cout<<"观察者3停止观察"<<endl; } } //观察目标抽象类:观察目标必须可以通知观察者,可以增加观察者,可以删除观察者 class AbstructObstrTarget{ public: virtual void addObstr1()=0; virtual void dropObstr1()=0; virtual void notifyObstr1()=0; } //具体观察目标类 class obstrTarget1:public AbstructObstrTarget{ public: void addObstr1(Abstructobservation*tmp){ ls.push_back(tmp); } void dropObstr1(Abstructobservation*tmp){ ls.remove(tmp); } void notifyObstr1(){ for(auto pos:ls){ (*pos)->update(); } } private: list<Abstructobservation*>ls;//观察者链表 }
装饰器模式
装饰模式又叫包装模式,通过一种对客户端透明的方式来拓展对象功能,是继承关系的一种替代。可以动态的给一个类增加功能。
装饰模式就是把要附加的功能分别放在单独的类中,并让这个类包装它要装饰的对象,当需要执行时,客户端就可以有选择的按顺序的使用装饰功能包装.
简述一下单例设计模式的懒汉式和饿汉式,如何保证线程安全
-
懒汉式设计模式
懒汉模式的特点是延迟加载,比如配置文件,采用懒汉式的方法,配置文件的实例直到用到的时候才会加载,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化。
-
饿汉模式
单例类定义的时候就进行实例化。因为main函数执行之前,全局作用域的类成员静态变量m_Instance已经初始化,故没有多线程的问题。
对于懒汉模式的线程安全问题有如下解决方法:
1、使用静态指针+用时初始化的懒汉模式,存在多线程调用静态接口创建多个实例的问题,破坏了唯一性,且会导致内存泄露的风险,可以使用嵌套类,在单例类内声明一个静态类对象成员,该成员的析构函数会删除单例类的实例
2、使用局部静态变量的懒汉模式,存在析构顺序依赖问题,先构造的单例类的析构函数可能会调用后构造的单例类的成员函数,但是根据析构顺序,后构造的先析构,所以此时先构造的单例类的析构函数调用时,后构造的单例类实例已经被删除,此时可能会导致程序崩溃
请说说工厂设计模式,如何实现,以及它的优点
-
工厂设计模式的定义
定义一个创建对象的接口,让子类决定实例化哪个类,而对象的创建统一交由工厂去生产,有良好的封装性,既做到了解耦,也保证了最少知识原则。
-
工厂设计模式分类
工厂模式属于创建型模式,大致可以分为三类,简单工厂模式、工厂方法模式、抽象工厂模式。
简单工厂只抽象产品,工厂方法抽象工厂和产品,每个产品对应一个工厂,抽象工厂也是抽象工厂和产品,但是针对的是产品族,而不是产品结构等级