C++设计模式介绍、分类与设计模式原则
一、设计模式定义二、设计模式的优点三、设计模式缺点四、设计模式中的抽象思维五、抽象的方法六、设计模式应用场景七、设计模式分类八、设计模式八大原则附加知识(1)C++面向对象三种访问修饰符(2)父类析构函数必须为虚函数(3)override关键字使用(4)final关键字使用(5)父类对象可以与子类对象相互转化吗(6)虚函数、虚函数表介绍
目录
一、设计模式定义
是一套被反复使用的代码设计经验的总结,是经过提炼的出色设计方法。设计模式主要是指面向对象这种编程模型下的设计模式;把变化与稳定的分割,管理变化,提高复用(稳定的部分可以封装为基类或者方法接口,使用子类应对变化)。设计模式可以更好地适应需求地变化,将变化来的代码修改影响减为最小。
二、设计模式的优点
设计模式一般应用于大型项目中,设计模式可以使各模块之间的代码灵活性和可复用性增强。
灵活性是指:可扩展性和低耦合型;增加新的功能,不需要大范围修改代码。
可复用性是指:可以到处重复使用,面向对象的三大特性:封装、继承、多态。泛型编程。面向对象程序设计原则之一:单一原则(一个类只干好一件事,不涉及其他事物)
三、设计模式缺点
代码的复杂度增加,增加了学习和阅读的负担,设计模式在一定程序会降低代码运行效率(对于带来的优点其下降运行效率一般可忽略)。
应用设计模式不当导致的代码灵活性、可复用性、可读性下降。
四、设计模式中的抽象思维
抽象思维强调对象的本质属性,主要用于一些软件设计中的解耦合的过程。
抽象思维的概念:能从事物中抽取出或者提炼出一些本质的,共性的内容,把这些共性的内容组合到一起封装成一个类或者方法。继承抽象类的子类都有不同的特点进行扩展。
五、抽象的方法
1、分解法:把一个复杂的事物分解成若干个单一功能的事物。
2、抽象法:从每个简单的事物中,抽象出本质的内容,封装起来。抽象法是设计模式的本质。
六、设计模式应用场景
通常应用于大型项目(几万到几十万行代码及以上项目),不建议应用于小型项目(小型项目要是适合也可使用设计模式)。对于大量重复性代码,需要使用设计模式进行设计,提高代码扩展性。
七、设计模式分类
常用的设计模式可以分为三大类:行为型模式、创建型模式、结构型模式
(1)创建型模式有6种:简单工厂模式(Simple Factory)、工厂方法模式(Factory Method)、抽象工厂模式(Abstract Factory)、单例模式(Singleton)、原型模式(Prototype)、建造者模式(Builder)。
(2)结构性模式有7种:装饰模式(Decorator)、外观模式(Facade)、组合模式(Composite)、享元模式(Flyweight)、代理模式(Proxy)、适配器模式(Adapter)、桥接模式(Bridge)
(3)行为型模式包括的设计模式有11种:模板方法模式(Template Method)、策略模式(Strategy)、观察者模式(Observer)、命令模式(Command)、迭代器模式(Iterator)、状态模式(State)、
中介模式(Mediator)、备忘录模式(Memento)、职责链模式(Chain Of Responsibility)、解释器模式(Interpreter)、访问者模式(Visitor)。
创建型模式定义:关注如何创建对象,将对象的创建和使用相互分离(解耦),取代传统对象创建方式带来的扩展性差的问题。
结构型模式:关注对象之间的关系。涉及如何组合各种对象以便获得更加灵活的结构,通过继承以及更多的关系组合获得更加灵活的程序结构。达到简化设计模式。
行为模式定义:关注对象的行为或者交互方面的内容,主要涉及算法和对象之间的职责分配。通过使用对象组合,行为模式可以描述一组对象如何协作来完成一个整体任务。
注意:设计模式代码一般不是一次设计设计好的,是多次修改而成。软件开发需求变化是频繁的,尝试寻找变化点,把变化部分和稳定部分分离开发,在变化的地方使用设计模式。
八、设计模式八大原则
任何设计模式的代码需要符合设计模式的原则,设计模式的原则如下:
1、依赖倒置原则(DIP)
(1)高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定)。
(2)抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)。
(1)中的高层模块指的是调用低层变化的Caller类对象或者方法,变化的低层模块指的是待调用Concre1、Concret2、、、对象或方法。抽象指的是对具有共同特征一类事物的抽象类。
(2)中的抽象跟(1)一致。实现细节指的是继承于抽象的具体类的子类,并对基类中的虚函数进行重写override。抽象类中不能依赖子类中的变化。
倒置原则较为重要,写个demo,如下:
#include <iostream>
#include <vector>
class AbsProduct
{
public:
virtual void color(){};
virtual ~AbsProduct(){};
};
class Caller
{
public:
std::vector<AbsProduct*>vecProducts; //高层模块依赖于抽象,使用指针,才可以使用多态应对变化。
void PrintProductColor(){
for(unsigned int i = 0;i < vecProducts.size(); i++)
{
vecProducts.at(i)->color();
}
};
};
class Product1: public AbsProduct
{
public:
virtual void color(){
std::cout << "Product1 color" << std::endl;
};
};
class Product2: public AbsProduct
{
public:
virtual void color(){
std::cout << "Product2 color" << std::endl;
};
};
int main()
{
Caller obj;
AbsProduct *pro1 = new Product1();
obj.vecProducts.push_back(pro1);
AbsProduct *pro2 = new Product2();
obj.vecProducts.push_back(pro2);
obj.PrintProductColor();
delete pro1;
pro1 = nullptr;
delete pro2;
pro2 = nullptr;
obj.vecProducts.clear();
return 0;
}
运行结果如下:
上述代码中的高层模块Caller是稳定的,低层模块是具体的产品Product1,产品Product2,产品Product3,,,等类型产品,特性是不同的。高层模块需要调用调用低层模块的接口, 而低层的多样不稳定,会使代码应对需求变化的改动较大。为使代码更具有应对变化的能力,将底层模块各种产品的共同属性抽象出一个类型,这样高层模块就会依赖抽象,低层模块也就依赖抽象,这样这个代码模块的可复用性就得到了加强。倒置是指高层的主程序调用依赖抽象接口,在抽象接口具体运行时的晚绑定的接口实现过程。
2、开放封闭原则(OCP)
(1)对扩展开放,对更改封闭。
(2)类模块应该是可扩展的,但是不可修改。
解释:(1)中的扩展指的是应对需求的变化可以创建一个继承父类的新类。对更改封闭指的是改变是在子类内部。
3、单一职责原则(SRP)
(1)一个类应该仅有一个引起它变化的原因。
(2)变化的方向隐含着该类的责任。
4、Liskov替换原则(LSP)
(1)子类必须能够替换它们的基类(IS-A)
(2)继承表达类型抽象。
注意:父类让子类继承的方法注意访问修饰符。
5、接口隔离原则(ISP)
(1)不应该强迫客户程序依赖它们不用的方法。(能用protected不用public)
(2)接口应该小而完备。
6、优先使用对象组合,而不是类继承
(1)类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
(2)继承在某种程度上破坏了封装性,子类父类耦合度高。
(3)而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
7、封装变化点
使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
8、针对接口编程,而不是针对实现编程
(1)不将变量类型声明为某个特定的具体类,而是声明为某个接口。(具体类不是绝对化,如vector,string,等)
(2)客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
(3)减少系统中各部分的依赖关系,从而实现“高内聚,松耦合”的类型设计方案。
九、设计模式使用场景
1、如果代码设计中存在稳定和变化的结构,则可以使用设计模式来管理变化,提高复用。
2、如果代码设计中都是稳定结构,不存在变化结构部分,则不使用设计模式。
3、如果代码设计中都是变化结构,不存稳定的结构部分,则不使用设计模式。
附加知识
设计模式主要利用类的多态、继承、封装方法对事物进行抽象设计。并对共用的属性和方法抽象成基类,对变化的属性使用虚函数进行多态设计。对面向对象的相关知识点可以进行回顾
(1)C++面向对象三种访问修饰符
public: 允许该类函数、子类函数、友元函数、该类对象可以访问。
protected:只允许该类函数、子类函数、友元函数可以访问。
private:只允许本类的成员函数可以访问。
具体可参考:
C++中public、protected、private的区别_风雨也无晴的博客-CSDN博客
友元(友元函数、友元类和友元成员函数) C++_夜雨听萧瑟的博客-CSDN博客
(2)父类析构函数必须为虚函数
定义父类对象初始化时,让父类对象实际指向子类。同时父类析构函数必须为虚函数,这样在父类对象析构时,不会调用子类的虚构函数,导致子类的对象不能释放,造成子类对象的内存泄露。具体可参考:为什么父类析构函数必须为虚函数_父类析构函数不是虚函数会怎么样_IM-STONE的博客-CSDN博客
(3)override关键字使用
在子类中重写父类的虚函数时,在其后面加上override关键字,如果父类不存在该虚函数,则编译不通过。
class A
{
public:
virtual void FunA()=0; //纯虚函数,子类必须实现该函数。
virtual int FunB(){}; //虚函数,子类可以重写,也可以不用重写
}
class B:public A
{
public:
void FunA()override{};
int FunB()override{};
}
C++:重载,重定义,重写的区别_重定义和重写的区别__来信的博客-CSDN博客
(4)final关键字使用
如果当前类不能有派生类,则可以在类后面添加关键字final;如果不想该虚函数不被重写,在该虚函数后面添加final。如果继承final类或者重载final修饰的函数,会导致编译报错。
class A final //用法1:该类不能被继承。
{}
class B
{
public:
virtual void fun()final{}; //用法2:该虚函数不能被重写。
}
C++ final关键字_mayue_csdn的博客-CSDN博客
(5)父类对象可以与子类对象相互转化吗?
父类对象与子类对象可以相互转换,前提是父类对象一定是用子类对象初始化的。子类对象是对父类对象的扩展,子类属性一般是大于父类属性。
具体分析可参考:父类对象和子类对象之间可以相互转换吗_父对象转成子对象_扶公瑾以苏的博客-CSDN博客
(6)虚函数、虚函数表介绍
可参考下面链接
(超重要)构造函数为什么不能为虚函数?析构函数为什么要虚函数?_构造函数能不能为虚函数_HeisenbergWDG的博客-CSDN博客
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)