面向对象很好地解决了“抽象”的问题,但是必不可免地要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
典型模式:Singleton、Flyweight
单例模式(Singleton)
定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
——《设计模式》GoF
动机
- 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。
- 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实
例? - 这应该是类设计者的责任,而不是使用者的责任。
结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TRcdHYLo-1663861791314)(http://qny.cwnest.top/designPattern/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F.png)]
代码
class Singleton
{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
~Singleton();
};
Singleton* Singleton::m_instance = nullptr;
//线程非安全版本
Singleton * Singleton::getInstance(){
if(m_instance == nullptr){
m_instance = new Singleton();
}
return m_instance;
};
// 线程安全版本,但锁的代价过高
Singleton * Singleton::getInstance(){
Lock lock; //直接加锁
if(m_instance == nullptr){
m_instance = new Singleton();
}
return m_instance;
};
// 双检查锁, 但由于内存读写reorder不安全
Singleton * Singleton::getInstance(){
if(m_instance == nullptr){
Lock lock; //判断为空时加锁,再次判断
if(m_instance == nullptr){
// 我们假定的顺序是先分配内存再调用构造器最后再赋值给m_instance
// 但是底层reorder之后可能出现先分配内存,然后指向m_instance,再调用构造器
// 如果是这种情况的话,在还没有调用构造器的时候,ThreadB拿到执行权,
// 进行判断m_instance不是nullptr直接返回,
// 但此时m_instance只是原始内存,还没有调用构造器,分配的对象无法使用。
m_instance = new Singleton();
}
}
return m_instance;
};
// c++ 11版本之后的实现,java语言加volatile关键字
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton * Singleton::getInstance(){
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire); // 获取内存fence
if (tmp =nullptr){
std:lock_guard<std:mutex> lock(m_mutex);
tmp m_instance.load(std:memory_order_relaxed);
if (tmp == nullptr){
tmp = new Singleton;
std::atomic_thread._fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std:memory_order_relaxed);
}
}
return tmp;
};
要点
- Singleton模式中的实例构造器可以设置为protected以允许子类派生。
- Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。
- 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。
总结
单例模式目前来看就是不向用户开放构造函数,而是通过提供一个获取实例的方法来保证所有使用者共享一个实例对象。目前老师实现的是懒汉模式,需要解决在多线程情况下产生的并发问题,但是貌似使用饿汉式(即在构造函数中直接创建对象)没有这个问题(不知道对不对)。
享元模式(Flyweight)
定义
运用共享技术有效地支持大量细粒度的对象。
——《设计模式》GoF
动机
- 在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价一主要指内存需求方面的代价。
- 如何在避免大量细粒度对象问题的同时,让外部客户程序仍然能够透明地使用面向对象的方式来进行操作?
结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ptfbxG4-1663861791315)(http://qny.cwnest.top/designPattern/%E4%BA%AB%E5%85%83%E6%A8%A1%E5%BC%8F.png)]
代码
//字体对象
class Font {
private:
//unique object key
string key;
//...
public:
Font(const string & key){
//...
}
};
class FontFactory {
private:
map<string, Font* > fontPool;
public:
Font* getFont(const string& key){
//查找key
map<string,Font*>::iterator item = fontPool.find(key);
if (item != footPool.end()){
return fontPool[key];
}
else{
Font* font = new Font(key);
fontPool[key] = font;
return font;
}
}
void clear(){
//...
}
};
要点
- 面向对象很好地解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。
- Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。
- 对象的数量太大从而导致对象内存开销加大一什么样的数量才算大?这需要我们仔细的根据具体应用情况进行评估,而不能凭空臆断。
总结
享元模式通过对象池来从存储共享对象来避免创建大量相同对象,通常对象池中存储的对象是只读的,例如java中的String等。