常见的设计模式

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

这些模式可以分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)、行为型模式(Behavioral Patterns)

单例模式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象,属于创建型模式。

注意:

  1. 单例类只能有一个实例。

  2. 单例类必须自己创建自己的唯一实例。

  3. 单例类必须给所有其他对象提供这一实例。

简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它们的实例。

getInstance()的返回值是一个对象的引用,并不是一个新的实例,所以不要错误的理解成多个对象。

一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

常见写法
饿汉式
饿汉式单例模式是指在程序启动时就创建唯一实例的单例模式

#include<iostream>
using namespace std;

// 使用指针来在类内部定义存放实例的对象
class Singleton {
private:
    static Singleton* instance; // 静态成员变量,用于存储唯一实例的指针
    Singleton() {} // 私有构造函数,防止外部创建实例
public:
    static Singleton* getInstance() { // 必须得是静态成员函数,用于获取唯一实例的指针,因为不能与某一实例绑定
        return instance; // 直接返回唯一实例的指针
    }
};

// 类中的静态成员需要在类外初始化
Singleton* Singleton::instance = new Singleton(); // 在程序启动时创建唯一实例

int main() {
	Singleton* s1 = Singleton::getInstance(); // 获取唯一实例的指针
	Singleton* s2 = Singleton::getInstance(); // 再次获取唯一实例的指针
	if (s1 == s2) { // 判断两个指针是否相等,即是否为同一个实例
		cout << "s1 and s2 are the same instance." << endl;
	}
	else {
		cout << "s1 and s2 are different instances." << endl;
	}
	return 0;
}

饿汉式单例模式的优点是实现简单,线程安全,不需要加锁等复杂操作。缺点是在程序启动时就创建实例,浪费内存

加锁的饱汉式

#include <mutex>

class Singleton {
private:
    static Singleton* instance; // 静态成员变量,用于存储唯一实例的指针
    static std::mutex mtx; // 静态成员变量,用于加锁
    Singleton() {} // 私有构造函数,防止外部创建实例
public:
    static Singleton* getInstance() { // 必须得是静态成员函数,用于获取唯一实例的指针,因为不能与某一实例绑定,由于函数为静态,被它访问的成员也都得是静态的
        std::lock_guard<std::mutex> lock(mtx); // 加锁
        if (instance == nullptr) { // 如果实例不存在,则创建一个新实例
            instance = new Singleton();
        }
        return instance; // 返回唯一实例的指针
    }
};

// 类中的静态成员需要在类外初始化
Singleton* Singleton::instance = NULL; 
mutex Singleton::mtx;

饱汉式单例模式的优点是在第一次使用时才创建实例,避免了浪费系统资源的问题。缺点是在多线程环境下可能会出现多个线程同时调用getInstance函数,导致创建多个实例的情况。为了保证线程安全,使用加锁的方式来避免多个线程同时创建实例。

这里使用std::mutex来实现加锁,保证了在多线程环境下只有一个线程可以创建实例。在getInstance函数中,我们使用了std::lock_guard来自动加锁和解锁,避免了手动加锁和解锁的繁琐操作。

std::lock_guard是一个RAII(Resource Acquisition Is Initialization)类,用于在作用域内自动获取和释放互斥锁。它可以确保在作用域结束时自动释放互斥锁,从而避免了手动释放锁的繁琐和容易出错的操作。

lock_guard是一个模板类,可以用于不同类型的互斥锁。lock_guard的构造函数接受一个互斥锁的引用,并在构造函数中获取互斥锁。lock_guard的析构函数会在作用域结束时自动释放互斥锁。

std::lock_guard可以避免死锁的发生。由于std::lock_guard在构造函数中获取互斥锁,因此可以确保在获取锁时不会发生死锁。

std::lock_guard禁止拷贝和赋值操作,从而确保同一时刻只有一个std::lock_guard对象可以持有互斥锁。

何时使用:当您想控制实例数目,节省系统资源的时候。

关键代码:构造函数是私有的。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

优点

  1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。

  2. 避免对资源的多重占用(比如写文件操作)。

缺点

  1. 没有接口,不能继承。

  2. 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

观察者模式

对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。观察者模式属于行为型模式。

具体而言就是 观察者通过接口提供给被观察者通知消息的能力:当被观察者发生变化时,遍历所有对其的观察者,挨个调用接口通知。注意,如果顺序执行遍历,某一观察者错误会导致系统卡壳,一般采用异步方式。

常见写法

#include <iostream>
#include <vector>
#include <string>


using namespace std;

class Observer { // 抽象观察者类
public:
	virtual void update() = 0; // 更新方法
};

class Subject { // 抽象主题类
private:
	vector<Observer*> observers; // 观察者列表
public:
	void attach(Observer* observer) { // 添加观察者
		observers.push_back(observer);
	}
	void detach(Observer* observer) { // 删除观察者
		for (auto it = observers.begin(); it != observers.end(); ++it) {
			if (*it == observer) {
				observers.erase(it);
				break;
			}
		}
	}
	void notify() { // 通知所有观察者
		for (auto observer : observers) {
			observer->update();
		}
	}
};

class ConcreteObserver : public Observer { // 具体观察者类
private:
	string name;
public:
	void update() override { // 更新方法
		cout << name << " is notified." << endl;
	}
	ConcreteObserver(string a):name(a){}
};

int main() {
	Subject subject; // 创建主题对象
	ConcreteObserver observer1("a"), observer2("b"); // 创建观察者对象
	subject.attach(&observer1); // 添加观察者1
	subject.attach(&observer2); // 添加观察者2
	subject.notify(); // 通知所有观察者
	subject.detach(&observer2); // 删除观察者2
	subject.notify(); // 通知所有观察者
	return 0;
} 

运行结果

a is notified.
b is notified.
a is notified.

在上面的示例中,我们定义了一个Observer抽象观察者类和一个Subject抽象主题类。Observer类中定义了一个update方法,用于更新观察者对象。Subject类中定义了一个观察者列表,以及attachdetachnotify三个方法,分别用于添加观察者、删除观察者和通知所有观察者。

main函数中,我们创建了一个主题对象和两个观察者对象,并将观察者对象添加到主题对象的观察者列表中。然后,我们调用notify方法通知所有观察者,并删除一个观察者对象,再次调用notify方法通知所有观察者。

关键代码:在抽象类里有一个数组用于存放观察者们。

优点

  1. 观察者和被观察者是抽象耦合的。
  2. 可以建立一套触发机制。

缺点

  1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

装饰器模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
可以动态增加功能,动态撤销。就增加功能来说,装饰器模式相比生成子类更为灵活。

一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。在不想增加很多子类的情况下扩展类,就可以使用装饰器模式。

简单的说就是在继承类的内部定义一个基类对象,继承类中的方法通过基类对象调用基类方法,接着在此基础上扩展基类方法的功能。

#include <iostream>

using namespace std;

class Component { // 抽象组件类
public:
	virtual void operation() = 0; // 操作方法
};

class ConcreteComponent : public Component { // 具体组件类
public:
	void operation() override { // 实现操作方法
		cout << "ConcreteComponent::operation" << endl;
	}
};

class Decorator : public Component { // 抽象装饰器类
private:
	Component* component; // 持有组件对象的指针
public:
	Decorator(Component* component) { // 构造函数,传入组件对象的指针
		this->component = component;
	}
	void operation() override { // 实现操作方法,调用组件对象的操作方法
		component->operation();
	}
};

class ConcreteDecorator : public Decorator { // 具体装饰器类
public:
	ConcreteDecorator(Component* component) : Decorator(component) {} // 构造函数,传入组件对象的指针
	void operation() override { // 实现操作方法,调用组件对象的操作方法,并添加额外的功能
		Decorator::operation();
		cout << "ConcreteDecorator::operation" << endl;
	}
};

int main() {
	Component* component = new ConcreteComponent(); // 创建具体组件对象
	component->operation(); // 调用具体组件对象的操作方法
	Component* decorator = new ConcreteDecorator(component); // 创建具体装饰器对象,传入具体组件对象的指针
	decorator->operation(); // 调用具体装饰器对象的操作方法,实际上调用了具体组件对象的操作方法,并添加了额外的功能
	return 0;
}

在上面的示例中,我们定义了一个Component抽象组件类和一个ConcreteComponent具体组件类。Component类中定义了一个操作方法,ConcreteComponent类中实现了操作方法。

然后,我们创建了一个Decorator抽象装饰器类,它持有一个Component组件对象的指针,并实现了操作方法,调用了组件对象的操作方法。接着,我们创建了一个ConcreteDecorator具体装饰器类,它继承了Decorator类,并在操作方法中调用了Decorator类的操作方法,并添加了额外的功能。

main函数中,我们创建了一个具体组件对象和一个具体装饰器对象,并将具体组件对象的指针传入具体装饰器对象的构造函数中。然后,我们调用具体组件对象的操作方法和具体装饰器对象的操作方法,实际上调用了具体组件对象的操作方法,并添加了额外的功能。

关键代码

  1. Component 类充当抽象角色,不应该具体实现。
  2. 修饰类引用和继承 Component 类,具体扩展类重写父类方法。

优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

缺点:多层装饰比较复杂。

适配器模式

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。

这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。它将两种完全不同的事物联系到一起,就像现实生活中的变压器。

适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。

JAVA 中的 jdbc( Java 数据库连接)就是适配器模式的实例。

#include <iostream>

using namespace std;

class Target { // 目标接口
public:
	virtual void request() = 0; // 请求方法
};

class Adaptee { // 源接口
public:
	void specificRequest() { // 特殊请求方法
		cout << "源接口特殊请求方法" << endl;
	}
};

class Adapter : public Target { // 适配器类
private:
	Adaptee* adaptee; // 持有源接口的指针
public:
	Adapter(Adaptee* adaptee) { // 构造函数,传入源接口的指针
		this->adaptee = adaptee;
	}
	void request() override { // 请求方法,调用源接口的特殊请求方法
		adaptee->specificRequest();
	}
};

int main() {
	Adaptee adaptee; // 创建源接口对象
	Adapter adapter(&adaptee); // 创建适配器对象,传入源接口的指针
	adapter.request(); // 调用目标接口的请求方法,实际上调用了源接口的特殊请求方法
	return 0;
}

在上面的示例中,我们定义了一个Target目标接口和一个Adaptee源接口。Target接口中定义了一个请求方法,Adaptee接口中定义了一个特殊请求方法。

然后,我们创建了一个Adapter适配器类,它继承了Target接口,并持有一个Adaptee接口的指针。在Adapter类中,我们实现了请求方法,调用了源接口的特殊请求方法。

main函数中,我们创建了一个源接口对象和一个适配器对象,并将源接口的指针传入适配器对象的构造函数中。然后,我们调用目标接口的请求方法,实际上调用了适配器对象的请求方法,进而调用了源接口的特殊请求方法。

实际上完成了从目标接口到源接口的转换。

如何解决:继承或依赖(推荐)。

关键代码:适配器类继承或依赖已有的对象,然后在适配器类中实现想要的目标接口。

优点

  1. 可以让任何两个没有关联的类一起运行。

  2. 提高了类的复用。

  3. 增加了类的透明度。

  4. 灵活性好。

缺点

  1. 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。

  2. 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。

工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

具体而言, 先定义一个抽象的接口表示抽象产品,定义多个抽象接口的实现类表示具体产品,定义一个工厂类用来实例化抽象的接口表示生产产品

#include <iostream>

using namespace std;

class Product { // 抽象产品类
public:
    virtual void use() = 0; // 使用方法
};

class ConcreteProductA : public Product { // 具体产品类A
public:
    void use() override { // 实现使用方法
        cout << "ConcreteProductA::use" << endl;
    }
};

class ConcreteProductB : public Product { // 具体产品类B
public:
    void use() override { // 实现使用方法
        cout << "ConcreteProductB::use" << endl;
    }
};

class Factory { // 抽象工厂类
public:
    virtual Product* createProduct() = 0; // 创建产品方法
};

class ConcreteFactoryA : public Factory { // 具体工厂类A
public:
    Product* createProduct() override { // 实现创建产品方法,返回具体产品类A的对象
        return new ConcreteProductA();
    }
};

class ConcreteFactoryB : public Factory { // 具体工厂类B
public:
    Product* createProduct() override { // 实现创建产品方法,返回具体产品类B的对象
        return new ConcreteProductB();
    }
};

int main() {
    Factory* factoryA = new ConcreteFactoryA(); // 创建具体工厂类A的对象
    Product* productA = factoryA->createProduct(); // 使用具体工厂类A的对象创建具体产品类A的对象
    productA->use(); // 调用具体产品类A的使用方法
    Factory* factoryB = new ConcreteFactoryB(); // 创建具体工厂类B的对象
    Product* productB = factoryB->createProduct(); // 使用具体工厂类B的对象创建具体产品类B的对象
    productB->use(); // 调用具体产品类B的使用方法
    return 0;
}

在上面的示例中,我们定义了一个Product抽象产品类和两个ConcreteProduct具体产品类。Product类中定义了一个使用方法,ConcreteProduct类中实现了使用方法。

然后,我们创建了一个Factory抽象工厂类和两个ConcreteFactory具体工厂类。Factory类中定义了一个创建产品方法,ConcreteFactory类中实现了创建产品方法,返回具体产品类的对象。

main函数中,我们创建了一个具体工厂类A的对象和一个具体产品类A的对象,并调用具体产品类A的使用方法。然后,我们创建了一个具体工厂类B的对象和一个具体产品类B的对象,并调用具体产品类B的使用方法。

关键代码:创建过程在其子类执行。

应用实例:Hibernate 换数据库只需换方言和驱动就可以

主要解决:主要解决接口选择的问题。

何时使用:我们明确地计划不同条件下创建不同实例时。

如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。

优点

  1. 一个调用者想创建一个对象,只要知道其名称就可以了。

  2. 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

  3. 屏蔽产品的具体实现,调用者只关心产品的接口。

缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

抽象工厂模式

抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

有四个角色,抽象工厂模式,具体工厂模式,抽象产品模式,具体产品模式。不再是由一个工厂类去实例化具体的产品,而是由抽象工厂的子类去实例化产品。可以理解为是工厂类的工厂类。

#include <iostream>

using namespace std;

class ProductA { // 抽象产品A类
public:
	virtual void use() = 0; // 使用方法
};

class ConcreteProductA1 : public ProductA { // 具体产品A1类
public:
	void use() override { // 实现使用方法
		cout << "ConcreteProductA1::use" << endl;
	}
};

class ConcreteProductA2 : public ProductA { // 具体产品A2类
public:
	void use() override { // 实现使用方法
		cout << "ConcreteProductA2::use" << endl;
	}
};

class ProductB { // 抽象产品B类
public:
	virtual void eat() = 0; // 吃方法
};

class ConcreteProductB1 : public ProductB { // 具体产品B1类
public:
	void eat() override { // 实现吃方法
		cout << "ConcreteProductB1::eat" << endl;
	}
};

class ConcreteProductB2 : public ProductB { // 具体产品B2类
public:
	void eat() override { // 实现吃方法
		cout << "ConcreteProductB2::eat" << endl;
	}
};

class Factory { // 抽象工厂类
public:
	virtual ProductA* createProductA() = 0; // 创建产品A方法
	virtual ProductB* createProductB() = 0; // 创建产品B方法
};

class ConcreteFactory1 : public Factory { // 具体工厂1类
public:
	ProductA* createProductA() override { // 实现创建产品A方法,返回具体产品A1类的对象
		return new ConcreteProductA1();
	}
	ProductB* createProductB() override { // 实现创建产品B方法,返回具体产品B1类的对象
		return new ConcreteProductB1();
	}
};

class ConcreteFactory2 : public Factory { // 具体工厂2类
public:
	ProductA* createProductA() override { // 实现创建产品A方法,返回具体产品A2类的对象
		return new ConcreteProductA2();
	}
	ProductB* createProductB() override { // 实现创建产品B方法,返回具体产品B2类的对象
		return new ConcreteProductB2();
	}
};

int main() {
	Factory* factory1 = new ConcreteFactory1(); // 创建具体工厂1类的对象
	ProductA* productA1 = factory1->createProductA(); // 使用具体工厂1类的对象创建具体产品A1类的对象
	productA1->use(); // 调用具体产品A1类的使用方法
	ProductB* productB1 = factory1->createProductB(); // 使用具体工厂1类的对象创建具体产品B1类的对象
	productB1->eat(); // 调用具体产品B1类的吃方法
	delete productA1; // 释放具体产品A1类的对象
	delete productB1; // 释放具体产品B1类的对象
	Factory* factory2 = new ConcreteFactory2(); // 创建具体工厂2类的对象
	ProductA* productA2 = factory2->createProductA(); // 使用具体工厂2类的对象创建具体产品A2类的对象
	productA2->use(); // 调用具体产品A2类的使用方法
	ProductB* productB2 = factory2->createProductB(); // 使用具体工厂2类的对象创建具体产品B2类的对象
	productB2->eat(); // 调用具体产品B2类的吃方法
	delete productA2; // 释放具体产品A2类的对象
	delete productB2; // 释放具体产品B2类的对象
	return 0;
}

在上面的示例中,我们定义了两个抽象产品类ProductAProductB,以及四个具体产品类ConcreteProductA1ConcreteProductA2ConcreteProductB1ConcreteProductB2

ProductA类中定义了一个使用方法,ProductB类中定义了一个吃方法,ConcreteProduct类中实现了使用方法和吃方法。

然后,我们创建了一个抽象工厂类Factory和两个具体工厂类ConcreteFactory1ConcreteFactory2Factory类中定义了一个创建产品A方法和一个创建产品B方法,ConcreteFactory类中实现了创建产品A方法和创建产品B方法,返回具体产品类的对象。

在main函数中,我们创建了一个具体工厂1类的对象和一个具体产品A1类的对象,并调用具体产品A1类的使用方法,创建了一个具体产品B1类的对象,并调用具体产品B1类的吃方法。

然后,我们创建了一个具体工厂2类的对象和一个具体产品A2类的对象,并调用具体产品A2类的使用方法,创建了一个具体产品B2类的对象,并调用具体产品B2类的吃方法。

如何解决:在一个产品族里面,定义多个产品。

关键代码:在一个工厂里聚合多个同类产品。

代理模式(proxy)

在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。

在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。为其他对象提供一种代理以控制对这个对象的访问。

具体而言就是 在新类中创建原有类的私有对象 从而封装对该类的访问 这样就可以对访问加以控制。

#include <iostream>

using namespace std;

class Subject { // 抽象主题类
public:
	virtual void request() = 0; // 请求方法
};

class RealSubject : public Subject { // 真实主题类
public:
	void request() override { // 实现请求方法
		cout << "请求成功" << endl;
	}
};

class Proxy : public Subject { // 代理类
private:
	RealSubject* realSubject; // 持有真实主题对象的指针
	string name;
public:
	Proxy(string sname):name(sname){ // 构造函数,创建真实主题对象的指针
		realSubject = new RealSubject();
	}
	void request() override { // 实现请求方法,调用真实主题对象的请求方法
		if (name == "VIP") {
			realSubject->request();
		}
		else cout << "无权访问" << endl;
	}
};

int main() {
	Proxy* proxy1 = new Proxy("cilet"); // 创建代理对象
	proxy1->request(); // 调用代理对象的请求方法,实际上调用了真实主题对象的请求方法
	Proxy* proxy2 = new Proxy("VIP"); // 创建代理对象
	proxy2->request(); // 调用代理对象的请求方法,实际上调用了真实主题对象的请求方法
	return 0;
}

在上面的示例中,我们定义了一个Subject抽象主题类和一个RealSubject真实主题类。Subject类中定义了一个请求方法,RealSubject类中实现了请求方法。

然后,我们创建了一个Proxy代理类,它持有RealSubject真实主题对象的指针,并实现了请求方法。Proxy代理类添加了对真实主题对象的访问控制,只有符合条件的对象才能调用真实主题对象的请求方法。

main函数中,我们创建了两个代理对象,并尝试调用了代理对象的请求方法,由于代理类进行了访问控制,实际上只有第二个对象调用了真实主题对象的请求方法。

如何解决:增加中间层。

关键代码:实现与被代理类组合。

应用实例

  1. Windows 里面的快捷方式。

优点

  1. 职责清晰。

  2. 高扩展性。

  3. 智能化。

缺点

  1. 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
  2. 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

注意事项:

  1. 和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。

  2. 和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。

享元模式

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。

享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。

常见写法

#include <iostream>
#include <map>

using namespace std;

class Flyweight { // 抽象享元类
public:
	virtual void operation() = 0; // 操作方法
};

class ConcreteFlyweight : public Flyweight { // 具体享元类
private:
	char c; // 内部状态
public:
	ConcreteFlyweight(char c) : c(c) {
		cout << "构造对象 "<<c<<endl;
	} // 构造函数,初始化内部状态
	void operation() override { // 实现操作方法,输出内部状态
		cout << "ConcreteFlyweight::operation " << c << endl;
	}
};

class FlyweightFactory { // 享元工厂类
private:
	map<char, Flyweight*> flyweights; // 持有享元对象的指针的map
public:
	Flyweight* getFlyweight(char c) { // 获取享元对象的方法
		if (flyweights.find(c) == flyweights.end()) { // 如果map中没有该对象,则创建一个新的对象并添加到map中
			flyweights[c] = new ConcreteFlyweight(c);
		}
		return flyweights[c]; // 返回map中的对象
	}
};

int main() {
	FlyweightFactory* factory = new FlyweightFactory(); // 创建享元工厂对象
	Flyweight* flyweight1 = factory->getFlyweight('a'); // 获取享元对象a
	flyweight1->operation(); // 调用享元对象a的操作方法
	Flyweight* flyweight2 = factory->getFlyweight('b'); // 获取享元对象b
	flyweight2->operation(); // 调用享元对象b的操作方法
	Flyweight* flyweight3 = factory->getFlyweight('a'); // 再次获取享元对象a
	flyweight3->operation(); // 调用享元对象a的操作方法
	delete factory; // 释放享元工厂对象
	return 0;
}

在上面的示例中,我们定义了一个Flyweight抽象享元类和一个ConcreteFlyweight具体享元类。

Flyweight类中定义了一个操作方法,ConcreteFlyweight类中实现了操作方法,并添加了一个内部状态c

然后,我们创建了一个FlyweightFactory享元工厂类,它持有享元对象的指针的map,并实现了获取享元对象的方法。

main函数中,我们创建了一个享元工厂对象,并使用它来获取享元对象ab,并调用它们的操作方法。我们还再次获取了享元对象a,并调用它的操作方法。由于享元对象a已经存在于map中,因此不需要创建新的对象,而是直接返回map中的对象。

运行结果

构造对象 a
ConcreteFlyweight::operation a
构造对象 b
ConcreteFlyweight::operation b
ConcreteFlyweight::operation a

如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。

关键代码:用 HashMap 存储这些对象。

应用实例

  1. JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。
  2. 数据库的数据池。

优点:大大减少对象的创建,降低系统的内存,使效率提高。

缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

相关面试题

1. 对象复用的了解,零拷贝的了解

对象复用

对象复用其本质是一种设计模式:Flyweight享元模式。

通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。

零拷贝

零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

零拷贝技术可以减少数据拷贝和共享总线操作的次数。

在C++中,vector的一个成员函数emplace_back()很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高

2. 介绍面向对象的三大特性,并且举例说明

三大特性:继承、封装和多态

(1)继承

让某种类型对象获得另一个类型对象的属性和方法。

它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

常见的继承有三种方式:

  • 实现继承:指使用基类的属性和方法而无需额外编码的能力

  • 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力

  • 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力(C++里好像不怎么用)

例如,将人定义为一个抽象类,拥有姓名、性别、年龄等公共属性,吃饭、睡觉、走路等公共方法,在定义一个具体的人时,就可以继承这个抽象类,既保留了公共属性和方法,也可以在此基础上扩展跳舞、唱歌等特有方法

(2)封装

数据和代码捆绑在一起,避免外界干扰和不确定性访问。

封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。

(3)多态

同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)。

多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单一句话:允许将子类类型的指针赋值给父类类型的指针

实现多态有两种方式:覆盖(override)重载(overload)

  • 覆盖:是指子类重新定义父类的虚函数的做法。

  • 重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。例如:基类是一个抽象对象——人,那教师、运动员也是人,而使用这个抽象对象既可以表示教师、也可以表示运动员。