后端面试知识点总结 设计模式

设计模式

设计模式(Design Pattern)代表了最佳实践。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决问题。

设计模式是一套被反复使用的、多人知晓的、经过分类编目的、代码设计经验的总结。

使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

设计模式是软件设计中常见问题的典型解决方案。每个模式就像一张蓝图,可以通过对齐进行定制来解决代码中的特定设计问题。

设计模式六大原则

单一职责(Single Responsibility Principle)

类的职责要单一,不能将太多的职责放在一个类中。

开闭原则(Open Close Principle)

**对扩展开放,对修改关闭。**在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。

里氏代换原则(Liskov Substitution Principle)

是面向对象设计的基本原则之一。该原则说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤是抽象化,而基类和子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范

依赖倒转原则(Dependence Inversion Principle)

这个原则是开闭原则的基础,具体内容为:针对接口编程,依赖于抽象而不依赖于具体

接口隔离原则(Interface Segregation Principle)

使用多个隔离的接口,比使用单个接口更好。即降低类之间的耦合度。

迪米特法则(Demeter Principle):最少知道原则

一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。

合成复用原则(Composite Reuse Principle)

尽量使用合成/聚合的方式,而不是使用继承。

创建型模式

单例模式(Singleton)

单例模式是一种创建型设计模式,能够保证一个类只有一个实例,并提供一个访问该实例的全局节点

解决的问题

1,保证一个类只有一个实例。最常见的原因是控制某些共享资源的访问权限。

2,为该实例提供一个全局访问节点。和全局变量一样,单例模式也允许在程序的任何地方访问特定对象。但是他可以保护该实例不被其他代码覆盖。

解决方案

所有实例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设置为私有,防止其他对象使用单例类的new运算符
  • 新建一个静态构造方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有该函数的调用都将返回这一缓存对象。

如果代码能够访问单例类,那他就能调用单例类的静态方法。无论何时调用该方法,他总是能够返回相同的对象。

单例模式结构

单例(Singleton)类声明一个名为getInstance获取实例的静态方法来返回其所属类的一个相同实例。

单例的构造方法必须对客户端代码隐藏,调用获取实例方法必须是获取单例对象的唯一方式。

应用场景

  • 如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。
    • 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
  • 如果需要更加严格的控制全局变量,可以使用单例模式。
    • 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
  • 注意:可以随时调整限制并设定生成单例实例的数量, 只需修改 获取实例方法, 即 getInstance 中的代码即可实现。

实现方式

  • 在类中添加一个私有静态成员变量用户保存单例实例。
  • 声明一个公有静态构建方法用于获取单例实例。
  • 在静态方法中实现“延迟初始化”。该方法会在首次被调用时创建一个新对象,并将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。
  • 将类的构造函数设置为私有。类的静态方法仍能调用构造函数,但是其他对象不能调用。
  • 检查客户端代码,将对单例的构造函数的调用替换为对其静态构造方法的调用。

代码实现

// 懒汉式,线程不安全
class Singleton{
     
public:
	static Singleton* getInstance(){
     }
private:
	Singleton(){
     }
	static Singleton *singleton;
};
Singleton* Singleton::singleton = NULL;
Singleton* Singleton::getInstance(){
     
    if(singleton == NULL)
        singleton = new Singleton();
    return singleton;
}

// 懒汉式,线程安全
#include 
class Singleton {
     
private:
    Singleton() {
     }
    static Singleton* singleton;
    static pthread_mutex_t mutex;
public:
    static Singleton* getInstance();
};
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;
Singleton* Singleton::singleton = NULL;
Singleton* Singleton::getInstance(){
     
    if(singleton == NULL){
      
        pthread_mutex_lock(&mutex);
        if(singleton == NULL)
            singleton = new Singleton();
        pthread_mutex_unlock(&mutex);
    }
    return singleton;
}

// 饿汉式,线程安全,初始就创建一个实例
class Singleton{
     
private:
    Singleton (){
     }
    static Singleton* singleton;
public:
    static Singleton* getInstance(){
     
        return singleton;
    }
};
Singleton* Singleton::singleton = new Singleton();

优缺点

优点:

  • 可以保证一个类只有一个实例
  • 获得了一个指向该实例的全局访问节点
  • 仅在首次请求实例对象时对齐初始化

缺点:

  • 违反了单一职责原则。该模式同时解决了两个问题。
  • 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等
  • 该模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。
  • 到哪里的客户端代码单元测试可能比较困难,因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。

工厂方法模式(Builder)

工厂方法模式是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。

解决方案

工厂方法模式建议使用特殊的工厂方法代替对于对象构造函数的直接调用(即使用new运算符)。

但是需要注意:仅当这些产品具有共同的基类或者接口时,子类才能返回不同类型的产品。同时基类中的工厂方法还应将其返回类型声明为这一共有接口。

调用工厂方法的代码(通常被视为客户端代码)无需了解不同子类返回实际对象之间的差别。客户端将所有产品视为抽象的运输。客户端都知道所有运输对象都提供交付方法,但是并不关心其具体实现方式。

工厂方法模式结构

  • 产品(Product):将会对接口进行声明。对于所有由创建者及其子类构建的对象。这些接口都是通用的。
  • 具体产品(Concrete Product):产品接口的不同实现。
  • 创建者(Creator):类声明返回产品对象的工厂方法。该方法的返回对象类型必须与产品接口相匹配。
    • 主要职责并不是创建产品。一般来说,创建者类包含一些与产品相关的核心业务逻辑。工厂方法将这些逻辑处理从具体产品类中分离出来。
  • 具体创建者(Concrete Creator):将会重写基础工厂方法,使其返回不同类型的产品。注意并不一定每次调用工厂方法都会创建新的实例。工厂方法也可以返回缓存、对象池或者其他来源的已有对象。

应用场景

  • 在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可以使用工厂方法。
    • 工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码。
    • 例如, 如果需要向应用中添加一种新产品, 你只需要开发新的创建者子类, 然后重写其工厂方法即可。
  • 如果你希望用户能扩展你软件库或框架的内部组件,可使用工厂方法
    • 解决方案是将各框架中构造组件的代码集中到单个工厂方法中, 并在继承该组件之外允许任何人对该方法进行重写。
  • 如果你希望复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法

实现方法

  • 让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
  • 在创建类中添加一个空的工厂方法。 该方法的返回类型必须遵循通用的产品接口。
  • 在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。 你可能需要在工厂方法中添加临时参数来控制返回的产品类型。
  • 为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。
  • 如果应用中的产品类型太多, 那么为每个产品创建子类并无太大必要, 这时你也可以在子类中复用基类中的控制参数。
  • 如果代码经过上述移动后, 基础工厂方法中已经没有任何代码, 你可以将其转变为抽象类。 如果基础工厂方法中还有其他语句, 你可以将其设置为该方法的默认行为。

代码实现

例子:生产处理器的厂商,一个工厂生产处理器A,一个生产处理器B

class Core{
     
public:
	virutal void show() = 0;
};
// 处理器A
class CoreA: public Core{
     
public:
	void show(){
     cout << "Core A" << endl;}
};
// 处理器B
class CoreB: public Core{
     
public:
	void show(){
     cout << "Core B" << endl;}
};
class Factory{
     
public:
	virtual Core* CreateCore() = 0;
};
// 生产处理器A的工厂
class FactoryA: Factory{
     
public:
	CoreA* CreateCore(){
     return new CoreA;}
};
// 生产处理器B的工厂
class FactoryB: Factory{
     
public:
	CoreB* CreateCore(){
     return new CoreB;}
};

优缺点

优点:

  • 可以避免创建者和具体产品之间的紧密耦合。
  • 单一职责原则。 你可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护。
  • 开闭原则。 无需更改现有客户端代码, 你就可以在程序中引入新的产品类型。

缺点:

  • 应用工厂方法模式需要引入许多新的子类, 代码可能会因此变得更复杂。 最好的情况是将该模式引入创建者类的现有层次结构中。

抽象工厂模式(Abstract Factory)

抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。

解决方案

抽象工厂模式建议为系列中的每件产品明确声明接口 (例如椅子、 沙发或咖啡桌)。 然后, 确保所有产品变体都继承这些接口。 例如, 所有风格的椅子都实现 椅子接口; 所有风格的咖啡桌都实现 咖啡桌接口, 以此类推。

抽象工厂模式结构

  • 抽象产品(Abstract Product):为构成系列产品的一组不同但相关的产品声明接口。
  • 具体产品(Concrete Product):是抽象产品的多种不同类型实现。所有变体都必须实现对应的抽象产品。
  • 抽象工厂(Abstract Factory):接口声明了一组创建各种抽象产品的方法
  • 具体工厂(Concrete Factory):实现抽象工厂的构建方法。每个具体工厂都对应特定产品变体,且仅 创建此种产品实体。
  • 尽管具体工厂会对具体产品进行初始化, 其构建方法签名必须返回相应的抽象产品。 这样, 使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。 客户端 (Client) 只需通过抽象接口调用工厂和产品对象, 就能与任何具体工厂/产品变体交互。

应用场景

  • 如果代码需要与多个不同系列的相关产品交互, 但是由于无法提前获取相关信息, 或者出于对未来扩展性的考虑, 你不希望代码基于产品的具体类进行构建, 在这种情况下, 你可以使用抽象工厂。
    • 抽象工厂为你提供了一个接口, 可用于创建每个系列产品的对象。 只要代码通过该接口创建对象, 那么你就不会生成与应用程序已生成的产品类型不一致的产品。
  • 如果你有一个基于一组抽象方法的类, 且其主要功能因此变得不明确, 那么在这种情况下可以考虑使用抽象工厂模式。
    • 在设计良好的程序中, 每个类仅负责一件事。 如果一个类与多种类型产品交互, 就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。

实现方式

  • 以不同的产品类型与产品变体为维度绘制矩阵。
  • 为所有产品声明抽象产品接口。 然后让所有具体产品类实现这些接口。
  • 声明抽象工厂接口, 并且在接口中为所有抽象产品提供一组构建方法。
  • 为每种产品变体实现一个具体工厂类。
  • 在应用程序中开发初始化代码。 该代码根据应用程序配置或当前环境, 对特定具体工厂类进行初始化。 然后将该工厂对象传递给所有需要创建产品的类。
  • 找出代码中所有对产品构造函数的直接调用, 将其替换为对工厂对象中相应构建方法的调用。

代码实现

例子:生产处理器的厂商,一个工厂生产处理器A,一个生产处理器B,同时公式发展迅速,推出了很多不同种类的处理器(单核或多核),在c++实现中,就需要定义一个个的工厂类。

// 单核
class SingleCore{
     
public:
	virtual void show() = 0;
};

class SingleCoreA{
     
public:
	void show(){
     cout << "SingleCore A" << endl;}
};

class SingleCoreB{
     
public:
	void show(){
     cout << "SingleCore B" << endl;}
};
// 多核
class MultiCore{
     
public:
	virtual void show() = 0;
};
class MultiCoreA{
     
public:
	void show(){
     cout << "MultiCore A" << endl;}
};
class MultiCoreB{
     
public:
	void show(){
     cout << "MultiCore B" << endl;}
};
// 工厂
class Factory{
     
public:
	virtual SingleCore* CreateSingleCore() = 0;
	virtual MultiCore* CreateMultiCore() = 0;
};
// 工厂A,专门生产A处理器
class FactoryA: Factory{
     
public:
	SingleCore* CreateSingleCore(){
     return new SingleCoreA;}
	MultiCore* CreateMultiCore(){
     return new MultiCoreA;}
};
// 工厂B,专门生产B处理器
class FactoryB: Factory{
     
public:
	SingleCore* CreateSingleCore(){
     return new SingleCoreB;}
	MultiCore* CreateMultiCore(){
     return new MultiCoreB;}
};

优缺点

优点:

  • 可以确保同一工厂生成的产品相互匹配。
  • 可以避免客户端和具体产品代码的耦合。
  • 单一职责原则。 你可以将产品生成代码抽取到同一位置, 使得代码易于维护。
  • 开闭原则。 向应用程序中引入新产品变体时, 你无需修改客户端代码。

缺点:

  • 由于采用该模式需要向应用中引入众多接口和类, 代码可能会比之前更加复杂。

生成器模式(Builder,建造者模式)

生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。

解决方案

生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。

生成器模式让你能够分步骤创建复杂对象。 生成器不允许其他对象访问正在创建中的产品。

该模式会将对象构造过程划分为一组步骤,每次创建对象时, 你都需要通过生成器对象执行一系列步骤。 重点在于你无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。

当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。

在这种情况下, 你可以创建多个不同的生成器, 用不同方式实现一组相同的创建步骤。 然后你就可以在创建过程中使用这些生成器 (例如按顺序调用多个构造步骤) 来生成不同类型的对象。

**主管:**可以进一步将用于创建产品的一系列生成器步骤调用抽取成为单独的主管类。 主管类可定义创建步骤的执行顺序, 而生成器则提供这些步骤的实现。

  • 严格来说, 你的程序中并不一定需要主管类。 客户端代码可直接以特定顺序调用创建步骤。 不过, 主管类中非常适合放入各种例行构造流程, 以便在程序中反复使用。
  • 此外, 对于客户端代码来说, 主管类完全隐藏了产品构造细节。 客户端只需要将一个生成器与主管类关联, 然后使用主管类来构造产品, 就能从生成器处获得构造结果了。

生成器模式结构

  • 生成器(Builder):接口声明在所有类型生成器中通用的产品构造步骤。
  • 具体生成器(Concrete Builder):提供构造过程的不同实现。具体生成器也可以构造不遵循通用接口的产品。
  • 产品(Product):最终生成的对象。有不同生成器构造的产品无需属于同一类层次结构或者接口。
  • 主管(Director):定义调用构造步骤的顺序,这样就可以创建和复用特定的产品和配置。
  • 客户端(Client):必须将某个生成器对象与主管类关联。一般情况下,只需要通过主管类构造函数的参数进行一次性关联即可。

应用场景

  • 使用生成器模式可以避免“重叠构造函数”的出现
    • 生成器模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。 应用该模式后, 你再也不需要将几十个参数塞进构造函数里了。
  • 当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用生成器模式。
    • 如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。
    • 基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。
  • 使用生成器构造组合树或其他复杂对象。
    • 生成器模式让你能分步骤构造产品。 你可以延迟执行某些步骤而不会影响最终产品。 你甚至可以递归调用这些步骤, 这在创建对象树时非常方便。
    • 生成器在执行制造步骤时, 不能对外发布未完成的产品。 这可以避免客户端代码获取到不完整结果对象的情况。

实现方法

  • 清晰地定义通用步骤, 确保它们可以制造所有形式的产品。 否则你将无法进一步实施该模式。
  • 在基本生成器接口中声明这些步骤。
  • 为每个形式的产品创建具体生成器类, 并实现其构造步骤。
  • 考虑创建主管类。 它可以使用同一生成器对象来封装多种构造产品的方式。
  • 客户端代码会同时创建生成器和主管对象。
  • 只有在所有产品都遵循相同接口的情况下, 构造结果可以直接通过主管类获取。 否则, 客户端应当通过生成器获取构造结果。

代码实现

例子:以建造小人为例

class Builder{
     
public:
	virtual void BuildHead(){
     }
	virtual void BuildBody(){
     }
	virtual void BuildLeftArm(){
     }
	virtual void BuildRightArm(){
     }
	virtual void BuildLeftLeg(){
     }
	virtual void BuildRightLeg(){
     }
};
class ThinBuilder: Builder{
     
public:
	void BuildHead(){
     cout << "Build Thin Head" << endl;}
	void BuildBody(){
     cout << "Build Thin Body" << endl;}
	void BuildLeftArm(){
     cout << "Build Thin LeftArm" << endl;}
	void BuildRightArm(){
     cout << "Build Thin RightArm" << endl;}
	void BuildLeftLeg(){
     cout << "Build Thin LeftLeg" << endl;}
	void BuildRightLeg(){
     cout << "Build Thin RightLeg" << endl;}
};
class FatBuilder: Builder{
     
public:
	void BuildHead(){
     cout << "Build Fat Head" << endl;}
	void BuildBody(){
     cout << "Build Fat Body" << endl;}
	void BuildLeftArm(){
     cout << "Build Fat LeftArm" << endl;}
	void BuildRightArm(){
     cout << "Build Fat RightArm" << endl;}
	void BuildLeftLeg(){
     cout << "Build Fat LeftLeg" << endl;}
	void BuildRightLeg(){
     cout << "Build Fat RightLeg" << endl;}
};
class Director{
     
private:
	Builder* m_pBuilder;
public:
	Director(Builder* builder){
     m_pBuilder = builder;}
	void Create(){
     
		m_pBuilder->BuildHead();
		m_pBuilder->BuildBody();
		m_pBuilder->BuildLeftArm();
		m_pBuilder->BuildRightArm();
		m_pBuilder->BuildLeftLeg();
		m_pBuilder->BuildRightLeg();
	}
};

// 客户端使用
int main(){
     
	ThinBuilder thin;
	Director director(&thin);
	director.Create();
	retun 0;
}

优缺点

优点:

  • 可以分步创建对象,暂缓创建步骤或者递归运行创建步骤
  • 生成不同形式的产品时,可以复用相同的制造代码
  • 单一职责原则。可以将复杂构造代码从产品的业务逻辑中分离出来。

缺点:

  • 由于该模式需要新增多个类,因此代码整体复杂程度有所增加。

原型模式(Clone)

原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。

解决方案

原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个 克隆方法。

其运作方式如下: 创建一系列不同类型的对象并不同的方式对其进行配置。 如果所需对象与预先配置的对象相同, 那么你只需克隆原型即可, 无需新建一个对象。

原型模式结构

  • 原型(Prototype):接口将对克隆方法进行声明。在绝大部分情况下,其中只会由一个名为clone的方法
  • 具体原型(Concrete Prototype):将实现克隆方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需要处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等。
  • 客户端(Client):可以复制实现了原型接口的任何对象。

应用场景

  • 如果需要复制一些对象,同时有希望代码独立于这些对象所属的具体类,可以使用原型模式。
    • 通常出现在代码需要处理第三方代码通过接口传递过来的对象时。 即使不考虑代码耦合的情况, 你的代码也不能依赖这些对象所属的具体类, 因为你不知道它们的具体信息。
    • 原型模式为客户端代码提供一个通用接口, 客户端代码可通过这一接口与所有实现了克隆的对象进行交互, 它也使得客户端代码与其所克隆的对象具体类独立开来。
  • 如果子类的区别仅在于其对象的初始化方式,那么可以使用该模式来减少子类的数量,别人创建这些子类的目的可能是为了创建特定类型的对象。
    • 在原型模式中, 你可以使用一系列预生成的、 各种类型的对象作为原型。
    • 客户端不必根据需求对子类进行实例化, 只需找到合适的原型并对其进行克隆即可。

实现方式

  • 创建原型接口, 并在其中声明 克隆方法。 如果你已有类层次结构, 则只需在其所有类中添加该方法即可。
  • 原型类必须另行定义一个以该类对象为参数的构造函数。 构造函数必须复制参数对象中的所有成员变量值到新建实体中。 如果你需要修改子类, 则必须调用父类构造函数, 让父类复制其私有成员变量值。
  • 克隆方法通常只有一行代码: 使用 new运算符调用原型版本的构造函数。 注意, 每个类都必须显式重写克隆方法并使用自身类名调用 new运算符。 否则, 克隆方法可能会生成父类的对象。
  • 你还可以创建一个中心化原型注册表, 用于存储常用原型。

代码实现

例如:简历打印。原始的手稿相当于原型,通过复印创造出更多的新简历是原型模式的基本思想。

// 父类
class Resume{
     
protected:
	char* name;
public:
	Result(){
     }
	virtual ~Result(){
     }
	virtual Resume* clone(){
     return NULL;}
	virtual void Set(char* str){
     }
	virtual void Show(){
     }
};

class ResumeA: public Resume{
     
public:
	ResumeA(const char* str);
	ResumeA(const ResumeA &r);
	~ResumeA();
	ResuleA* Clone();
	void show();
};
ResumeA::ResumeA(const char* str){
     
	if(str == NULL){
     
		name = new char[1];
		name[0] = '\0';
	}
	else{
     
		name = new char[strlen(str)+1];
		strcpy(name, str);
	}
}
ResumeA::~ResumeA(){
     delete[] name;}
ResumeA::ResumeA(const Resume& r){
     
	name = new char[strlen(r.name)+1];
	strcpy(name, r.name);
}
ResumeA* ResumeA::Clone(){
     
	return new ResumeA(*this);
}
void ResumeA::Show(){
     
	cout << "ResumeA name:" << name << endl;
}

// 客户端
int main(){
     
    Resume* r1 = new ResumeA("A");
    Resume* r2 = new ResumeB("B");
    Resume* r3 = r1->Clone();
    Resume* r4 = r2->Clone();
    r1->Show();r2->Show();
    delete r1; delete r2;
    r1 = r2 = NULL;
    // 深拷贝对于r3,r4无影响
    r3->Show(); r4->Show();
    delete r3; delete r4;
    r3 = r4 = NULL;
    return 0;
}

优缺点

优点:

  • 可以克隆对象,而无需与他们所属的具体类相耦合
  • 可以克隆预生成原型,避免反复运行初始化代码
  • 可以更方便生成复杂对象
  • 可以用继承以外的方法来处理复杂对象的不同配置

缺点:

  • 克隆包含循环引用的复杂对象可能非常复杂。

结构型模型

适配器模型(Adapter,Wrapper)

适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。

解决方案

  • 可以创建一个适配器。 这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。
  • 适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。 例如, 你可以使用一个将所有数据转换为英制单位 (如英尺和英里) 的适配器封装运行于米和千米单位制中的对象。
  • 适配器不仅可以转换不同格式的数据, 其还有助于采用不同接口的对象之间的合作。
  • 有时你甚至可以创建一个双向适配器来实现双向转换调用。

适配器模式结构

对象适配器:

  • 客户端(Client):包含当前程序业务逻辑的类
  • 客户端接口(Client Interface):描述了其他类与客户端代码合作时必须遵循的协议。
  • 服务(Service):客户端与其接口不兼容,因此无法直接调用其功能。
  • 适配器(Adapter):一个可以同时与客户端和服务端交互的类:他在实现客户端接口的同时封装了服务对象。适配器接受客户端通过适配器接口发起的调用,并将其转换为是用于被封装服务对象的调用。
  • 客户端的代码只需通过接口与适配器交互即可。因此,可以向程序中添加新类型的适配器而无需修改已有代码。

类适配器:

  • 类适配器不需要封装任何对象,因为他同时继承了客户端和服务的行为。适配功能在重写的方法中完成。最后生成的适配器可以替代已有的客户端类进行使用。

应用场景

  • 当希望使用某个类,但是其接口与其他代码不兼容时,可以使用适配器类。
    • 适配器模式允许创建一个中间层类,其可做为代码与遗留类、第三方类后者提供怪异接口的类之间的转换器。
  • 如果需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法,但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
    • 你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。

实现方式

  • 确保至少有两个类的接口不兼容:
    • 一个无法修改 (通常是第三方、 遗留系统或者存在众多已有依赖的类) 的功能性服务类。
    • 一个或多个将受益于使用服务类的客户端类。
  • 声明客户端接口, 描述客户端如何与服务交互。
  • 创建遵循客户端接口的适配器类。 所有方法暂时都为空。
  • 在适配器类中添加一个成员变量用于保存对于服务对象的引用。 通常情况下会通过构造函数对该成员变量进行初始化, 但有时在调用其方法时将该变量传递给适配器会更方便。
  • 依次实现适配器类客户端接口的所有方法。 适配器会将实际工作委派给服务对象, 自身只负责接口或数据格式的转换。
  • 客户端必须通过客户端接口使用适配器。 这样一来, 你就可以在不影响客户端代码的情况下修改或扩展适配器。

代码实现

STL中就用到了适配器模式。STL中实现了一种数据结构为双端队列(deque),支持前后两端的插入和删除。STL实现栈和队列时,没有从头开始定义他们,而是直接使用双端队列实现的。这里双端队列就扮演了适配器的角色。队列用到了他的后端插入,前端删除。栈用到了后端插入,后端删除。

// 双端队列
class Deque{
     
public:
	void push_back(int x){
     cout << "deque push_back" << endl;}
	void push_front(int x){
     cout << "deque push_front" << endl;}
	void pop_back(){
     cout << "deque pop_back" << endl;}
	void pop_front(){
     cout << "deque pop_front" << endl;}
};
// 顺序容器
class Sequence{
     
public:
	virtual void push(int x) = 0;
	virtual void pop() = 0;
};
// 栈
class Stack::Sequence{
     
public:
	void push(int x){
     deque.push_back(x);}
	void pop(){
     deque.pop_back();}
private:
	Deque deque;
};
// 队列
class Queue::Sequence{
     
public:
	void push(int x){
     deque.push_back(x);}
	void pop(){
     deque.pop_front();}
private:
	Deque deque;
};

int main(){
     
	Sequence *s1 = new Stack();
	Sequence *s2 = new Queue();
	s1->push(1);s1->pop();
	s2->push(2);s2->pop();
	delete s1; delete s2;
	s1 = s2 = NULL:
	return 0;
}

优缺点

优点:

  • _单一职责原则_你可以将接口或数据转换代码从程序主要业务逻辑中分离。
  • 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

缺点:

  • 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。

桥接模式(Bridge)

桥接模式是一种结构型设计模式, 可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而能在开发时分别使用。

解决方案

问题的根本原因是我们试图在两个独立的维度—形状与颜色—上扩展形状类。 这在处理类继承时是很常见的问题。

桥接模式通过将继承改为组合的方式来解决这个问题。 具体来说, 就是抽取其中一个维度并使之成为独立的类层次, 这样就可以在初始类中引用这个新层次的对象, 从而使得一个类不必拥有所有的状态和行为

桥接模式结构

  • 抽象部分(Abstraction):提供高层控制逻辑,依赖于完成底层实际工作的实现对象。
  • 实现部分(Implementation):为所有具体实现声明通用接口。抽象部分仅能通过在这里声明的方法与实现对象交互。
  • 具体实现(Concrete Implementations):包括特定于平台的代码。
  • 精确抽象(Refined Abstration):提供控制逻辑的变体。于其父类一样,通过通用实现接口与不同的实现进行交互。
  • 通常情况下,客户端仅关心如何与抽象部分合作。但是,客户端需要将抽象对象与一个实现对象连接起来。

应用场景

  • 如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类), 可以使用桥接模式。
    • 类的实现行数越多,弄清其运行方式就越困难,对齐进行修改所花费的时间就越长。桥接模式可以将庞杂类拆分为几个类层次结构。 此后, 你可以修改任意一个类层次结构而不会影响到其他类层次结构。 这种方法可以简化代码的维护工作, 并将修改已有代码的风险降到最低。
  • 如果你希望在几个独立维度上扩展一个类, 可使用该模式。
    • 桥接建议将每个维度抽取为独立的类层次。 初始类将相关工作委派给属于对应类层次的对象, 无需自己完成所有工作。
  • 如果你需要在运行时切换不同实现方法, 可使用桥接模式。

实现方式

  • 明确类中独立的维度。 独立的概念可能是: 抽象/平台, 域/基础设施, 前端/后端或接口/实现。
  • 了解客户端的业务需求, 并在抽象基类中定义它们。
  • 确定在所有平台上都可执行的业务。 并在通用实现接口中声明抽象部分所需的业务。
  • 为你域内的所有平台创建实现类, 但需确保它们遵循实现部分的接口。
  • 在抽象类中添加指向实现类型的引用成员变量。 抽象部分会将大部分工作委派给该成员变量所指向的实现对象。
  • 如果你的高层逻辑有多个变体, 则可通过扩展抽象基类为每个变体创建一个精确抽象。
  • 客户端代码必须将实现对象传递给抽象部分的构造函数才能使其能够相互关联。 此后, 客户端只需与抽象对象进行交互, 无需和实现对象打交道。

代码实现

例子:考虑装操作系统,有多种配置的计算机,同样有多款操作系统,如何使用桥接模式呢?可以将操作系统和计算机分别抽象出来,减少他们的耦合度。

// 操作系统
class OS{
     
public:
  virtual void InstallOS_Imp()
};
class WindowOS: public OS{
     
public:
  void InstallOS_Imp(){
     cout << "Install Window OS" << endl;}
};
class LinuxOS: public OS{
     
public:
  void InstallOS_Imp(){
     cout << "Install Linux OS" << endl;}
};
class MacOS: public OS{
     
public:
  void InstallOS_Imp(){
     cout << "Install Mac OS" << endl;}
};

// 计算机
class Computer{
     
public:
  vitual void InstallOS(OS *os){
     }
};
class DellComputer: public Computer{
     
public:
  void Install(OS *os){
     os->InstallOS_Imp();}
};
class HPComputer: public Computer{
     
public:
  void Install(OS *os){
     os->InstallOS_Imp();}
};
class AppleComputer: public Computer{
     
public:
  void Install(OS *os){
     os->InstallOS_Imp();}
};

int main(){
     
  OS *os1 = new WindowOS();
  OS *os2 = new LinuxOS();
  OS *os3 = new MacOS();
  Computer *computer1 = new AppleComputer();
  computer1->Install(os3);
  return 0;
}

优缺点

优点:

  • 你可以创建与平台无关的类和程序。
  • 客户端代码仅与高层抽象部分进行互动,不会接触到平台的详细信息。
  • 开闭原则。 你可以新增抽象部分和实现部分, 且它们之间不会相互影响。
  • 单一职责原则。 抽象部分专注于处理高层逻辑, 实现部分处理平台细节。
    缺点:
  • 对高内聚的类使用该模式可能会让代码更加复杂。

组合模式(Composite)

组合模式是一种结构性设计模式,你可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。

解决方案

组合模式建议使用一个通用接口来与产品和盒子进行交互, 并且在该接口中声明一个计算总价的方法。
该方式的最大优点在于你无需了解构成树状结构的对象的具体类。

组合模式结构

  • 组件(Component):接口描述了树中简单项目和复杂项目所共有的操作。
  • 叶节点(Leaf):是树的基本结构,它不包含子项目。一般情况下,叶节点最终会完成大部分的实际工作,因为他们无法将工作指派给其他部分。
  • 容器(Container):是包含叶节点或者其他容器等子项目的单位。容器不知道其子项目所属的具体类,他只通过通用的组件接口与其子项目交互。
  • 客户端(Client):通过组件接口与所有项目交互。因此客户端能以相同方式与树状结构中的简单或者复杂项目交互。

应用场景

  • 如果你需要实现树状对象结构,可以使用组合模式。
    • 组合模式为你提供了两种共享公共接口的基本元素类型: 简单叶节点和复杂容器。 容器中可以包含叶节点和其他容器。 这使得你可以构建树状嵌套递归对象结构。
  • 如果你希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式。
    • 组合模式中定义的所有元素共用同一个接口。 在这一接口的帮助下, 客户端不必在意其所使用的对象的具体类。

实现方式

  • 确保应用的核心模型能够以树状结构表示。 尝试将其分解为简单元素和容器。 记住, 容器必须能够同时包含简单元素和其他容器。
  • 声明组件接口及其一系列方法, 这些方法对简单和复杂元素都有意义。
  • 创建一个叶节点类表示简单元素。 程序中可以有多个不同的叶节点类。
  • 创建一个容器类表示复杂元素。 在该类中, 创建一个数组成员变量来存储对于其子元素的引用。 该数组必须能够同时保存叶节点和容器, 因此请确保将其声明为组合接口类型。
    • 实现组件接口方法时, 记住容器应该将大部分工作交给其子元素来完成。
  • 最后, 在容器中定义添加和删除子元素的方法。

代码实现

例子:比如一个集团公司,他有一个母公司,下设很多家子公司。不管是母公司或者子公司,都有各自的财务部,人力部等。对于母公司来说,不论是子公司,还是直属的财务部,人力部等,都是他的部门。

class Company{
     
public:
  Company(string name){
     m_name = name;}
  virtual ~Company(){
     }
  virtual void Add(Company *pCom){
     }
  virtual void Show(int depth){
     }
protected:
  string m_name;
};
// 具体的公司
class ConcreteCompany: public Company{
     
public:
  ConcreteCompany(string name): Company(name){
     }
  virtual ~ConcreteCompany(){
     }
  void Add(Company *pCon){
     m_listCompany.push_back(pCom);}
  void Show(int depth){
     
    for(int i = 0; i < depth; i++)
      cout << "-";
    cout << m_name<<endl;
    list<Company*>::iternator iter = m_listCompany.begin();
    for(; iter != m_listCompany.end(); iter++)
      (*iter)->Show(depth+2);
  }
private:
  list<Company*> m_listCompany;
};

//具体的部门
class FinaneDepartment: public Company{
     
public:
  FinanceDepartment(string name): Company(name){
     }
  vitrual ~FinanceDepartment(){
     }
  virtual void Show(int depth){
     
    for(int i = 0; i < depth; i++)
      cout << "-";
    cout << m_name << endl;
  }
};
class HRDepartment: public Compnay{
     
public:
    HRDepartment(string name): Company(name){
     }
  vitrual ~HRDepartment(){
     }
  virtual void Show(int depth){
     
    for(int i = 0; i < depth; i++)
      cout << "-";
    cout << m_name << endl;
  }
}

优缺点

优点:

  • 你可以利用多态和递归机制更方便地使用复杂树结构。
  • 开闭原则。 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分。
    缺点:
  • 对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,你需要过度一般化组件接口,使其变得令人难以理解。

装饰模式(Wrapper)

装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

解决方案

  • 封装器是装饰模式的别称,这个称谓明确的表达了该模式的主要思想。“封装器”是一个能与其他“目标”对象连接的对象。封装器包含与目标对象相同的一系列方法, 它会将所有接收到的请求委派给目标对象。 但是, 封装器可以在将请求委派给目标前后对其进行处理, 所以可能会改变最终结果。

装饰模式结构

  • 部件(Component):声明封装器和被封装对象的共用接口。
  • 具体部件(Concrete Component):是被封装对象所属的类。它定义了基础行为,但是装饰类可以改变这些行为。
  • 基础装饰(Base Decorator):拥有一个指向被封装对象的引用成员变量。该变量的类型应当被声明为通用部件接口,这样他就可以引用具体的部件和装饰。
  • 具体装饰类(Concrete Decorator):定义了可动态添加到部件的额外行为。具体装饰类会重写装饰基类的方法,并在调用父类方法之前或者之后进行额外的行为。
  • 客户端(Client):可以使用多层装饰来封装不见,只要它能使用通用 接口与所有对象互动即可。

应用场景

  • 如果希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。
    • 装饰能将业务逻辑组织为层次结构, 你可为各层创建一个装饰, 在运行时将各种不同逻辑组合成对象。 由于这些对象都遵循通用接口, 客户端代码能以相同的方式使用这些对象。
  • 如果用继承来扩展对象行为的方案难以实现或者根本不可行,你可以使用该模式。
    • 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装。

实现方式

  • 确保业务逻辑可用一个基本组件及多个额外可选层次表示。
  • 找出基本组件和可选层次的通用方法。 创建一个组件接口并在其中声明这些方法。
  • 创建一个具体组件类, 并定义其基础行为。
  • 创建装饰基类, 使用一个成员变量存储指向被封装对象的引用。 该成员变量必须被声明为组件接口类型, 从而能在运行时连接具体组件和装饰。 装饰基类必须将所有工作委派给被封装的对象。
  • 确保所有类实现组件接口。
  • 将装饰基类扩展为具体装饰。 具体装饰必须在调用父类方法 (总是委派给被封装对象) 之前或之后执行自身的行为。
  • 客户端代码负责创建装饰并将其组合成客户端所需的形式。

代码实现

例如:一个手机,允许为他添加特性,比如增加挂件,手机贴膜等。一种灵活的设计方式是,将手机嵌入到另一个对象中,有这个对象完成特性的添加,我们将这个嵌入的对象为装饰。

class Phone{
     
public:
	Phone(){
     }
	virutal ~Phone(){
     }
	virutal void ShowDecorate(){
     }
};

class iPhone: public Phone{
     
private:
	string m_name;
public:
	iPhome(String name): m_name(name){
     }
	~iPhone(){
     }
	void ShowDecorate(){
     cout << m_name << "的装饰" << endl;}
};

class NokiaPhone: public Phone{
     
private:
	string m_name;
public:
	NokiaPhone(string name): m_name(name){
     }
	~NokiaName(){
     }
	void ShowDecorate(){
     cout << m_name<< "的装饰" << endl;}
};

// 装饰类
class DecoratorPhone: public Phone{
     
private:
	Phone *m_phone;
public:
	DecoratorPhone(Phone *phone): m_phone(phone){
     }
	virtual void ShowDecorate(){
     m_phone->ShowDecorate();}
};

class DecoratorPhoneA: public DecoratorPhone{
     
public:
	DecoratorPhoneA(Phone *phone): DecoratorPhone(phone){
     }
	void ShowDecorate(){
     DecoratorPhone::ShowDecorate(); AddDecorate();}
private:
	void AddDecorate(){
     cout << "增加挂件" << endl;}
};

class DecoratePhoneB: public DecoratorPhone{
     
public:
	DecoratorPhoneB(Phone *phone): DecoratorPhone(phone){
     }
	void ShowDecorate(){
     DecoratorPhone::ShowDecorate(); AddDecorate();}
private:
	void AddDecorate(){
     cout << "屏幕贴膜" << endl;}
};

int main(){
     
    shared_ptr<Phone> phone1(new NokiaPhone("6300"));
    shared_ptr<Phone> dpa(new DecoratorPhoneA(phone1));	// 装饰,增加挂件
    shared_ptr<Phone> dpb(new DecoratorPhoneB(dpa));	// 装饰,屏幕贴膜
    dpb->ShowDecorate();
    return 0;
}

优缺点

优点:

  • 你无需创建新子类即可扩展对象的行为。
  • 你可以在运行时添加或删除对象的功能。
  • 你可以用多个装饰封装对象来组合几种行为。
  • 单一职责原则。 你可以将实现了许多不同行为的一个大类拆分为多个较小的类。

缺点:

  • 在封装器栈中删除特定封装器比较困难。
  • 实现行为不受装饰栈顺序影响的装饰比较困难。
  • 各层的初始化配置代码看上去可能会很糟糕。

外观模式(Facade)

外观模式是一种结构性设计模式,能为程序库、框架或者其他复杂类提供一个简单的接口。

解决方案

外观类为包含许多活动部件的复杂子系统提供一个简单的接口。与直接调用子系统相比,外观提供的功能可能比较有限,但它却包含了客户端真正关心的功能。

如果你的程序需要与包含几十种功能的复杂库整合, 但只需使用其中非常少的功能, 那么使用外观模式会非常方便。

外观模式结构

  • 外观(Facade):提供了一种访问特定子系统功能的便捷方式。其了解如何重定向客户端请求,知晓如何操作一切活动部件。
  • 创建附加外观(Additional Facade):可以避免多种不相关的功能都污染同一个外观,使其变成有一个复杂结构。客户端和其他外观都可以使用附加外观。
  • 复杂子系统(Complext subsystem):由数十个不同对象构成。如果要用这些对象完成有意义的工作,必须深入了解子系统的实现细节。子系统类不会意识到外观的存在,他们在系统内运作并且相互之间可直接进行交互。
  • 客户端(Client):使用外观代替对子系统对象的直接调用。

应用场景

  • 如果你需要一个指向复杂子系统的直接接口, 且该接口的功能有限, 则可以使用外观模式。
    • 子系统通常会随着时间的推进变得越来越复杂。为了解决这个问题, 外观将会提供指向子系统中最常用功能的快捷方式, 能够满足客户端的大部分需求。
  • 如果需要将子系统组织为多层结构, 可以使用外观。
    • 创建外观来定义子系统中各层次的入口。 你可以要求子系统仅使用外观来进行交互, 以减少子系统之间的耦合。

实现方式

  • 考虑能否在现有子系统的基础上提供一个更简单的接口。 如果该接口能让客户端代码独立于众多子系统类, 那么你的方向就是正确的。
  • 在一个新的外观类中声明并实现该接口。 外观应将客户端代码的调用重定向到子系统中的相应对象处。 如果客户端代码没有对子系统进行初始化, 也没有对其后续生命周期进行管理, 那么外观必须完成此类工作。
  • 如果要充分发挥这一模式的优势, 你必须确保所有客户端代码仅通过外观来与子系统进行交互。 此后客户端代码将不会受到任何由子系统代码修改而造成的影响, 比如子系统升级后, 你只需修改外观中的代码即可。
  • 如果外观变得过于臃肿, 你可以考虑将其部分行为抽取为一个新的专用外观类。

代码实现

例子:编译器需要经过4个步骤:词法分析,语法分析,中间代码生成,机器码生成。对于这个系统就可以使用外观模式。

class Sacnner{
     
public:
	void Scan(){
     cout << "词法分析" << endl;}
};
class Parser{
     
public:
	void Parse(){
     cout << "语法分析" << endl;}
};
class GenMidCode{
     
public:
	void GenCode(){
     cout << "中间代码生成" << endl;}
};
class GenMachineCode{
     
public:
	void GenCode(){
     cout << "机器码生成" << endl;}
};

class Compile{
     
public:
	void Run(){
     
		Scanner scanner;
		Parser parser;
		GenMidCode genMidCode;
		GenMachineCode genMachineCode;
		scanner.Scan();
		parser.Parse();
		genMidCode.GenCode();
		genMachineCode.GenCode();
	}
};

优缺点

优点:

  • 可以让自己的代码独立于复杂子系统。

缺点:

  • 外观可能称为程序中所有类都耦合的上帝对象(一个了解过多或者负责过多的对象)。

享元模式(Flyweight)

享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

解决方案

对象的常量数据通常被称为内在状态,其位于对象中,其他对象只能读取但不能修改其数值。而数值的其他状态常常能被其他对象“从外部”改变,因此被称为外在状态。

享元模式建议不在对象中存储外在状态, 而是将其传递给依赖于它的一个特殊方法。 程序只在对象中保存内在状态, 以方便在不同情景下重用。 这些对象的区别仅在于其内在状态 (与外在状态相比, 内在状态的变体要少很多), 因此你所需的对象数量会大大削减。

将仅存储内在状态的对象称为享元

外在状态在大部分情况下,都会被移动到容器对象中,也就是我们应用享元模式前的聚合对象中。

为了能更方便的访问各种享元,可以创建一个工厂方法来管理已有享元对象的缓存池。

享元模式结构

  • 享元模式只是一种优化。在应用该模式之前,要确定程序中存在于大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。
  • 享元(Flyweight):包含原始对象中部分能在多个对象中共享的状态。同一个享元对象可在许多不同情境中使用。享元中存储的状态被称为“内在状态”。传递给享元方法的状态被称为“外在状态”。
  • 情景(context):包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
  • 通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但可以将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。
  • 客户端(Client):负责计算或者存储享元的外在状态。在客户端来看,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。
  • 享元工厂(Flyweight Factory):会对已有享元的缓存池进行管理。

应用场景

  • 仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。
  • 应用该模式所获的收益大小取决于使用它的方式和情景,在下列情况中最有用:
    • 程序需要生成数量巨大的相似对象
    • 将耗尽目标设备的所有内存
    • 对象中包含可抽取且能在多个对象间共享的重复状态。

实现方式

  • 将需要改写为享元的类成员变量拆分为两个部分:
    • 内在状态: 包含不变的、 可在许多对象中重复使用的数据的成员变量。
    • 外在状态: 包含每个对象各自不同的情景数据的成员变量
  • 保留类中表示内在状态的成员变量, 并将其属性设置为不可修改。 这些变量仅可在构造函数中获得初始数值。
  • 找到所有使用外在状态成员变量的方法, 为在方法中所用的每个成员变量新建一个参数, 并使用该参数代替成员变量。
  • 你可以有选择地创建工厂类来管理享元缓存池, 它负责在新建享元时检查已有的享元。 如果选择使用工厂, 客户端就只能通过工厂来请求享元, 它们需要将享元的内在状态作为参数传递给工厂。
  • 客户端必须存储和计算外在状态 (情景) 的数值, 因为只有这样才能调用享元对象的方法。 为了使用方便, 外在状态和引用享元的成员变量可以移动到单独的情景类中。

代码实现

例子:以树为例

class TreeType{
     
private:
    string m_name;
    string m_color;
    string m_texture;
public:
    TreeType(string name, string color, string texture): m_name(name), m_color(color), m_texture(texture){
     
        cout << "construct with " << name << " " << color << " " << texture << endl;
    }
    void draw(int x, int y){
     
            cout<< "TreeType Draw at (" << x << "," << y << ")" << endl;
    }
};

class TreeFactory{
     
public:
    map<string, TreeType> treeTypes;
    TreeType getTreeType(string name, string color, string texture){
     
        map<string, TreeType>::iterator typeIter = treeTypes.find(name + color + texture);
        if(typeIter == treeTypes.end()){
     
            TreeType type(name, color, texture);
            treeTypes.insert(make_pair(name+color+texture, type));
            return type;
        }
        return typeIter->second;
    }
};

class Tree{
     
private:
    int x, y;
    TreeType type;

public:
    Tree(int _x, int _y, TreeType _type) : x(_x), y(_y), type(_type){
     }
    void draw(){
     
        type.draw(x, y);
    }
};

void plantTrees(int x, int y, string name, string color, string texture, TreeFactory& treeFactory, vector<Tree>& trees){
     
    TreeType type = treeFactory.getTreeType(name, color, texture);
    Tree tree(x, y, type);
    trees.push_back(tree);
}

优缺点

优点:

  • 如果程序中有很多相似的对象,那么将可以节省大量内存

缺点:

  • 可能需要牺牲执行速度来换取内存,因为每次调用享元方法时都需要重新计算部分情景数据
  • 代码会变得复杂

代理模式(Proxy)

代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符。 代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。

解决方案

代理模式建议新建一个与原服务对象接口相同的代理类,然后更新应用将代理对象传递给素有原始对象客户端。代理类收到客户端请求后会创建实际的服务对象,并将所有工作委派给他。

比如代理将自己伪装成数据库对象,可以在客户端或者实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。

代理模式结构

  • 服务接口(service interface):声明了服务接口,代理必须遵循该接口才能伪装成服务对象。
  • 服务(service):提供了一些实用的业务逻辑
  • 代理(Proxy):包含一个指向服务对象的引用成员变量。代理完成其任务后会将请求传毒给服务对象。
  • 客户端(Client):能通过统一接口与服务或者代理进行交互。

应用场景

  • 延迟初始化(虚拟代理)。如果有一个偶尔使用的重量级服务对象,一致保持该对象余小宁会消耗系统资源时,可以使用代理模式。
  • 访问控制(保护代理)。如果希望特定客户端使用服务对象,这里的对象可以是操作系统中非常重要的部分,而客户端则是已经启动的程序(包括恶意程序),此时可以使用代理模式。
  • 本地执行远程服务(远程代理)。是用于服务对象位于远程服务器上的情形。
  • 记录日志请求(日志记录代理)。是用于需要保存对于服务对象的请求历史记录时。代理可以向服务传递请求前进行记录。
  • 智能引用。可以在没有客户端使用某个重量级对象时历史销毁该对象。

实现方式

  • 如果没有现成的服务接口,需要创建一个接口来实现代理和服务对象的可交换性。
  • 创建代理类,其中必须包含一个存储指向服务的引用的成员变量。
  • 根据需求实现代理方法。
  • 可以考虑新建一个构建方法来判断客户端可获取的是代理还是实际服务。
  • 可以考虑为服务对象实现延迟初始化。

代码实现

例如:一个可以在文档中嵌入图形对象的文档编辑器。有些图形的创建开销很大,因此可以创建一个图像代理,先不打开图形,需要打开的时候再打开

class Image{
     
public:
	Image(string name) : m_imageName(name){
     }
	virtual ~Image(){
     }
	virtual void Show(){
     }
protected:
	string m_imageName;
};
class BigImage: public Image{
     
public:
	BigImage(string name): Image(name){
     }
	~BigImage(){
     }
	void Show(){
     cout << "Show Big Image" << m_imageName << endl;}
};
class BigImageProxy: public Image{
     
private:
	BigImage *m_bigImage;
public:
	BigImageProxy(string name) : Image(name), m_bigImage(0){
     }
	~BigImageProxy(){
     delete m_bigImage;}
	void Show(){
     
		if(m_bigImage == NULL)
			m_bigImage = new BigImage(m_imageName);
		m_bigImage->Show();
	}
}
int main(){
     
	Image *image = new BigImageProxy("proxy.jpg");
	image->Show();
	delete image;
	return 0;
}

行为模式

责任链模式(职责链模式,Chain of Responsibility)

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

解决方案

与许多其他行为设计模式一样, 责任链会将特定行为转换为被称作处理者的独立对象。

模式建议你将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存对于下一处理者的引用。 除了处理请求外, 处理者还负责沿着链传递请求。 请求会在链上移动, 直至所有处理者都有机会对其进行处理。

处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。

责任链模式结构

  • **处理者(Handler)**声明了所有具体处理者的通用接口。该接口通常仅包含单个方法用于请求处理,但有时其还会包含一个设置链上下个处理者的方法。
  • 基础处理者(Base Handler):是一个可选的类,可以将所有处理者共同的样本代码放置在这里。
  • 具体处理者(Concrete Handler):包含处理请求的实际代码,每个处理者接收到请求后,都必须决定是否进行处理,以及是否沿着链传递请求。处理者通常是独立且不可变的,需要通过构造函数一次性的获取所有必要的数据。
  • 客户端(Client):可根据程序逻辑一次性或者动态的生成链。

应用场景

  • 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。
  • 迪桑必须按照顺序执行多个处理者时,可以使用该模式
  • 如果所需处理者及其顺序必须在运行时可以进行改变,可以使用该模式

实现方式

  • 声明处理者接口并描述请求处理方法的签名。
  • 为了在具体处理者中消除重复的样本代码, 你可以根据处理者接口创建抽象处理者基类。
  • 依次创建具体处理者子类并实现其处理方法。 每个处理者在接收到请求后都必须做出两个决定:
    • 是否自行处理这个请求。
    • 是否将该请求沿着链进行传递。
  • 客户端可以自行组装链, 或者从其他对象处获得预先组装好的链。 在后一种情况下, 你必须实现工厂类以根据配置或环境设置来创建链。
  • 客户端可以触发链中的任意处理者, 而不仅仅是第一个。 请求将通过链进行传递, 直至某个处理者拒绝继续传递, 或者请求到达链尾。
  • 由于链的动态性,客户端需要准备好以下情况:
    • 链中可能只有单个链接。
    • 部分请求可能无法到达链尾。
    • 其他请求可能直到链尾都未被处理。

代码实现

例如:员工要求加薪,需要一层一层批准

class Manager{
     
protected:
	Manager *m_manager;
	string m_name;
public:
	Manager(Manager *manager, string name): m_manager(manager), m_name(name){
     }
	virtual void DealWithRequest(string name, int num){
     }
};
class CommonManager: public Manager{
     
public:
	CommonManager(Manager *manager, string name): Manager(manager, name){
     }
	void DealWithRequest(string name, int num){
     
		if(num < 500)
			cout << "commonmanager " << name << " accept " << "raises $" << num << endl;
		else{
     
			cout << "commonmanager cannot deal" << endl;
			m_manager->DealWithRequest(name, num);
		}
	}
};
class MajorManager: public Manager{
     
public:
	MajorManager(Manager *manager, string name): Manager(manager, name){
     }
	void DealWithRequest(string name, int num){
     
		if(num < 1000)
			cout << "majormanager " << name << " accept " << "raises $" << num << endl;
		else{
     
			cout << "majormanager cannot deal" << endl;
			m_manager->DealWithRequest(name, num);
		}
	}
};
class GeneralManager: public Manager{
     
public:
	GeneralManager(Manager *manager, string name): Manager(manager, name){
     }
	void DealWithRequest(string name, int num){
     
		cout << "generalmanager " << name << " accept " << "raises $" << num << endl;
	}
};

int main(){
     
	Manager *general = new GeneralManager(NULL, "A");
	Manager *major = new MajorManager(general, "B");
	Manager *common = new CommonManager(major, "C");
	common->DealWithRequest("D", 300);
	common->DealWithRequest("E", 1000);
	delete general;
	delete major;
	delete common;
	return 0;
}

优缺点

优点:

  • 可以控制请求处理的顺序
  • 单一职责原则。 你可对发起操作和执行操作的类进行解耦。
  • 开闭原则。 你可以在不更改现有代码的情况下在程序中新增处理者。

缺点:

  • 部分请求可能未被处理

中介者模式(Mediator)

中介者模式是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。

解决方案

中介者模式建议你停止组件之间的直接交流并使其相互独立。 这些组件必须调用特殊的中介者对象, 通过中介者对象重定向调用行为, 以间接的方式进行合作。 最终, 组件仅依赖于一个中介者类, 无需与多个其他组件相耦合。

采用这种方式, 中介者模式让你能在单个中介者对象中封装多个对象间的复杂关系网。 类所拥有的依赖关系越少, 就越易于修改、 扩展或复用。

中介者模式结构

  • 组件(Component):各种包含业务逻辑的类,每个组件都有一个指向中介者的引用,该引用被声明为中介者接口类型。组件不知道中介者实际所属的类。因此可以通过将其连接到不同的中介者以使其能在其他程序中复用。
  • 中介者(Mediator):接口声明了与组件交流的方法,但通常仅包括一个通知方法。组件可将任意上下文(包括自己的对象)作为该方法的参数,只有这样接收组件和发送者类之间才不会耦合。
  • 具体中介者(Concrete Mediator):封装了多种组件间的关系。具体中介者通常会保存所有组件的引用并对其进行管理,甚至有时会对其生命周期进行管理。
  • 组件并不知道其他组建的情况,如果组件内发生了重要事件,他只能通知中介者,中介者收到通知后能轻易地确定发送者,这足以判断接下来需要触发的组件了。

应用场景

  • 当一些对象和其他对象紧密耦合以致难以对其进行修改时,可以使用中介者模式
  • 当组件过于依赖其他组件而无法在不同应用中复用时,可以使用中介者模式
  • 如果为了能在不同情景下复用一些基本行为,导致需要被迫创建大量组件子类时,可以使用中介者模式

实现方式

  • 找到一组当前紧密耦合, 且提供其独立性能带来更大好处的类
  • 声明中介者接口并描述中介者和各种组件之间所需的交流接口。
  • 实现具体中介者类。
  • 可以更进一步让中介者负责组件对象的创建和销毁。
  • 组件必须保存对于中介者对象的引用。 该连接通常在组件的构造函数中建立, 该函数会将中介者对象作为参数传递。
  • 修改组件代码, 使其可调用中介者的通知方法, 而非其他组件的方法。 然后将调用其他组件的代码抽取到中介者类中, 并在中介者接收到该组件通知时执行这些代码。

代码实现

例如:租房为例,如果没有房屋中介,那么房客要自己找房东,房东要自己找房客。

class Mediator;
class Person{
     
portected:
	Mediator *m_mediator;
public:
	virtual void SetMediator(Mediator *mediator){
     }
	virtual void SendMessage(string msg){
     }
	virtual void GetMessage(string msg){
     }
};
class Mediator{
     
public:
	virtual void Send(string msg, Person *person){
     }
	virtual void SetA(Person *A){
     }
	virtual void SetB(Person *B){
     }
};
class Renter: public Person{
     
public:
	void SetMediator(Mediator *mediator){
     m_mediator = mediator;}
	void SendMessage(string msg){
     m_mediator->Send(msg, this);}
	void GetMessage(string msg){
     cout << "renter get msg" << endl;}
};
class Landlord: public Person{
     
public:
	void SetMediator(Mediator *mediator){
     m_mediator = mediator;}
	void SendMessage(string msg){
     m_mediator->Send(msg, this);}
	void GetMessage(string msg){
     cout << "landlord get msg" << endl;}
};
class HouseMediator: public Mediator{
     
private:
	Person *m_A;
	Person *m_B;
public:
	HouseMediator(): m_A(NULL), m_B(NULL){
     }
	void SetA(Person *A){
     m_A = A;}
	void SetB(Person *B){
     m_B = B;}
	void Send(string msg, Person *person){
     
		if(person == m_A)
			m_B->GetMessage(msg);
		else
			m_A->GetMessage(msg);
	}
};

优缺点

优点:

  • 单一职责原则。 你可以将多个组件间的交流抽取到同一位置, 使其更易于理解和维护。
  • 开闭原则。 你无需修改实际组件就能增加新的中介者。
  • 你可以减轻应用中多个组件间的耦合情况。
  • 你可以更方便地复用各个组件。

缺点:

  • 一段时候后,中介者 可能会演化为上帝对象。

观察者模式(Observer)

观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。

解决方案

拥有一些值得关注的状态的对象通常被称为目标, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者 (publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。

观察者模式建议为发布者类添加订阅机制,让每个对象都能订阅或者取消订阅发布者消息流。

实际上,该机制包括:1,一个用于存储订阅者对象引用的列表成员变量;2,几个用于添加或者删除该列表中订阅者的公有方法。

观察者模式结构

  • 发布者(Publisher):会向其他对象发送值得关注的事件。
  • 当新事件发生时,发送者会遍历订阅列表并调用每个订阅者对象的通知方法,该方法是在订阅者接口中声明的。
  • 订阅者(Subscriber):接口声明了通知接口。在绝大多数情况下,该接口仅包含一个更新方法。该方法可以拥有多个参数,使发布者能在更新时传递事件的详细参数。
  • 具体订阅者(Concrete Subscriber):可以执行一些操作来回应发布者的通知。
  • 订阅者通常需要一些上下文信息来正确的处理更新,因此发布这通常会将一些上下文数据作为通知方法的参数进行传递。
  • 客户端(Client):分别创建发布者和订阅者对象,然后为订阅者注册发布者更新。

应用场景

  • 当一个对象状态的改变需要改变其他对象,或者实际对象是事先未知的或者动态变化的时,可以使用观察者模式。
  • 当应用中的一些对象必须观察其他对象时,可以使用观察者模式。但仅能在有限时间内或者特定情况下使用。
  • 比如说在一个二维地图中有主角和各种npc怪物,主角移动到怪物附近怪物需要攻击主角,就可以用观察者模式。

实现方式

  • 仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。
  • 声明订阅者接口。 该接口至少应声明一个 update方法。
  • 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。
  • 确定存放实际订阅列表的位置并实现订阅方法。
  • 创建具体发布者类。
  • 在具体订阅者类中实现通知更新的方法。
  • 客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。

代码实现

例如:博主和读者的问题

class Observer{
     
public:
	Observer(){
     }
	virtual ~Observer(){
     }
	virutal void Update(){
     }
};
class Blog{
     
public:
	Blog(){
     }
	~Blog(){
     }
	void Attach(Observer *observer){
     m_observers.push_back(observer);}
	void Remove(Observer *observer){
     m_observers.remove(observer);}
	void Notify(){
     
		list<Observer*>::iterator iter = m_observers.begin();
		for(; iter != m_observers.end(); iter++)
			(*iter)->Update();
	}
	virtual void SetStatus(string s){
     m_status = s;}
	virtual string GetStatus(){
     return m_status;}
private:
	list<Observer*> m_observers;
protected:
	stdring m_status;
};
class BlogCSDN: public Blog{
     
private:
	string m_name;
public:
	BlogCSDN(string name): m_name(name){
     }
	~BlogCSDN(){
     }
	void SetStatus(string s){
     m_status="CSDN message: "+m_name+s;}
	string GetStatus(){
     return m_status;}
};
class ObserverBlog: public Observer{
     
private:
	string m_name;
	Blog *m_blog;
public:
	ObserverBlog(string name, Bolg *blog): m_name(name), m_blog(blog){
     }
	~ObserverBolg(){
     }
	void Update(){
     
		string status = m_blog->GetStatus();
		cout << m_name << "--------" << status << endl;
	}
};

优缺点

优点:

  • 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
  • 你可以在运行时建立对象之间的联系。

缺点:

  • 订阅者的通知顺序是随机的。

迭代器模式(Iterator)

迭代器模式是一种行为设计模式,让你能在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。

解决方案

迭代器模式的主要思想就是将集合的遍历行为抽取为单独的迭代器模式。

迭代器模式结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D1WayPxn-1602222462333)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/iterator.png)]

  • 迭代器接口声明了遍历集合所需的操作:获取下一个元素,获取当前位置和重新开始迭代等
  • 具体迭代器实现了遍历集合的一种特定算法。迭代器对象必须跟踪自身遍历的进度。这使得多个迭代器客户相互独立的遍历同一个集合。
  • 集合接口声明一个或者多个方法来获取于集合兼容的迭代器,请注意,返回方法的类型必须被声明为迭代器接口,因此具体集合可以返回各种 不同种类的迭代器。
  • 具体集合会在客户端请求迭代器时返回一个特定的具体迭代器类实体。
  • 客户端通过集合和迭代器的接口与两者进行交互。这样一个客户端无需与具体类进行耦合,允许同一个客户端代码使用各种不同的集合和迭代器。

应用场景

  • 当集合背后为复杂的数据结构,并且希望对客户端隐藏其复杂性时(出于使用便利性或者安全性考虑),可以使用迭代器模型
  • 使用该模式可以减少程序中重复的代码
  • 如果希望代码能够遍历不同的甚至是无法预知的数据机构,可以使用迭代器模式

实现方式

  • 声明迭代器接口,该接口必须提供至少一个方法来获取集合中的下个元素。但为了使用方便,还可以使用方便,还可以添加一些其他方法。
  • 声明集合接口并描述一个获取迭代器的方法。其返回值必须是迭代器接口。
  • 为希望使用迭代器进行遍历的集合实现具体迭代器类。迭代器对象必须与单个集合实体链接。链接关系通常通过迭代器的构造函数建立
  • 在你的集合类中实现集合接口。主要思想是针对特定集合为客户端代码提供创建迭代器的快捷方式。
  • 检查客户端代码,使用迭代器替换所有集合遍历函数。

优缺点

  • 单一职责原则。通过将体积庞大的遍历算法代码抽取为独立的类,可以对客户端代码和集合进行整理
  • 开闭原则。可以实现新型的集合和迭代器并将其传递给现有代码,无需修改现有代码
  • 可以并行遍历同一集合,因为每个迭代器对象都包含其自身的遍历状态

缺点:

  • 如果程序只与简单的集合进行交互,该模式可能矫枉过正
  • 对于某些特殊的集合,使用迭代器可能比直接遍历的效率低。

你可能感兴趣的:(知识点总结,面试知识点,设计模式,c++)