设计模式概述

什么是设计模式?

设计模式(Design Pattern)是一套被反复使用、被多数人所知晓的代码设计经验总结。使用设计模式是为了提高代码重用性和可靠性,让代码更容易被他人理解。一个完整的设计模式一般有以下几个元素:模式名称、问题、解决方案和效果。

设计模式可以被用在哪种语言上?

设计模式不局限于某种实际的编程语言,只要该语言满足面向对象的特性,即:封装、继承、多态,那么就可以使用设计模式,包括但不局限于C++、Java。本文的代码均以C++为例。

什么是类和对象?

类是一些相关的数据和方法的集合,而对象是类的一个实例。

类/对象之间的关系有多少种?

  1. 继承:让某个类获得另一个类的属性和方法,是一种类与类之间的关系。通常这两个类会被分为父类和子类,他们的关系在编译时期就被确定下来,是一种静态关系。
  2. 组合:让某个对象包含和使用另一个对象,是一种对象与对象之间的关系。通常这两个对象的关系会在运行时发生变化,是一种动态关系。

设计模式的分类?

  1. 创建型模式:主要用于创建对象。
  2. 结构型模式:主要用于将类或对象结合在一起形成功能更强大的新结构。
  3. 行为型模式:主要关注对象的行为和相互作用,将对象的功能抽象化。

什么是设计模式的原则?

所有的设计模式底层都遵循了一些原则,其中包括:单一职责原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则、合成复用原则。这些原则比设计模式本身更加重要,只要在开发过程中遵循某个或几个原则,代码就能获得极大的重用性和可靠性。

说说设计模式的单一职责原则(Single Responsibility Principle)?

单一职责原则就是对一个类而言,应该仅有一个引起它变化的原因,也就是该类应该只负责某个功能。如果一个类承担的职责过多,就等于把这些职责耦合到了一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力,导致设计变得脆弱。

说说设计模式的开闭原则(Open-Closed Principle)?

类虽然封装了代码,但在很多情况下无法做到完全封闭,后续可能会对类功能进行修改与扩展。开闭原则要求类的设计者要对类后续可能在哪些位置发生变化了然于心,然后在这些位置上创建虚函数。后续对类功能的修改和扩展只能通过新类的继承与重写虚函数来实现,不应该在已经成熟的类中做修改,以免破坏整体的代码结构。

说说设计模式的里氏替换原则 (Liskov Substitution Principle)?

里氏替换原则是对开闭原则的补充,它规定了在任意父类可以出现的地方,子类都一定可以出现。也就是说,子类可以重写父类的虚函数来进行修改与扩展,但这不应该影响父类方法本身的含义和功能。

说说设计模式的依赖倒置原则 (Dependence Inversion Principle)?

  1. 依赖倒置原则指的是:高层模块不应该依赖低层模块,两个都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。其中的高层模块可以理解为上层应用,低层模块可以理解为是一些库提供的底层API接口,而抽象即是指C++中的抽象类。
  2. 我们在设计代码时,应该设计一个中间的抽象类,在它的各个具体子类中封装不同的实现细节,也就是使用不同的底层API接口,而让上层应用统一使用抽象类来完成各种功能。这样一来这个抽象类就成了项目架构里高层与低层的桥梁,它将二者整合到一起,但又对二者屏蔽了对方的细节,大大提高了整个项目架构的稳定性和扩展性。

说说设计模式的接口隔离原则(Interface Segregation Principle)?

接口隔离原是指将不同的功能定义在不同的接口(类函数)中,一个接口只做一件事,以此实现接口的隔离。这样可以减少接口之间依赖的冗余性和复杂性,实现高内聚和低耦合。

说说设计模式的迪米特法则(Law Of Demeter)?

迪米特法则是指一个对象应该尽可能少地与其他对象发生相互作用,减少对象间不必要的依赖。它的核心思想在于降低对象间的耦合度,提高对象的内聚性,避免代码变得臃肿。

说说设计模式的合成复用原则(Composite/Aggregate Reuse Principle)?

合成复用原则是指尽可能通过组合来实现类的功能复用和扩展,而不要使用继承来扩展类的功能。相比起继承,组合具有更大的灵活性和动态变化的能力,可以防止继承泛滥,降低代码的维护成本。

创建型模式

什么是单例模式(Singleton)?

在一个项目中的全局范围内,某个类的对象实例有且仅有一个,其他模块仅能通过这个唯一实例来访问数据和调用功能,即单例模式。根据实现不同又分为饿汉式和懒汉式。

如何实现单例模式(饿汉式)?

在类加载的时候就立刻实例化,也就是在未进入main函数前,程序就会初始化一个单例对象,后续所有操作就只会使用这个单例对象。它的实现方式就是将构造函数私有化,禁止显式构造,外部只能通过一个类静态函数来获取类中的静态唯一实例,而这个实例对象则放在类外直接初始化。

/**********单例模式(饿汉式)**********/ 

#include <iostream>
using namespace std;

//单例模式(饿汉式)
class Singleton{
	public:

		//实例返回函数 
		static Singleton* GetInstance(){
			return m_Instance;  //直接返回静态实例指针 
		}

	private:

		//私有构造函数,不可显式构造 
		Singleton() {};
		 
		//静态实例指针 
		static Singleton *m_Instance;
};

//初始化单例模式的静态实例指针
Singleton* Singleton::m_Instance = new Singleton();

//测试函数 
void Singleton_Test(){

	//从单例模式中获取两个实例 
	Singleton *p1 = Singleton::GetInstance();
	Singleton *p2 = Singleton::GetInstance();

	//测试两个实例是否相同 
	if ( p1 == p2 ){
		cout << "Success" << endl;  
	}else{
		cout << "Fail" << endl;
	}
}

int main(){
	Singleton_Test();
	return 0; 
} 

单例模式(饿汉式)的优缺点?

  1. 优点:是最简单的一种单例形式,没有添加任何的锁,执行效率最高,且线程安全。
  2. 缺点:对象在未被使用时就被分配内存并初始化,这可能会造成内存浪费。由于不知道编译器对全局对象的初始化顺序,如果试图在其他全局对象的初始化操作中去使用单例对象,很可能因为单例对象还没有被初始化,导致出现难以排查的错误。

如何实现单例模式(懒汉式)?

在类加载的时候不实例化,而是在需要使用的时候再创建单例对象,后续所有操作就只会使用第一次创建的单例对象。它的实现方式类似于饿汉式,但其单例对象会在类外会初始化为空值(NULL),真正创建单例对象的时机延后到第一次调用实例返回函数时。

/**********单例模式(懒汉式)**********/ 

#include <iostream>
using namespace std;

//单例模式(懒汉式) 
class Singleton{
	public:

		//实例返回函数 
		static Singleton* GetInstance(){
			//判断是否需要创建单例对象 
			if ( m_Instance == NULL ){
				m_Instance = new Singleton;
			}

			return m_Instance;  //直接返回静态实例指针
		}

	private:

		//私有构造函数,不可显式构造 
		Singleton() {}; 

		//静态实例指针 
		static Singleton *m_Instance;
};

//初始化单例模式的静态实例指针,初始为NULL 
Singleton* Singleton::m_Instance = NULL;

//测试函数 
void Singleton_Test(){

	//从单例模式中获取两个实例 
	Singleton *p1 = Singleton::GetInstance();
	Singleton *p2 = Singleton::GetInstance();

	//测试两个实例是否相同  
	if ( p1 == p2 ){
		cout << "Success" << endl;  
	}else{
		cout << "Fail" << endl;
	}
}

int main(){
	Singleton_Test();
	return 0; 
} 

单例模式(懒汉式)的优缺点?

  1. 优点:内存利用率高,对象直到要使用才创建,也不用考虑全局对象的初始化顺序问题。
  2. 缺点:存在线程不安全问题,在多线程环境下,由于创建对象的过程在底层不是原子的,可能导致多个线程同时创建多个单例对象。虽然可以用线程的互斥同步机制来解决,但也因此导致了懒汉式的执行效率低下。

如何利用锁解决单例模式(懒汉式)的线程安全问题?

  1. 单检查锁:在创建单例对象时加上互斥锁,阻止其他线程创建。它强制线程在获取单例对象时,只能串行获取,而不管单例是否已经存在,失去了并发性,效率低下。
//实例返回函数   
static Singleton GetInstance(){  
    加上互斥锁   
    //判断是否需要创建单例对象   
    if ( m_Instance == NULL ){  
        m_Instance = new Singleton;  
    }  
    解开互斥锁   
    return m_Instance;  //直接返回静态实例指针  
}  
  1. 双检查锁:以单检查锁为基础,在被锁住的代码块的外层添加额外的检查判断。当对象被创建后,其他线程通过第一次检查就可以直接跳过加锁解锁,效率较高。
//实例返回函数     
static Singleton GetInstance(){    
    //判断是否需要创建单例对象     
    if ( m_Instance == NULL ){  
        加上互斥锁   
        if ( m_Instance == NULL ){ //第二次检查  
            m_Instance = new Singleton;  
        }  
        解开互斥锁   
    }     
    return m_Instance;  //直接返回静态实例指针    
}    

单例模式(懒汉式)的双检查锁有什么缺陷?

双检查锁的问题仍然出在创建对象过程的非原子性上。在创建对象时,底层的机器指令会分为三步:预分配对象所需内存->构造对象->对象指针赋值。为了优化执行效率,这三步可能会被重排序,比如预分配对象所需内存->对象指针赋值->构造对象。如果在前两步执行完之后就切换线程,那么当前线程会在第一次检查时直接跳过加锁解锁过程,转而返回一个野指针,它指向了一个没有被初始化的对象,这无疑十分危险。

如何彻底解决单例模式(懒汉式)的线程安全问题?

我们可以将单例对象设置为类函数的静态局部变量,而不是类的静态成员变量。在C++11标准下,编译器在底层会保证静态变量初始化的原子性。虽然创建对象是非原子的,但对象初始化是原子的,那么直接在初始化时创建对象就可以了,同时也不需要任何的锁机制了。当然,为了与饿汉式区分开,类静态成员变量在初始化并不能创建对象,那么此时使用类函数的静态局部变量就变得理所当然了。

//单例模式(懒汉式)   
class Singleton{  
    public:  
        //实例返回函数   
        static Singleton GetInstance(){  
            static Singleton m_Instance;  //创建静态局部变量   
            return &m_Instance;  //直接返回静态实例指针  
        }  
    private:  
        //私有构造函数,不可显式构造   
        Singleton() {};       
};  

如何销毁单例模式产生的单例对象?

  1. 单例对象一般不能由某个线程主动销毁,这可能导致其他使用该对象的线程发生错误。
  2. 通常单例对象是等待程序结束后由系统自动回收,但是这样的方式并不会调用单例对象的析构函数。一般推荐在单例类的内部,创建一个专门用于销毁单例对象的辅助类,它会在程序结束时调用单例对象的析构函数并销毁单例对象,如下所示。
class Singleton{  
    单例模式实现...
    private:  
        //辅助销毁类,仅负责单例对象的释放  
        class ExitInstance{  
            public:  
                //在该类的析构函数中会自动释放单例对象  
                ~ExitInstance(){  
                    if ( m_Instance != NULL ){  
                        delete m_Instance;  
                    }  
                }  
        };  
        //辅助销毁类静态对象,在程序结束时会自动调用其析构函数,从而调用delete释放单例对象  
        static ExitInstance m_Exit;  
};  
//初始化辅助销毁类静态对象   
Singleton::ExitInstance Singleton::m_Exit = ExitInstance();  
  1. 以上的前提都是单例对象必须是在堆中创建的,这意味着我们需要显式地使用delete才能完整地释放它。如果是以类函数的局部变量方式创建单例对象,则不用这么麻烦,程序会帮我们管理所有的静态变量,在程序结束时会安全且完整地释放它。

什么是工厂模式(Factory)?

工厂模式在创建对象时,不直接new,而是交由一个工厂类负责创建。它将创建对象的代码统一放在工厂类管理,让对象的使用者不必关心创建对象的具体逻辑,解耦了对象使用者和对象创建者的依赖关系。根据实现不同又分为简单工厂、工厂模式和抽象工厂。

如何实现简单工厂(Simple Factory)?

创建一个新的非抽象类——工厂类,它给外部提供了公共的成员函数用来创建产品对象。所有具体的产品类都继承自同一个产品抽象基类,并拥有自己的具体实现。

/**********简单工厂**********/ 

#include <iostream>
using namespace std;

//产品抽象基类
class Product{
	public:
		virtual void Run() = 0; //纯虚函数,由子类实现 
};

//A产品类,继承自产品抽象基类
class ProductA: public Product{
	public:
		void Run(){
			cout << "Product A" << endl;  //打印A 
		}
};

//B产品类,继承自产品抽象基类 
class ProductB: public Product{
	public:
		void Run(){
			cout << "Product B" << endl;  //打印B 
		}
}; 

//工厂类 
class Factory{
	public:

		//枚举类型,对应每一个具体的产品类
		enum ProductType {A, B};

		//产品对象创建函数
		Product* CreateProduct( ProductType type ){

			//根据产品类型创建对应产品对象并返回 
			switch (type){
				case A:
					return new ProductA;  //返回A产品对象 
				case B:
					return new ProductB;  //返回A产品对象 
			}

			return NULL; //没有对应产品,直接返回空指针 
		}
};

//测试函数 
void Factory_Test(){

	Factory factory; //创建工厂 

	Product *p = factory.CreateProduct(Factory::A); //命令工厂创建A产品对象 
	p -> Run(); //运行产品 
	delete p;
	p = NULL; 

	cout << "==================================" << endl; //分割线 

	p = factory.CreateProduct(Factory::B);  //命令工厂创建B产品对象
	p -> Run(); //运行产品

	delete p;
	p = NULL;
}

int main(){
	Factory_Test();
	return 0; 
} 

简单工厂(Simple Factory)的优缺点?

  1. 优点:实现简单,用途广泛,大部分问题用简单工厂就可以解决。
  2. 缺点:违背开闭原则,新增产品时需要更改工厂类中的对象创建函数,扩展性差。

如何实现工厂模式(Factory)?

工厂模式在简单工厂的基础上,将工厂类改造为抽象基类,它定义了创建产品对象的纯虚函数,而将具体的创建细节交由工厂子类去实现,不同的工厂子类负责创建不同的产品对象。

/**********工厂模式**********/ 

#include <iostream>
using namespace std;

//产品抽象基类
class Product{
	public:
		virtual void Run() = 0; //纯虚函数,由子类实现 
};

//A产品类,继承自产品抽象基类
class ProductA: public Product{
	public:
		void Run(){
			cout << "Product A" << endl;  //打印A 
		}
};

//B产品类,继承自产品抽象基类 
class ProductB: public Product{
	public:
		void Run(){
			cout << "Product B" << endl;  //打印B 
		}
}; 

//工厂抽象基类
class Factory{
	public:
		//产品对象创建函数(纯虚函数),具体实现交由工厂子类 
		virtual Product* CreateProduct() = 0;
};

//A工厂类
class FactoryA: public Factory{
	public:
		//创建A产品 
		Product* CreateProduct(){
			return new ProductA;  
		}
}; 

//B工厂类 
class FactoryB: public Factory{
	public:
		//创建B产品 
		Product* CreateProduct(){
			return new ProductB;
		}
}; 

//测试函数
void Factory_Test(){

	Factory *f = new FactoryA;  //创建A工厂
	 
	Product *p = f -> CreateProduct(); //使用A工厂生产A产品
	 
	p -> Run();  //运行产品

	delete p;
	delete f; 
	p = NULL;
	f = NULL; 

	cout << "==================================" << endl; //分割线 
	 
	f = new FactoryB;  //创建B工厂
	 
	p = f -> CreateProduct(); //使用B工厂生产B产品
	 
	p -> Run();  //运行产品

	delete p;
	delete f; 
	p = NULL;
	f = NULL; 
}

int main(){
	Factory_Test();
	return 0; 
} 

工厂模式(Factory)的优缺点?

  1. 优点:解耦了产品和工厂的依赖关系,遵循开闭原则,更具扩展性。
  2. 缺点:工厂模式创建出来的产品对象相对简单,难以应用在更复杂的场景下。

如何实现抽象工厂(Abstract Factory)?

抽象工厂在工厂模式的基础上,将产品类改造为普通类,它里面包含了数个零件,每个零件都是继承自某个零件抽象基类的具体零件,而零件组装成产品的过程则由工厂子类负责。

/**********抽象工厂**********/ 

#include <iostream>
using namespace std;

//Item零件抽象基类
class Item{
	public:
		virtual void Info() = 0;  //纯虚函数,由子类实现 
};

//ItemA零件类 
class ItemA:public Item{
	public:
		void Info(){
			cout << "ItemA" ;  //打印ItemA 
		}
};

//ItemB零件类 
class ItemB:public Item{
	public:
		void Info(){
			cout << "ItemB" ;  //打印ItemB 
		}
};

//Ware零件抽象基类 
class Ware{
	public:
		virtual void Ability() = 0;  //纯虚函数,由子类实现 
};

//WareA零件类
class WareA: public Ware{
	public:
		void Ability(){
			cout << "WareA" ;  //打印WareA
		}
};

//WareB零件类
class WareB: public Ware{
	public:
		void Ability(){
			cout << "WareB";  //打印WareB 
		}
};

//产品类,每个产品都包含了数个零件,由创建者指定组装产品的具体零件
class Product{
	public:

		//产品类的构造函数,需要指定组成该产品的具体零件 
		Product(Item *item, Ware *ware)
		: m_item(item), m_ware(ware) {}

		//析构函数,释放所有零件 
		~Product(){
			if ( m_item != NULL ) delete m_item;
			if ( m_ware != NULL ) delete m_ware;
		}

		//运行函数,在底层使用各种产品的各种零件 
		void Run(){ 
			cout << "Product = ";
			m_item -> Info();  //使用Item 

			cout << " + "; 

			m_ware -> Ability();  //使用Ware 
			cout << endl;
		}


	private:
		Item *m_item;  //Item零件 
		Ware *m_ware;  //Ware零件 
};

//工厂抽象基类
class Factory{
	public:
		//产品对象创建函数(纯虚函数),具体实现交由工厂子类
		virtual Product* CreateProduct() = 0;
};

//A工厂类
class FactoryA: public Factory{
	public:

		//创建A产品 
		Product* CreateProduct(){
			//将零件ItemA和零件WareA组装成A产品 
			return new Product(new ItemA, new WareA);  
		}
}; 

//B工厂类 
class FactoryB: public Factory{
	public:

		//创建B产品 
		Product* CreateProduct(){
			//将零件ItemB和零件WareB组装成B产品 
			return new Product(new ItemB, new WareB);
		}
}; 

//测试函数 
void Factory_Test(){

	Factory *f = new FactoryA;  //创建A工厂
	 
	Product *p = f -> CreateProduct(); //使用A工厂生产A产品
	 
	p -> Run();  //运行产品

	delete p;
	delete f; 
	p = NULL;
	f = NULL; 

	cout << "==================================" << endl; //分割线 
	 
	f = new FactoryB;  //创建B工厂
	 
	p = f -> CreateProduct(); //使用B工厂生产B产品
	 
	p -> Run();  //运行产品

	delete p;
	delete f; 
	p = NULL;
	f = NULL; 
}

int main(){
	Factory_Test();
	return 0; 
} 

抽象工厂(Abstract Factory)的优缺点?

  1. 优点:可创造出更复杂的对象,有着优秀的可扩展性。
  2. 缺点:代码复杂度较高,一般用于非常复杂的项目环境下。

结构型模式

什么是适配器模式(Adapter)?

适配器模式在不改变原本类函数功能的情况下,改造某个类的接口,使原本不兼容的对象能够相互兼容,相当于是对象间的桥梁。根据实现不同又分为类适配器和对象适配器。

如何实现类适配器(Class Adapter)?

创建一个适配器类,它可以将某个类的函数接口转换为另一个类的函数接口。其方法是让它继承它需要服务的两个类,然后重写目标类函数,在其中直接调用源类函数即可。

/**********类适配器**********/ 

#include <iostream>
using namespace std;


//目标类
class Target{
	public:
		//可以被其他对象使用的目标类函数接口
		virtual void Run(){
			cout << "Target is Running" << endl;
		}
};

//源类 
class Origin{
	public:
		//无法被其他对象使用的源类函数接口
		virtual void Working(){
			cout << "Origin is Working" << endl;
		}
};

//适配器类,继承自源类和目标类
class Adapter:public Origin, public Target{
	public:
		//通过重写目标类函数,在不改变源类函数功能的情况下,使其可以被其他对象使用 
		void Run(){
			Working(); 
		}
};

//测试函数 
void Adapter_Test(){
	Target *t = new Target; //获取目标类对象 
	t -> Run();  //通过目标类对象能使用Run函数

	cout << "==================================" << endl; //分割线 

	Origin *o = new Origin;  //获取源类对象 
	o -> Working();   //通过源类对象能使用Working函数

	cout << "==================================" << endl; //分割线 

	//但不能通过源类对象使用Run函数 
	//o -> Run(); 

	delete t;
	delete o;
	t = NULL;
	o = NULL;

	t = new Adapter;  //获取适配器类对象 
	t -> Run();  //通过适配器类对象能使用Run函数,而且功能与源类对象的Working函数一样 

	delete t;
	t = NULL;
 }
 
 int main(){
	Adapter_Test();
	return 0; 
} 

类适配器(Class Adapter)的优缺点?

  1. 优点:实现简单,使用方便。
  2. 缺点:编程语言必须支持多继承,而且容易导致继承关系复杂。

如何实现对象适配器(Object Adapter)?

对象适配器在适配器类的实现上有所不同,适配器类只继承目标类,然后通过组合的方式保存源类对象指针。在重写目标类函数时,通过源类对象指针来间接调用源类函数。

/**********对象适配器**********/ 

#include <iostream>
using namespace std;


//目标类
class Target{
	public:
		//可以被其他对象使用的目标类函数接口
		virtual void Run(){
			cout << "Target is Running" << endl;
		}
};

//源类 
class Origin{
	public:
		//无法被其他对象使用的源类函数接口
		virtual void Working(){
			cout << "Origin is Working" << endl;
		}
};

//适配器类,继承自目标类
class Adapter:public Target{
	public:
		//构造函数给源类对象指针赋值 
		Adapter(Origin *origin) 
		: m_origin(origin) {}

		//析构函数,释放源类对象 
		~Adapter(){
			if (m_origin != NULL ) delete m_origin;
		} 

		//重写目标类函数并通过源类对象指针调用源类函数,转换源类的函数接口
		void Run(){
			m_origin -> Working();
		}

	private:
		Origin *m_origin;  //源类对象指针 
};

//测试函数 
void Adapter_Test(){
	Target *t = new Target; //获取目标类对象 
	t -> Run();  //通过目标类对象能使用Run函数

	cout << "==================================" << endl; //分割线 

	Origin *o = new Origin;  //获取源类对象 
	o -> Working();   //通过源类对象能使用Working函数

	cout << "==================================" << endl; //分割线 

	//但不能通过源类对象使用Run函数 
	//o -> Run(); 

	delete t;
	delete o;
	t = NULL;
	o = NULL; 

	t = new Adapter(new Origin);  //获取适配器类对象,并传入源类对象 
	t -> Run();  //通过适配器类对象能使用Run函数,而且功能与源类对象的Working函数一样

	delete t;
	t = NULL; 
 }
 
int main(){
	Adapter_Test();
	return 0; 
} 

对象适配器(Object Adapter)的优缺点?

  1. 优点:通过组合的方式实现适配器,被大多数面向对象的语言支持,更加通用和灵活。
  2. 缺点:在实现方式上略有复杂,需要涉及较多的对象。

什么是装饰器模式(Decorator)?

装饰者模式可以给一个对象动态地添加新的功能,同时又不改变其结构。它实现了原功能的扩展,提供了比继承更有弹性的替代方案。

如何实现装饰器模式(Decorator)?

装饰模式定义了一个抽象基类——组件类,其子类有具体组件类和装饰者类。具体组件类会重写父类虚函数,实现具体功能。装饰者类通过组合的方式保存了一个组件类对象指针(既可以是具体组件类也可以是装饰者类)作为被装饰者,它并不会重写父类虚函数,因此它也是个抽象基类。装饰者的子类会重写父类虚函数,在被装饰者的外部添加额外实现。因此,每个装饰者都可变为被装饰者,被一层一层地添加功能并迭代下去。

/**********装饰器模式**********/ 

#include <iostream>
using namespace std;

//组件抽象基类 
class Component{
	public:
		virtual void Run() = 0;  //纯虚函数,交由子类实现 
};

//A组件类,继承自组件类 
class ComponentA: public Component{
	public:
		void Run(){  
			cout << "ComponentA" << endl; //打印ComponentA 
		}
};

//装饰器类,继承自组件类,但不实现父类的纯虚函数,所以仍是抽象基类 
class Decorator: public Component{
	public:
		//构造函数,赋值组件类对象 
		Decorator(Component *cpt): m_cpt(cpt) {}

		//析构函数,释放被装饰的组件类对象 
		~Decorator(){
			if ( m_cpt != NULL ) delete m_cpt; 
		} 

	protected:
		//组件类对象指针,它作为被装饰者,既可以是具体的组件类对象,也可以是具体的装饰器类对象
		//为了可以被装饰器子类使用,同时又不被外界访问,它必须是保护成员 
		Component *m_cpt;  
};

//A装饰器类,继承自装饰器类 
class DecoratorA:public Decorator{
	public:
		//构造函数,初始化父类对象
		DecoratorA(Component *cpt): Decorator(cpt) {}   

		//重写组件类虚函数,在被装饰者的基础上添加额外实现 
		void Run(){
			cout << "DecoratorA" << endl;
			m_cpt -> Run();
		} 
}; 

//B装饰器类,继承自装饰器类
class DecoratorB:public Decorator{
	public:
		//构造函数,初始化父类对象
		DecoratorB(Component *cpt): Decorator(cpt) {} 
		 
		//重写组件类虚函数,在被装饰者的基础上添加额外实现
		void Run(){
			cout << "DecoratorB" << endl;
			m_cpt -> Run();
		} 
}; 

//测试函数 
void Decorator_Test(){
	Component *p = new ComponentA;  //获得没有任何装饰的A组件对象
	p -> Run();  //运行组件

	cout << "==================================" << endl; //分割线 
	 
	p = new DecoratorA(p);  //添加装饰A 
	p -> Run();  //运行组件

	cout << "==================================" << endl; //分割线 

	p = new DecoratorB(p);  //添加装饰B 
	p -> Run();  //运行组件 

	delete p;
	p = NULL; 
}

int main(){
	Decorator_Test();
	return 0; 
} 

装饰器模式(Decorator)的优缺点?

  1. 优点:相比起单纯的继承,它更具备弹性和灵活度,同时又不会增加太多的子类。
  2. 缺点:装饰者太多容易增加代码复杂度,还可能因为装饰而抹除某类对象的特殊属性。

什么是外观模式(Facade)?

外观模式可以封装复杂的代码底层逻辑,只提供使用者关心的上层接口。

如何实现外观模式(Facade)?

外观模式创建一个上层应用类,它调用了项目中各种类对象和函数,但是对用户隐藏具体的调用细节,而是提供了一个个简单函数接口供用户使用。

/**********外观模式**********/ 

#include <iostream>
using namespace std;

//A部分 
class PartA{
	public:
		void Run(){
			cout << "PartA" << endl;  //打印PartA 
		}
};

//B部分 
class PartB{
	public:
		void Working(){
			cout << "PartB" << endl;  //打印PartA 
		}
};


//上层应用类,封装底层逻辑,提供上层接口 
class App{
	public:
		//构造函数赋值 
		App(PartA *a, PartB *b ): m_a(a), m_b(b) {}

		//析构函数,释放所有底层对象 
		~App(){
			if (m_a != NULL ) delete m_a;
			if (m_a != NULL ) delete m_b;
		}

		//上层接口
		void Operate(){
			m_a -> Run();  //调用A部分函数
			cout << "==================================" << endl; //分割线 
			m_b -> Working();  //调用B部分函数
		}

	private:
		PartA *m_a;  //A部分对象 
		PartB *m_b;  //B部分对象 
};

//测试函数 
void Facade_Test(){
	App *app = new App(new PartA, new PartB);  //创建上层应用,并传入各种底层对象
	 
	app -> Operate();  //调用上层接口

	delete app;
	app = NULL; 
}

int main(){
	Facade_Test();
	return 0; 
} 

外观模式(Facade)的优缺点?

  1. 优点:化繁为简,用户不必关心复杂的底层实现逻辑,使用方便,应用广泛。
  2. 缺点:用户无法实现自定义功能,只能交由应用设计者进行迭代升级。

什么是代理模式(Proxy)?

代理模式为某种对象提供一种外部代理,以控制其他对象对它的访问,相当于是一个中介。

如何实现代理模式(Proxy)?

代理模式创建了一个抽象基类,其子类有被代理类和代理类,它们有着共同的函数接口,因此对外界来说两者的使用方式是相同的。被代理类实现了功能的所有运行逻辑,代理类则通过组合的方式保存并管理被代理类对象,以此控制外部对被代理对象的访问和操作。

/**********代理模式**********/ 

#include <iostream>
using namespace std;

//系统抽象基类 
class System{
	public:
		virtual void Run() = 0; //纯虚函数,交由子类实现 
};

//我自己实现的系统 
class MySystem: public System{
	public:
		void Run(){
			cout << "Mysystem is running" << endl;  //打印系统正在运行... 
		}
};

//外部代理,外部的系统使用者只能通过代理来使用我的系统 
class Proxy: public System{
	public:
		//构造函数赋值,创建代理时默认创建新系统
		Proxy(): m_system(new MySystem) {} 

		//析构函数,释放底层系统对象
		~Proxy(){
			if (m_system != NULL ) delete m_system;
		} 

		//运行系统,但在底层会先经过代理的特殊处理,之后才真正运行底层系统
		void Run(){
			cout << "Proxy is tackling..." << endl;
			if ( m_system != NULL ){
				m_system -> Run(); 
			}
		}
	private:
		MySystem *m_system;  //底层系统对象 
};

//测试函数 
void Proxy_Test(){
	Proxy *p = new Proxy;  //创建代理 
	p -> Run();  //通过代理运行系统

	delete p;
	p = NULL; 
}

int main(){
	Proxy_Test();
	return 0; 
} 

代理模式(Proxy)的优缺点?

  1. 优点:降低对象间的耦合度,对象间的交流需要通过代理来传递,外界无法得知对象内部的信息,甚至可以做到一个对象通过一个代理来与多个对象进行交互,有着较高的安全性、扩展性和维护性。
  2. 缺点:增加了代码复杂度,对象间的交流效率可能会因为代理而降低。

适配器模式、装饰器模式和代理模式的区别?

三者的代码实现非常相似,都用到了继承和组合,但在实现细节和目的上有着很大不同。

  1. 适配器模式:主要关心接口的转换,在不改变功能的情况下修改某个对象的接口。
  2. 装饰器模式:主要关心功能的扩展,在不改变接口的情况下增加某个对象的功能。
  3. 代理模式:主要关心对象的控制,它既不改变接口也不增加功能,它是对象间交流的中介,起到保护对象安全,控制对象行为的作用。

行为型模式

什么是模版方法模式(Template)?

模板方法模式就是在基类中定义一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤,这样子类就可以在基于父类的架构上有着自己独特的实现。

如何实现模版方法模式(Template)?

创建一个模版抽象基类,在其中定义一个具体的公共算法框架,以及多个纯虚函数作为框架中的算法步骤,然后在各个子类中去重写这些虚函数,以实现自己的独有功能。

/**********模版方法模式**********/ 

#include <iostream>
using namespace std;

//模版抽象基类
class Template{
	public:
		virtual void Step1() = 0; 	//算法步骤1,纯虚函数,由子类实现 
		virtual void Step2() = 0;  //算法步骤2?,纯虚函数,由子类实现

		//算法框架 
		void Run(){  
			Step1();  //先执行算法步骤1
			Step2();  //后执行算法步骤2?
		}
};

//模式A 
class ModelA: public Template{
	public:
		void Step1(){
			cout << "I'm ModelA" << endl;
		}
		void Step2(){
			cout << "I can print AAA" << endl;
		}
};


//模式B 
class ModelB: public Template{
	public:
		void Step1(){
			cout << "I'm ModelB" << endl;
		}
		void Step2(){
			cout << "I can print BBB" << endl;
		}
};

//测试函数 
void Template_Test(){
	Template *t = new ModelA;  //创建模式A
	t -> Run();  //运行

	delete t;
	t = NULL;

	cout << "==================================" << endl; //分割线 

	t = new ModelB;  //创建模式A
	t -> Run();  //运行

	delete t;
	t = NULL;
}

int main(){
	Template_Test();
	return 0; 
} 

模版方法模式(Template)的优缺点?

  1. 优点:代码复用,去除了子类中的重复代码,仅需将某个具体步骤交由子类实现。
  2. 缺点:扩展性差,受到模版的约束,在子类中无法增加或删除某个算法步骤。

什么是策略模式(Strategy)?

策略模式定义了一系列的算法,并且将每种算法都放入独立的类中,在实际使用时这些算法对象可以相互替换,将算法决定权交给使用者而不是编写者。

如何实现策略模式(Strategy)?

策略模式定义了策略抽象基类,它的子类负责定义具体算法,这些算法是相互独立的。功能类会通过组合的方式去使用这些算法,但具体要使用哪个算法则由用户决定。

/**********策略模式**********/ 

#include <iostream>
using namespace std;

//策略抽象基类 
class Strategy{
	public:
		virtual void Operate() = 0; //纯虚函数,交由子类实现 
}; 

//策略A,继承自策略类 
class StrategyA: public Strategy{
	public:
		void Operate(){
			cout << "StrategyA" << endl;  //打印策略A 
		}
};

//策略B,继承自策略类 
class StrategyB: public Strategy{
	public:
		void Operate(){
			cout << "StrategyB" << endl;  //打印策略B 
		}
};

//功能类,使用各种策略(算法) 
class Function{
	public:

		//构造函数赋值
		Function(Strategy *s): m_strategy(s) {} 

		//析构函数,释放策略类对象
		~Function(){
			if (m_strategy != NULL ) delete m_strategy;
		} 
		 
		//策略修改函数,用于修改策略
		void Change(Strategy *s){

			//先释放原来的策略类对象 
			if (m_strategy != NULL ) delete m_strategy;

			m_strategy = s;
		}

		//运行函数,在内部通过策略类对象指针调用指定算法 
		void Run(){
			if ( m_strategy != NULL ) m_strategy -> Operate();  
		}

	private:
		Strategy *m_strategy; //策略类对象指针 
};

//测试函数 
void Strategy_Test(){
	Function *f = new Function(new StrategyA);  //创建功能类,传入策略A 
	f -> Run();  //功能运行

	cout << "==================================" << endl; //分割线  

	f -> Change(new StrategyB);  //修改为策略B
	f -> Run();  //功能运行 

	delete f; 
}

int main(){
	Strategy_Test();
	return 0; 
} 

策略模式(Strategy)的优缺点?

  1. 优点:算法可以由用户动态切换,避免多重条件判断,扩展性良好
  2. 缺点:会增加较多的类,且所有的策略类都需要对外暴露才可以让用户理解与使用。

什么是观察者模式(Observer)?

观察者模式在对象之间定义一对多的依赖,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新,所以也叫“发布-订阅模式”。

如何实现观察者模式(Observer)?

观察者模式由观察者类和主题类(被观察者类)组成。主题类给所有想观察它的观察者们提供了注册、注销以及同步数据的操作,而观察者则提供了一个数据更新操作。当主题类对象发生变化时,就可以通过同步数据使得所有观察者完成数据更新。

/**********观察者模式**********/ 

#include <iostream>
#include <list>
using namespace std;

//观察者抽象基类 
class Observer{
	public:
		virtual void Update() = 0; //纯虚函数,用于观察者的更新,由子类实现 
};

//观察者A 
class ObserverA: public Observer{
	public:
		void Update(){
			cout << "ObserverA: Update Data" << endl; //观察者A更新了数据 
		}
};

//观察者B 
class ObserverB: public Observer{
	public:
		void Update(){
			cout << "ObserverB: Update Data" << endl; //观察者B更新了数据 
		}
};

//主题类,被观察者 
class Subject{
	public:

		//添加观察者
		void Add(Observer *o){   
			m_list.push_back(o);
		}

		//删除观察者
		void Delete(Observer *o){   
			m_list.remove(o);
		}

		//通知所有观察者 
		void Notify(){  
			for ( list<Observer*>::iterator it = m_list.begin(); it != m_list.end(); it++ ){
				(*it) -> Update();  //更新观察者数据
			}
		}

	private:
		list<Observer*> m_list;  //观察者队列 
};

//测试函数 
void Obserser_Test(){
	Subject *s = new Subject;  //创建主题类对象
	Observer *a = new ObserverA;  //创建观察者A 
	Observer *b = new ObserverB;  //创建观察者B 

	//添加观察者
	s -> Add(a); 
	s -> Add(b);  

	//通知所有观察者 
	s -> Notify();

	delete a;
	delete b;
	delete s;
}

int main(){
	Obserser_Test();
	return 0; 
} 

MFC中为什么有时候菜单项会是灰色的?

  1. 优点:观察者和被观察者是相对独立的,两者通过建立某种触发机制来完成数据同步。
  2. 缺点:如果观察者和被观察者之间有循环依赖,则观察行为会使两者进入死循环。观察者只能知道目标发生了某种变化,而不知道引发变化的原因。

参考资料

《Head First设计模式》

《什么是设计模式》

《设计模式:设计模式的七大原则》

《C++设计模式(全23种)》

《设计模式》

《12种设计模式C++源代码》