常见设计模式--通俗易懂版

一、设计模式原则

1.单一职责原则

核心:一个类只负责一个功能领域中相应的职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 

思想:如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。

单一职责原则注意事项和细节

1) 降低类的复杂度,一个类只负责一项职责。

2) 提高类的可读性,可维护性

3) 降低变更引起的风险

4) 通常情况下, 我们应当遵守单一职责原则 ,只有逻辑足够简单,才可以在代码级违

2.开闭原则

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

子类重写父类的虚函数

3.依赖倒置原则

  • 抽象不应该依赖于细节,细节应当依赖于抽象
  • 高层模块不应该依赖低层模块,两个都应该依赖于抽象
  • 依赖倒转 ( 倒置 ) 的中心思想是面向接口编程
  • 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去实现

依赖倒转原则的注意事项和细节

1) 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好 .

2) 变量的声明类型尽量是抽象类或接口 , 这样我们的变量引用和实际对象间,就存在

一个缓冲层,利于程序扩展和优化

  1. 继承时遵循里氏替换原则

4.里氏替换原则

子类对象能够随时随地替换父类对象,并且替换完之后,语法不会报错,业务逻辑也不会出现问题,即继承父类而不去改变父类

二、创建型模式

1.单例模式

单例模式是指在内存中只会创建且仅创建一次对象的设计模式在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

1.1饿汉模式

#include <iostream>
using namespace std;
//定义一个单例模式的任务队列
class TaskQueue{
public:
    TaskQueue(const TaskQueue& t) = delete;
    TaskQueue& operator =(const TaskQueue& t)=delete;
    static TaskQueue* gettaskQ()
	{
    return m_taskQ;
    }
    void print()
    {
    cout<<"我是单例模式的一个成员函数"<<endl;
    }
private:
    TaskQueue()=default;
    //TaskQueue(const TaskQueue &t) = default;
    //TaaskQueue& operator =(const TaskQueue &t)=default;
    //只能通过类名访问静态属性或方法
    static TaskQueue* m_taskQ;
};
TaskQueue* TaskQueue::m_taskQ=new TaskQueue;
int main(){
    TaskQueue* taskQ=TaskQueue::gettaskQ();
    taskQ->print();
    return 0;
}

1.2懒汉模式

  1. 使用双检锁解决线程安全问题
#include <iostream>
#include<mutex>
using namespace std;
//定义一个单例模式的任务队列
class TaskQueue{
public:
    TaskQueue(const TaskQueue& t) = delete;
    TaskQueue& operator =(const TaskQueue& t)=delete;
    static TaskQueue* gettaskQ()
	{
		if(m_taskQ==nullptr){
			m_mutex.lock();
		if(m_taskQ==nullptr)
		{
			m_taskQ=new TaskQueue;
		}
			m_mutex.unlock();
    }
     return m_taskQ;
}
    void print()
    {
    cout<<"我是单例模式的一个成员函数"<<endl;
    }
private:
    TaskQueue()=default;
    //TaskQueue(const TaskQueue &t) = default;
    //TaaskQueue& operator =(const TaskQueue &t)=default;
    //只能通过类名访问静态属性或方法
    static TaskQueue* m_taskQ;
	 static mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ=nullptr;
mutex TaskQueue::m_mutex;
int main(){
    TaskQueue* taskQ=TaskQueue::gettaskQ();
    taskQ->print();
    return 0;
}

但是实际上m_taskQ = new TaskQueue;在执行过程中对应的机器指令可能会被重新排序。正常过程如下:

  • 第一步:分配内存用于保存TaskQueue对象。
  • 第二步:在分配的内存中构造一个TaskQueue 对象(初始化内存)。
  • 第三步:使用m_taskQ指针指向分配的内存。

但是被重新排序以后执行顺序可能会变成这样:

  • 第一步:分配内存用于保存TaskQueue 对象。

第二步:使用m_taskQ指针指向分配的内存。

第三步:在分配的内存中构造一个TaskQueue对象(初始化内存)

这样重排序并不影响单线程的执行结果,但是在多线程中就会出问题。如果线程A按照第二种顺序执行机器指令,执行完前两步之后失去CPU时间片被挂起了,此时线程B在第3行处进行指针判断的时候m_taskQ指针是不为空的,但这个指针指向的内存却没有被初始化,最后线程B使用了一个没有被初始化的队列对象就出问题了(出现这种情况是概率问题,需要反复的大量测试问题才可能会出现)。

在C++11中引入了原子变量 atmic,通过原子变量可以实现一种更安全的懒汉模式的单例,代码如下:

2.原子变量解决双检锁定问题

#include <iostream>
#include<mutex>
#include<atomic>
using namespace std;
//定义一个单例模式的任务队列
class TaskQueue{
public:
    TaskQueue(const TaskQueue& t) = delete;
    TaskQueue& operator =(const TaskQueue& t)=delete;
    static TaskQueue* gettaskQ()
	{
		TaskQueue* task = m_taskQ.load();
		if(task==nullptr){
			m_mutex.lock();
			task=m_taskQ.load();
		if(task==nullptr)
		{
			task=new TaskQueue;
			m_taskQ.store(task);
		}
			m_mutex.unlock();
    }
		return task;
	}
    void print()
    {
    cout<<"我是单例模式的一个成员函数"<<endl;
    }
private:
    TaskQueue()=default;
    //TaskQueue(const TaskQueue &t) = default;
    //TaaskQueue& operator =(const TaskQueue &t)=default;
    //只能通过类名访问静态属性或方法
    //static TaskQueue* m_taskQ;
	static mutex m_mutex;
	static atomic<TaskQueue*> m_taskQ; 
};
//TaskQueue* TaskQueue::m_taskQ=nullptr;
mutex TaskQueue::m_mutex;
atomic <TaskQueue*> TaskQueue::m_taskQ;
int main(){
    TaskQueue* taskQ=TaskQueue::gettaskQ();
    taskQ->print();
    return 0;
}

上面代码中使用原子变量atomic的 store()方法来存储单例对象,使用load()方法.来加载单例对象。在原子变量中这两个函数在处理指令的时候默认的原子顺序是memory_order_seq_cst(顺序原子操作 –sequentiallyconsistent),使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(dataraces),不足之处就是使用这种方法实现的懒汉模式的单例执行效率更低一些。

3.使用局部静态对象解决线程安全问题---》编译器必须支持C++11

#include <iostream>

using namespace std;
//定义一个单例模式的任务队列
class TaskQueue{
public:
    TaskQueue(const TaskQueue& t) = delete;
    TaskQueue& operator =(const TaskQueue& t)=delete;
    static TaskQueue* gettaskQ()
	{
		static TaskQueue task;
		return &task; //因为返回的是指针,创建的是对象,所以要取地址
	}
    void print()
    {
    cout<<"我是单例模式的一个成员函数"<<endl;
    }
private:
    TaskQueue()=default;
    //TaskQueue(const TaskQueue &t) = default;
    //TaaskQueue& operator =(const TaskQueue &t)=default;
 
};
int main(){
    TaskQueue* taskQ=TaskQueue::gettaskQ();
    taskQ->print();
    return 0;
}

可行的原因:是因为在C++11标准中有如下规定,并且这个操作是在编译时由编译器保证的:

如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。

1.3懒汉模式和饿汉模式的区别

懒汉模式的缺点在创建实例对象的时候有安全问题,但这样可以减少内存的浪费〈如果用不到就不去申请内存了)。饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。对于现在的计算机而言,内存容量都是足够大的,这个缺陷可以被无视。

2.简单工厂模式

简单工厂模式相关类的创建和使用步骤如下:

1.创建一个新的类,可以将这个类称之为工厂类。对于简单工厂模式来说,需要的工厂类只有一个。

2.在这个工厂类中添加一个公共的成员函数,通过这个函数来创建我们需要的对象,关于这个函数一般将其称之为工厂函数。

3.关于使用,首先创建一个工厂类对象,然后通过这个对象调用工厂函数,这样就可以生产出一个指定类型的实例对象了。

优点:

本着高内聚低耦合的原则,将系统的逻辑部分和功能分开。

缺点:

简单工厂模式会增加系统类的个数,在一定程度上增加了系统的复杂度和理解难度;

系统扩展难,一旦增加新产品,就需要修改工厂逻辑,不利于系统的扩展与维护;简单工厂模式中所有产品的创建都是由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间耦合度高,严重影响了系统的灵活性和扩展性。

#include <iostream>
using namespace std;

// 抽象产品类
class Product {
public:
    virtual void operation() = 0;
};

// 具体产品类A
class ProductA : public Product {
public:
    void operation() {
        cout << "ProductA operation" << endl;
    }
};

// 具体产品类B
class ProductB : public Product {
public:
    void operation() {
        cout << "ProductB operation" << endl;
    }
};

// 工厂类
class Factory {
public:
    Product* createProduct(char type) {
        switch(type) {
            case 'A':
                return new ProductA();
            case 'B':
                return new ProductB();
            default:
                return nullptr;
        }
    }
};

int main() {
    Factory factory;
    Product* productA = factory.createProduct('A');
    productA->operation(); // 输出: ProductA operation

    Product* productB = factory.createProduct('B');
    productB->operation(); // 输出: ProductB operation

    delete productA;
    delete productB;

    return 0;
}

3.工厂模式

工厂方法模式定义:在工厂模式中,工厂父类负责定义创建产品对象的公告接口,而工厂子类负责生成具体的产品对象目的是将产品的实例化操作延迟到工厂子类中完成,通过工厂子类来确定究竟应该实例化哪一个具体产品类。

优点:系统的扩展性好,符合“开闭原则”  。系统加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品即可。

缺点:在添加新产品时,需要编写新的具体产品类,而且要提供与之对应的具体工厂类,系统中类的个数将成对增加,一定程度上增加了系统的复杂度

#include <iostream>

// 抽象产品类
class Product {
public:
    virtual ~Product() {}
    virtual void operation() = 0;
};

// 具体产品类A
class ConcreteProductA : public Product {
public:
    void operation() override {
        std::cout << "I'm A" << std::endl;
    }
};

// 具体产品类B
class ConcreteProductB : public Product {
public:
    void operation() override {
        std::cout << "I'm B" << std::endl;
    }
};

// 抽象工厂类
class Factory {
public:
    virtual ~Factory() {}
    virtual Product* createProduct() = 0;
};

// 具体工厂类A
class ConcreteFactoryA : public Factory {
public:
    Product* createProduct() override {
        return new ConcreteProductA();
    }
};

// 具体工厂类B
class ConcreteFactoryB : public Factory {
public:
    Product* createProduct() override {
        return new ConcreteProductB();
    }
};

int main() {
    // 使用具体工厂A创建产品A
    Factory* factoryA = new ConcreteFactoryA();
    Product* productA = factoryA->createProduct();
    productA->operation();

    // 使用具体工厂B创建产品B
    Factory* factoryB = new ConcreteFactoryB();
    Product* productB = factoryB->createProduct();
    productB->operation();

    delete factoryA;
    delete factoryB;
    delete productA;
    delete productB;

    return 0;
}

三、设计模式的作用

1. 代码重用性 (即:相同功能的代码,不用多次编写)

2. 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)

3. 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)

4. 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)

5. 使程序呈现高内聚,低耦合的特性

 

 

 

  • 34
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值