设计模式原则
1.依赖倒置原则(DIP)
高层模块(稳定)不应该依赖于底层模块,二者都应该依赖于抽象
抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象
2.开发封闭原则(OCP)
对扩展开发,对更改封闭
类模块应该是可扩展的,但是不可修改
3.单一职责原则(SRP)
一个类应该仅有一个引起它变化的原因
变化的方向隐含类的责任
4.Liskov替换原则(LSP)
子类必须能够替换他们的基类(IS-A)
继承表达类型抽象
5.接口隔离原则(ISP)
不应该强迫程序依赖他们不用的方法
接口该小而完备
优先使用对象组合,而不是类继承
类继承通过为”白箱复用”,对象组合通常为”黑箱复用”
继承在某种程度上破坏了封装性,子类父类耦合度高。
对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
封闭变化点
使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
针对接口编程,而不是针对实现编程
不将变量类型声明为某个特定的具体类,而是声明为某个接口。
客户程序无需获知对象的具体类型,只需要知道对象所具有的接口
减少系统中各部分的依赖关系,从而实现“高内聚,低耦合”的类型设计方案
将设计模式提升为设计经验
设计习语
描述与特点编程语言相关的底层模式,技巧及相关用法
设计模式
类与相互通信的对象之间的组织关系,包括它们的角色、职责、协作方式等方面(主要解决变化中的复用性问题)
架构模式
描述系统中与基本结构组织关系密切的高层模式,包括子系统划分,职责,以及如何组织它们之间关系的规则
重构关键技法
静态->动态
早绑定->晚绑定
继承->组合
编译时依赖->运行时依赖
紧耦合->松耦合
GoF-23模式分类
从目的看:
创建型:用来创建对象。单例、原型、抽象工厂、建造者、工厂方法这五个都属于这一分类。这种类别起到了将对象的创建与其使用进行分离解耦。
结构型:用来处理类或对象之间如何组合以构成更大的结构体。桥接、装饰、适配器、代理、外观、享元、组合这七个属于这一类。
行为型:用来处理类或对象之间如何交互、协同工作和分配职责。模板方法、解释器、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录这11个属于这一类。
从范围看:
类模式:主要用来处理类与其子类之间继承关系,这种关系是编译时确定的,运行时是静态和不变的。工厂方法、模板方法、(类)适配器、解释器这四个属于这一种。
对象模式:用于处理对象之间的组合或者聚合关系,运行时可以变化,是动态的。其他的19种都属于这一模式。
从封装变化角度对模式分类
组件协作:通过晚期绑定,来实现框架与应用程序之间额松耦合,是二者之间协作时常用的模式
Templete Method (模板方法)
Strategy(策略模式)
Observe / Event(观察者模式)
单一职责:类与类之间责任划分的问题
Decorator
Bridge
对象创建:
Factory Method
Abstract Factory
Prototype
Builder
对象性能:
Singleton
Flyweight
接口隔离:
Facade
Proxy
Mediator
Adapter
状态变化:
Mement
State
数据结构:
Composite
Iterator
Chain of Resposibility
行为变化:
Commad
Visitor
领域问题
Interpreter
组件协作
Templete Method (模板方法)
动机
在软件构建中,对于某项任务,它常常有稳定的整体结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务整体的结构同时实现
如何在确定稳定操作结构的前提下,来灵活应对各个子步骤之间的变化或者晚期实现需求
例子
以下为常规列程
//程序库开发人员
class Library
{
public:
void step1(){
//...
}
void step3(){
//...
}
void step4(){
//...
}
};
//应用程序开发任务
class Application
{
public:
bool step2(){
//...
}
bool step5(){
//...
}
};
int main()
{
Library lib();
Application app();
lib.step1();
if(app.step2())
{
lib.step3();
}
for(int i = 0 ;i < 4 ; i++)
{
app.step4();
}
lib.step5();
}
以下为使用模板方法设计模式后改良
class Library
{
public:
virtual ~Library(){};
//稳定的步骤!!!
void run()
{
step1();
if(step2())
{
step3();
}
for(int i = 0 ;i < 4 ; i++)
{
step4();
}
step5();
}
protected:
void step1(){
//...
}
void step3(){
//...
}
void step4(){
//...
}
virtual bool step2() = 0; //变化
virtual bool step4() = 0; //变化
};
class Application:public Library
{
protected:
virtual bool step2(){
//...子类重写实现
}
virtual bool step5(){
//...子类重写实现
}
};
int main()
{
Library* pLib = new Application();
pLib->run();
delete pLib();
}
该设计模式最关键的是run方法,在实际生产业务中相对稳定。
类图
策略模式
动机
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担
如果在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
例程
汇率计算
enum TxaBase{
CN_TAX;
US_TAX;
DE_TAX;
};
class SalesOrder
{
TxaBase tax;
public:
double CalcTax()
{
if(tax == CN_TAX)
{
//...
}
if(tax == US_TAX)
{
//...
}
if(tax == DE_TAX)
{
//...
}
//...
}
};
从以上来看,当突然需要新增一个日本汇率时,需要再在代码中新增if(…),违背了开发封闭原则(OCP)
class TaxStrategy
{
public:
double CalcTax()
virtual double Calc(const Context& context) = 0;
virtual ~TaxStrategy(){}
};
class CNTax:public TaxStrategy
{
virtual double Calc(const Context& context)
{
// ....
}
};
class USTax:public TaxStrategy
{
virtual double Calc(const Context& context)
{
// ....
}
};
class DETax:public TaxStrategy
{
virtual double Calc(const Context& context)
{
// ....
}
};
class SalesOrder
{
private:
TaxStrategy* strategy;
public:
SalesOrder(StrategyFactory* strategyFactory){
this->strategy = strategyFactory->NewStrategy(); //使用工厂模式返回一个具体实例
}
~SalesOrder();
double CalcTax()
{
Context context();
double val = strategy->Calc();
}
};
以上程序就是通过 策略模式 后改良的程序。当我们在实际开发过程中,如果遇到需要很多if、else(switch 、case)的情况,除非情况是固定不会变化的,都可采用策略模式!!
类图
观察者模式
动机
在软件构建过程中,我们需要为某些对象简历一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
#include <iostream>
#include <list>
using std::cout;
using std::endl;
using std::cin;
class Observer
{
public:
virtual ~Observer() {};
virtual void Update(int) = 0;
};
class Subject
{
public:
virtual ~Subject() {};
virtual void Attach(Observer*) = 0;
virtual void Detach(Observer*) = 0;
virtual void Notify() = 0;
};
class ConcreteObserver : public Observer
{
private:
Subject *_pSubject;
public:
ConcreteObserver(Subject* pSubject) :_pSubject(pSubject)
{
//在目标中注册当前观察者(此处的观察者是广义上的观察者,目标并不知道具体谁要观察它,目标只进行广播即可)
this->_pSubject->Attach(this);
cout << "I'm the observer \" 1 \".\n";
}
void Update(int value) override
{
cout << "ConcreteObserver get the update.New State:" << value << endl;
}
};
class ConcreteObserver2 : public Observer
{
private:
Subject *_pSubject;
public:
ConcreteObserver2(Subject* pSubject) :_pSubject(pSubject)
{
//在目标中注册当前观察者(此处的观察者是广义上的观察者,目标并不知道具体谁要观察它,目标只进行广播即可)
this->_pSubject->Attach(this);
cout << "I'm the observer \" 2 \".\n";
}
void Update(int value) override
{
cout << "ConcreteObserver2 get the update.New State:" << value << endl;
}
};
class ConcreteSubject :public Subject
{
private:
std::list<Observer*> _pObserverList; //!!多个观察者
int _iState;
public:
void SetState(int state)
{
_iState = state;
}
void Attach(Observer* pObserver) override
{
_pObserverList.push_back(pObserver);
}
void Detach(Observer* pObserver) override
{
_pObserverList.remove(pObserver);
}
void Notify() override
{
auto begin = _pObserverList.begin();
auto end = _pObserverList.end();
while (begin != end)
{
(*begin)->Update(_iState);
begin++;
}
}
};
int main()
{
//创建目标
ConcreteSubject *pSubject = new ConcreteSubject();
//创建观察者
Observer *pObserver = new ConcreteObserver(pSubject);
Observer *pObserver2 = new ConcreteObserver2(pSubject);
//改变当前状态
pSubject->SetState(2);
//广播给所有广义上的观察者
pSubject->Notify();
//去除某个观察者
pSubject->Detach(pObserver);
//改变当前状态
pSubject->SetState(3);
//重新广播
pSubject->Notify();
//结束,释放对象
delete pObserver;
delete pObserver2;
delete pSubject;
return 0;
}
类图
“单一职责”模式:
装饰模式
动机
在某些情况下我们可能会“过度地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类鹏展问题?从而使得任何“功能扩展变化”所导致的影响降为最低?
例程
class Stream
{
public:
virtual void read(int number) = 0;
virtual void seek(int pos) = 0;
virtual void write(char* data) = 0;
virtual ~Stream() {};
};
class FileStream:public Stream
{
public:
virtual void read(int number){
//读文件流
}
virtual void seek(int pos){
//定位文件流
}
virtual void write(char* data) {
//写文件流
}
virtual ~FileStream() {};
};
//网络流
class NetworkStream :public Stream
{
public:
virtual void read(int number) {
//读文件流
}
virtual void seek(int pos) {
//定位文件流
}
virtual void write(char* data) {
//写文件流
}
virtual ~NetworkStream() {};
};
//内存流
class MemStream :public Stream
{
public:
virtual void read(int number) {
//读文件流
}
virtual void seek(int pos) {
//定位文件流
}
virtual void write(char* data) {
//写文件流
}
virtual ~MemStream() {};
};
class CryptoFileStream :public FileStream {
public:
virtual void read(int number) {
//加密操作
FileStream::read(number);
}
virtual void seek(int pos) {
//加密操作
FileStream::seek(pos);
}
virtual void write(char* data) {
//加密操作
FileStream::write(data);
}
virtual ~CryptoFileStream() {};
};
class CryptoNetworkStream :public NetworkStream {
public:
virtual void read(int number) {
//加密操作
NetworkStream::read(number);
}
virtual void seek(int pos) {
//加密操作
NetworkStream::seek(pos);
}
virtual void write(char* data) {
//加密操作
NetworkStream::write(data);
}
virtual ~CryptoNetworkStream() {};
};
class CryptoMemStream :public MemStream {
public:
virtual void read(int number) {
//加密操作
MemStream::read(number);
}
virtual void seek(int pos) {
//加密操作
MemStream::seek(pos);
}
virtual void write(char* data) {
//加密操作
MemStream::write(data);
}
virtual ~CryptoMemStream() {};
};
以上程序,当我们需要在三个流上加上相同的操作(加密时),需要分别继承出来,然后再进行挨个添加,分析代码不难看出,冗杂代码很多,相同的代码反复在写,且若想在加个操作,或者想再加个操作,或是换个操作,代码修改起来会十分困难,且代码量巨大。
class Stream
{
public:
virtual void read(int number) = 0;
virtual void seek(int pos) = 0;
virtual void write(char* data) = 0;
virtual ~Stream() {};
};
class FileStream:public Stream
{
public:
virtual void read(int number){
//读文件流
}
virtual void seek(int pos){
//定位文件流
}
virtual void write(char* data) {
//写文件流
}
virtual ~FileStream() {};
};
//网络流
class NetworkStream :public Stream
{
public:
virtual void read(int number) {
//读文件流
}
virtual void seek(int pos) {
//定位文件流
}
virtual void write(char* data) {
//写文件流
}
virtual ~NetworkStream() {};
};
//内存流
class MemStream :public Stream
{
public:
virtual void read(int number) {
//读文件流
}
virtual void seek(int pos) {
//定位文件流
}
virtual void write(char* data) {
//写文件流
}
virtual ~MemStream() {};
};
class DecoratorStream :public Stream //装饰类
{
protected:
Stream* stream;
public:
DecoratorStream(Stream* stm) :Stream(stm){
};
};
class CryptoStream :public DecoratorStream {
public:
CryptoStream(Stream* stm) :DecoratorStream(stm) {};
virtual ~CryptoStream() {};
virtual void read(int number) {
stream->read(number);
}
virtual void seek(int pos) {
stream->seek(pos);
}
virtual void write(char* data) {
stream->write(data);
}
};
以上代码是使用装饰模式修改的Demo,当还需要其他操作时,只需继承装饰类(DecoratorStream),再在调用时按照需求传递Stream即可。 在实际开发中,当遇到主体类在多个方向上的扩展需要,往往考虑装饰模式,装饰模式的特点主要表现在 即是is-a 同样也是 has-a的关系。
类图
桥接模式
动机
由于某些类型的固有的实现逻辑,使得他们具有两个变化的维度,乃至多个维度的变化。
如何应对这种“多维度的变化”?如何利用面向对象技术来使得类型可以轻松沿着两个乃至多个方向变化,而不引入额外的复杂度?
将抽象部分与实现分离,使得他们可以独立变化
感觉用不着:用例——略 关键就是把一个类多个维度的变化,各个都抽出来,用多态的方式搞定。
“对象创建”模式
通过“对象创建”模式绕开new,来避免对象创建过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
工厂方法
动机
在软件系统中,经常面临创建对象的工作;由于需求的变化需要创建的对象的具体类型经常变化。
如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合?
例程
#include<cstdio>
class Operator // 抽象类-计算器类
{
public:
virtual void GetResult() = 0;
void SetNumA(double numA)
{
a = numA;
}
void SetNumB(double numB)
{
b = numB;
}
protected:
double a, b;
};
class OperatorAdd : public Operator //加法器类,继承计算器类
{
public:
void GetResult()
{
printf("a+b=%lf\n", a + b);
}
};
class OperatorSub : public Operator // 减法器类,继承计算器类
{
public:
void GetResult()
{
printf("a-b=%lf\n", a - b);
}
};
class IFactory // 工厂类
{
public:
virtual Operator* CreateOperator() = 0;
};
class AddFactory : public IFactory
{
Operator* CreateOperator()
{
return new OperatorAdd();
}
};
class SubFactory : public IFactory
{
Operator* CreateOperator()
{
return new OperatorSub();
}
};
int main()
{
IFactory* operFactory = new AddFactory();
Operator* oper = operFactory->CreateOperator();
oper->SetNumA(1.1);
oper->SetNumB(2.2);
oper->GetResult();
return 0;
}
工厂方法的关键在于,定义一个创建对象的接口,子类使用虚函数返回具体对象的实例,使得一个类的实例化延迟到子类。
类图
抽象工厂
动机
在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。
如何对应这种变化?如何如果常规对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作”的紧耦合?
就是在工厂方法的基础上,一个工厂提供接口返回多个相互依赖的对象。
原型模式
在创建一个较为复杂的对象时,采用原型模式。具体为函数clone返回一个自身的深拷贝对象。 用得较少。。略。。
构建器
用得也很少,也蛮好理解 略吧。
单例模式
太常用了,就不过多介绍作用
分析几种常规实现:
class Singleton
{
private:
static Singleton* instance;
private:
Singleton() {};
~Singleton() {};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton* getInstance() {
if(instance == NULL) {
instance = new Singleton();
}
return instance;
}
};
// init static member
Singleton* Singleton::instance = NULL;
以上为常用的懒汉式加载,单线程下可以使用,存在内存泄漏问题,可加入内部类实现资源释放,或使用智能指针;一下为内部类实现
class Singleton
{
private:
static Singleton* instance;
private:
Singleton() { };
~Singleton() { };
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
private:
class Deletor {
public:
~Deletor() {
if(Singleton::instance != NULL)
delete Singleton::instance;
}
};
static Deletor deletor; //!!!
public:
static Singleton* getInstance() {
if(instance == NULL) {
instance = new Singleton();
}
return instance;
}
};
// init static member
Singleton* Singleton::instance = NULL;
注:懒汉式非线程安全,分别看以下情况:
static Singleton* getInstance() {
Lock lock;
if(instance == NULL) {
instance = new Singleton();
}
return instance;
}
static Singleton* getInstance() {
if(instance == NULL) {
Lock lock; // 基于作用域的加锁,超出作用域,自动调用析构函数解锁
if(instance == NULL) {
instance = new Singleton();
}
}
return instance;
}
以上第一种方式,线程安全了,但线程多时会大大影响效率;
第二种方式,双检查锁的方式,看似天衣无缝;但由于内存读写reorder不安全。简单的说明就是 线程在指令集情况下,运行顺序可能会发生改变。常规分配内存操作:1.分配空间 2.调用构造函数 3.赋值到引用;但在指令集可能会发生1.分配空间 2.赋值到引用 3.调用构造器; 故当多线程情况下,可能会返回一个未调用构造器的错误对象!!
以下为C11的跨平台实现:
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);
if (tmp == nullptr)
{
std::lock_guard <std::mutex>(memory_order_relaxed);
if (tmp == nullptr)
{
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
以下为饿汉式
class Singleton
{
private:
static Singleton instance;
private:
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton& getInstance() {
return instance;
}
}
// initialize defaultly
Singleton Singleton::instance;
饿汉式的优点就是线程安全,但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。
Effective C++ 提出的最优雅的方式,面试的时候写这种!!
class Singleton
{
private:
Singleton() { };
~Singleton() { };
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton& getInstance() {
static Singleton instance; //返回局部静态变量
return instance;
}
};
享元模式
动机
在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价——主要指内存需求方面的代价。
如何在避免大量细粒度对象问题的同时,让外部客户程序仍能够透明地使用面向对象的方式进行操作?
享元模式的思路,主要就是共享对象,当然这种对象往往创建出来就是只读的。当对象在内存中不存在就创建并放入内存,当需要该对象时,直接取出来即可。
常见实现方法,就是放到一个map里。
接口隔离模式
门面模式
为子系统中的一组接口提供一个一致(稳定)的界面,Facade模式定义了一个高层接口,这个接口使得这一子系列更加容易使用(复用)。
1> 从客户程序的角度来看,门面模式简化了整个组件系统的接口,对于组件内部与外部客户程序来说,达到了一种解耦的效果,内部子系统的任何变化都不会影响到Facede接口的变化。
2> 门面设计模式更加的从架构的层次去看整个的系统,而不是单个的类的层次。
3> 门面模式并不是一个集装箱,可以任意的放进任何多个对象。内部应该时“相互耦合关系比较大的一系列组件”,而不是一个简单的功能的集合。
代理模式
代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。
动机
远程代理,也就是为一个对象在不同的地址空间提供局部代表。这样可以隐藏一个对象存在于不同地址空间的事实。
虚拟代理,是根据需要创建开销很大的对象。通过他来存放实例化需要很长时间的真实对象。
安全代理,用来控制真实对象访问时的权限。
智能指引,是指当调用真实的对象时,代理处理另外一些事。
例程
class Subject //Subject 定义了RealSubject和Proxy的共用接口..这样就在任何使用RealSubject的地方都可以使用Proxy
{
public:
virtual void func()
{
cout << "Subject" << endl;
}
};
class RealSubject :public Subject // RealSubject 定义proxy所代表的真实实体
{
public:
virtual void func()
{
cout << "RealSubject" << endl;
}
};
class Proxy : public Subject //proxy 保存一个引用使得代理可以访问实体,并且提供一个于Subject的接口相同的接口 这样代理就可以用来替代实体
{
RealSubject real;
public:
virtual void func()
{
cout << "Proxy" << endl;
real.func();
}
};
#include <iostream>
using namespace std;
class Subject //Subject 定义了RealSubject和Proxy的共用接口..这样就在任何使用RealSubject的地方都可以使用Proxy
{
public:
virtual void func()
{
cout << "Subject" << endl;
}
};
class RealSubject :public Subject // RealSubject 定义proxy所代表的真实实体
{
public:
virtual void func()
{
cout << "RealSubject" << endl;
}
};
class Proxy : public Subject //proxy 保存一个引用使得代理可以访问实体,并且提供一个于Subject的接口相同的接口 这样代理就可以用来替代实体
{
RealSubject real;
public:
virtual void func()
{
cout << "Proxy" << endl;
real.func();
}
};
int main()
{
Proxy proxy;
proxy.func();
return 0;
}
实际场景下,往往用到代理模式的情况都相对复杂。
类图
适配器模式
伪码
//新接口
class ITarget {
public :
virtual void process() = 0;
};
//老接口
class IAdaptee {
public:
virtual void foo(int data) = 0;
virtual int bar() = 0;
};
class Adapter :public ITarget {
protected:
IAdaptee* pAdaptee;
public:
virtual void process() {
int data = pAdaptee->bar();
pAdaptee->foo(data);
};
};
在实际问题中,往往业务会更加附加,适配器模式还是比较好理解,即是适配器继承新接口,包含原有接口,调用原有类的方法,以新接口的规则返回!
中介者模式
动机
在软件构建过程中,经常会出现多个对象相互关联交互的情况,对象之间常常会维持一种复杂的引用关系,如果遇到一些需求的更改,这种直接的引用关系将面临不断的变化。
在这种情况下,我们可使用一和“中介对象”来管理对象间的关联关系,避免相互交互的对象间的紧耦合引用关系,从而更好地抵御变化