一、简单工厂模式
1、面向对象的好处
通过封装、继承、多态把程序的耦合度降低。
用设计模式使得程序更加的灵活,容易修改,并且易于复用。
如实现计算机的功能时,将业务逻辑与界面逻辑分开,让它们的耦合度下降。只有分开,才可以达到容易维护或扩展。
2、紧耦合VS.送耦合
计算器有加、减、乘、除等功能,如果在实现加法功能时,影响到乘法功能,则说明两者的的耦合性高,否则为松耦合。
3、简单工厂模式
封装、继承和多态。
用类的方式实现封装,如加法类、减法类等;用父类、子类的方式实现继承,如运算类的派生类有加法类、减法类等;用虚函数的形式实现多态,如作为父类的运算类,其声明纯虚函数,在加法或减法类中实现该虚函数。额外用一个类(即简单工厂类)来确定实例化对象的类型,该类根据运算符的类型决定生成哪个类对象,根据该类对象在运行时的类型决定调用该虚函数的实现。
缺点:每添加新的运算功能,需要添加新的子类派生自运算类,还应在简单工厂类中添加该类的实例化代码。因此破坏了开放-封闭原则。
4、UML类图
(1)依赖关系
如动物依赖于氧气和水。用虚线+箭头,从动物分别指向氧气和水。
(2)合成(组合)关系
是一种强“拥有”关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。如鸟和其翅膀就是合成(组合关系)。合成关系用实心的棱形+实线箭头来表示。其中的数字"1"和数字“2”称为基数,表明这一端的类可以有几个实例,如鸟有两只翅膀。如果一个类可能有无数个实例,则就用’n’来表示。
(3)聚合关系
聚合表示一种弱“拥有”关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。如大雁和雁群这两个类,大雁是群居动物,每只大雁都是属于一个雁群,一个雁群可以有多只大雁,所以它们之间满足聚合关系。聚合关系用空心的棱形+实线箭头来表示。
(4)继承关系
如大雁类继承自动物类。如果是抽象类,就用斜体显示。其中,"+“表示public,”-"表示private,“#”表示protected。
(5)关联关系
当一个类"知道"另一个类时,可以用关联。如企鹅需要知道气候的变化,需要了解气候规律。关联关系用实线箭头来表示。
(6)接口图
如大雁的"飞翔"接口。用虚线表示。
计算器的UML类图如下:
5、在简单工厂模式下,计算器的实现代码
// SimpleFactoryModel.h文件
#pragma once
// 操作基类
template<typename T>
class COperator
{
public:
virtual T getResult() = 0;
virtual void setArgs(T lpa, T rpa);
protected:
T lah, rah;
};
template<typename T>
void COperator<T>::setArgs(T lpa, T rpa)
{
lah = lpa;
rah = rpa;
}
// 加法类
template<typename T>
class CAddOperator : public COperator<T>
{
public:
virtual T getResult()
{
return COperator<T>::lah + COperator<T>::rah;
}
};
// 减法类
template<typename T>
class CSubOperator : public COperator<T>
{
public:
virtual T getResult()
{
return COperator<T>::lah - COperator<T>::rah;
}
};
// 乘法类
template<typename T>
class CMulOperator : public COperator<T>
{
public:
virtual T getResult()
{
return COperator<T>::lah * COperator<T>::rah;
}
};
// 除法类
template<typename T>
class CDivOperator : public COperator<T>
{
public:
virtual T getResult()
{
if (0 == COperator<T>::rah)
{
std::cout << "除数不能为0" << std::endl;
return 0;
}
return COperator<T>::lah / COperator<T>::rah;
}
};
// 工厂类
template<typename T>
class CCalculatorFactory
{
public:
static COperator<T> * createObject(char c);
};
template<typename T>
COperator<T> * CCalculatorFactory<T>::createObject(char c)
{
COperator<T> * oper;
switch (c)
{
case '+':
oper = new CAddOperator<T>();
break;
case '-':
oper = new CSubOperator<T>();
break;
case '*':
oper = new CMulOperator<T>();
break;
case '/':
oper = new CDivOperator<T>();
break;
default:
oper = new CAddOperator<T>();
break;
}
return oper;
}
测试代码如下:
#include <iostream>
#include "SimpleFactoryModel.h"
int main()
{
using namespace std;
// 创建对象
COperator<double> *p = CCalculatorFactory<double>::createObject('/');
p->setArgs(23.23, 10.74);
cout << p->getResult() << endl;
delete p;
p = CCalculatorFactory<double>::createObject('+');
p->setArgs(23.23, 10.74);
cout << p->getResult() << endl;
delete p;
p = CCalculatorFactory<double>::createObject('-');
p->setArgs(23.23, 10.74);
cout << p->getResult() << endl;
delete p;
p = CCalculatorFactory<double>::createObject('*');
p->setArgs(23.23, 10.74);
cout << p->getResult() << endl;
delete p;
getchar();
return 0;
getchar();
return 0;
}
代码参考博文:
https://blog.csdn.net/konglongdanfo1/article/details/83380031
二、商场模式——策略模式
1、需求—商场收银系统
(1)正常情况,根据单价和数量计算收费;
(2)打折情况,如每件商品在原价的基础上打八折等;
(3)满减情况,如满300减30等。
2、简单工厂的实现
面向对象的百年城,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。
存在的问题:
商场可能经常性地更改打折额度和满减情况,每次维护或扩展收费方式要改动这个工厂,以至代码需要重新编译部署。
3、策略模式
策略模式定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化,不会影响到使用算法的客户。
“商场收银时如何促销,用打折还是返利,其实都是一些算法,用工厂来生成算法对象,这没有错,但算法本身只是一种策略,最重要的是这些算法随时都可能相互替换的,这就是变化点,而封装变化点是我们面向对象的一种很重要的思维方式。”
//抽象算法类
class Strategy
{
public:
virtual void AlgorithmInterface()=0;
}
//各ConcreteStrategy,封装了具体的算法或行为,继承于Strategy
//具体算法A
class ConcreteStrategyA:public Strategy
{
public:
//算法A实现方法
void AlgorithmInterface()[
std::cout << "算法A实现" << std::endl;
}
}
//具体算法B
class ConcreteStrategyB:public Strategy
{
public:
//算法A实现方法
void AlgorithmInterface()[
std::cout << "算法B实现" << std::endl;
}
}
//具体算法C
class ConcreteStrategyC:public Strategy
{
public:
//算法A实现方法
void AlgorithmInterface()[
std::cout << "算法C实现" << std::endl;
}
}
//Context,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用
class Context
{
public:
Context(Strategy strategy){
//初始化时,传入具体策略对象
this->strategy=strategy;
}
//上下文接口
void ContextInterface(){
//根据具体的策略对象,调用其算法的方法
strategy.AlgorithmInterface();
}
private:
Strategy strategy;
}
客户端代码:
int main()
{
Context* context;
//由于实例化不同的策略,所以最终在调用context->ContextInterface(),所获结果就不尽相同
context=new Context(new ConcreteStrategyA());
context->ContextInterface();
delete context;
context=new Context(new ConcreteStrategyB());
context->ContextInterface();
delete context;
context=new Context(new ConcreteStrategyC());
context->ContextInterface();
delete context;
getchar();
return 0;
}
4、策略模式与简单工厂结合
在策略模式下实现的商场收银代码如下:
//现金收费抽象类
class CashSuper
{
public:
virtual acceptCash(double money)=0;//
}
//正常收费子类
class CashNomal:public CashSuper
{
return money;
}
//打折收费子类
class CashRebate:public CashSuper
{
public:
CashRebate(string moneyRebate)
{
this->moneyRebate=moneyRebate;
}
double acceptCash(double)
{
return money*moneyRebate;
}
private:
double moneyRebate=ld;
}
//返利收费子类
class CashReturn:public CashSuper
{
public:
CashReturn(string monerCondition,string monerReturn)
{
this->moneyCondition=monerCondition;
this->moneyReturn=monerReturn;
}
double acceptCash(double money)
{
double result=monry;
if(money>=moneyCondition)
result=money-(money/moneyCondition)*moneyReturn;
return result;
}
private:
double moneyCondition=0.0;
double moneyReturn=0.0;
}
//CashContext 类
class CrashContext
{
public:
CrashContext(string type)//注意参数不是具体的收费策略对象,而是一个字符串,表示收费类型
{
//实例化具体策略的过程由客户端转移到Context类中。简单工厂的应用
switch(type)
{
case "正常收费":
CashNormal cs0=new CashNormal();
cs=cs0;
break;
case "满300返100":
CashReturn cr1=new CrashReturn("300","100");
cs=cr1;
break;
case "打8折":
CashRebate cr2=new CashRebate("0.8");
cs=cr2;
break;
}
}
double GetResult(double money)
{
return cs.acceptCash(Money);
}
private:
CashSuper cs=nullptr;
}
客户端测试代码:
//在简单工厂模式下:
CashSuper csuper=CashFactory::createCashAccept("类型");
...=csuper.GetResult(...);
//策略模式与简单工厂模式结合的用法:
CashContext csuper=new CashContext("类型");
...=csuper.GetResult(...);
5、策略模式解析
(1)策略模式的优点
策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,他们可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。
策略模式的Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。对于打折、返利或者其他的算法,其实都是对实际商品收费的一种计算方式,通过继承,可以得到它们的公共功能,即获得计算费用的结果GetResult,这使得算法间有了抽象的父类CashSuper。
策略模式简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。
每个算法可保证它没有错误,修改其中任一个时也不会影响其他的算法。
当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为。将这些行为封装在一个个独立的Strategy类中,可以在使用这些行为的类中消除条件语句。
策略模式是用来封装算法的,但在实践中,我们发现可与用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
在基本的策略模式中,选择所用具体实现的职责由客户对象承担,并转给策略模式的Context对象。这本身并没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现的职责也可以由Context来承担,这就最大化地减轻了客户端的职责。
(2)缺点
因为在CashContext里还用到了switch,也就是说,如果我们需要增加一种算法,比如"满200送50",就必须更改CashContext中的switch代码。即任何需求的变更都是需要成本的。
反射技术可以解决上述问题。
三、拍摄UFO——单一职责原则
1、单一原则职责
大多数时候,一件产品简单一些,职责单一一些,或许是更好的选择。这就和设计模式中的一大原则——单一职责的道理是一样的。
我们在做编程的时候,很自然就会给一个类加各种各样的功能,比如我们写一个窗体应用程序,一般都会生成一个Formal这样的类,于是我们就把各种各样的功能,像某种商业运算的算法呀,像数据库访问的SQL语句呀,都写到这样的类当中,这就意味着,无论任务需求要来,你都需要更改这个窗体类,维护麻烦,复用不可能,也缺乏灵活性。
单一职责原则(SRP):就一个类而言,应该仅有一个引起它变化的原因。
2、俄罗斯方块游戏的设计
(1)功能需求
首先方块下落动画的原理是画四个小方块,擦掉,然后再在下一行画四个方块。不断地绘出和擦掉就形成了动画,所以应该要有画和擦方块地代码。然后左右键实现左移和右移,下键实现加速,上键实现旋转,这其实都应该是函数,当然左右移动需要考虑碰撞的问题,下移需要考虑堆积和消层的问题。
(2)职责分析
下落、旋转、碰撞判断、移动、堆积这些游戏逻辑,和界面表示没有关系,不应该写在界面类中。
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个给类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受意向不到的破坏。
事实上,完全可以找出哪些是界面,哪些是游戏逻辑,然后进行分离。
所谓游戏逻辑,不过就是数组的每一项值变化的问题,下落、旋转、碰撞判断、移动、堆积这些都是在做数组具体项的值的变化。而界面表示逻辑,不过是根据数组的数据进行绘出和擦除,或者根据键盘命令调用数组的相应的方法进行改变。因此,至少应该考虑将此程序分为两个类,一个是游戏逻辑的类,一个是WInForm窗体的类。当有一天改变界面,或者更换界面时,不过是窗体类的变化,和游戏逻辑无关,以此达到复用的目的。
软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。其实要去判断是否应该分离出类来。也不难,那就是如果你能够想到多余一个的动机去改变一个类,那么这个类就具有多于一个的职责,就应该考虑类的职责分离。
界面的变化是和游戏本身没有关系的,界面是容易变化的,而游戏逻辑是不太容易变化的,将它们分开有利于界面的改动。
单一职责的代码易维护、易扩展、易复用、灵活多样。
四、考研求职两不误——开放-封闭原则
1、开放-封闭原则
开放-封闭原则(OCR),是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。
这个原则有两个特征,一个是说对于“扩展是开放的”,另一个是说对于“更改是封闭的”。
在面对需求的变化时,设计的软件可以相对容易改变,不至于说,新需求一来,就要把整个程序推倒重来。怎样的设计才能面对需求的改变却可以保持相对稳定,从而使得系统可以在第一个版本以后不断推出新的版本呢?
2、如何应对变化
开放-封闭原则的意思就是说,你设计的时候,时刻要考虑,尽量让这个类是足够好,写好了就不要修改了,如果新需求来,我们增加一些类就完事了,原来的代码能不动则不动。
然而,绝对的对修改关闭是不可能的。无论模块多么的“封闭”,都会存在一些无法对之封闭的变化。既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测最有可能发生的变化的中类,然后构造抽象来隔离那些变化。
预测变化往往是困难的,但可以等到变化发生时立即采取行动。
在我们最初编写代码时,假设变化不会发生。当发生变化时,我们就创建抽象来隔离以后发生的同类变化。
比如,第一章提到的加法程序,可以很快在一个client类中完成,此时变化还没有发生。然后让你加一个减法功能,你发现,增加功能需要修改原来这个类,这就违背了“开放-封闭原则”,于是你就考虑重构程序,增加一个抽象的运算类,通过一些面向对象的手段,如继承,多态等隔离具体加法、减法与client耦合,需求依然可以满足,还能应对变化。这时如果再加乘除法功能,就不需要再去修改client以及加法减法的类了,而是增加乘法和触发子类就可。即面对需求,对程序的改动是通过增加新代码进行的,而不是更改现有的代码。这就是开放-封闭原则的精神所在。
我们希望的是在开发工作展开不久就知道可能发生的变化。查明可能发生的变化所等待的时间越长,要创建正确的抽象就越困难。
开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是在可维护、可扩展、可复用、灵活性好。开发人员应该对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。
五、会修电脑不会修收音机?——依赖倒转原则
1、依赖倒转原则
依赖倒转原则,也翻译成依赖倒置原则,原意是抽象不应该依赖细节,细节应该依赖抽象,即针对接口编程,不要对实现编程。无论主板、CPU、内存、硬盘都是针对接口设计的,如果针对实现来设计,内存 就要对应到具体的某个品牌的主板。
概括来说:
高层模块不应该依赖低层模块。两个都应该依赖抽象;抽象不应该依赖细节。细节应该依赖抽象。
2、里氏代换原则
一个软件实体如果使用的是一个父类的话,那么一定适用其子类,而且它察觉不出父类和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化,简单地说,子类型必须能够替换掉它们的父类型。
只有当子类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。
正是由于子类类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展。
结合里氏代换原则,理解“高层模块不应该依赖低层模块,两个都应该依赖抽象”。
依赖倒转其实就是谁也不依赖谁,除了约定的接口,大家可以灵活自如。
依赖倒转其实可以说是面向对象设计的标志,用哪种语言来编写程序不重要,如果编写时考虑的都是如何针对抽象编程而不是针对细节编程,即程序中所有的依赖关系都是终止于抽象类或者接口类,俺就是面向对象的设计,反之那就是过程化的设计了。
六、穿什么有这么重要?——装饰模式
1、需求
写一个可以给人搭配不同的服饰的系统,比如类似QQ、网络游戏或论坛都有的Avatar系统。
服饰有大T恤、垮裤、破球鞋、西装、领带、皮鞋等。可以任意组合搭配。
2、装饰模式
动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更加灵活。
具体代码如下:
//Component类
class Component
{
public:
virtual void Operation()=0;
}
//ConcreteComponent类
class ConcreteComponent:public Component
{
public:
void Operation(){
cout<<"具体对象的操作"<<endl;
}
}
//Decorator 类
class Decorator:public Component
{
public:
void SetComponent(Component* component){
//设置Component
this->m_component=component;
}
void Operation()//重写Operation(),实际执行的是Component的Operation()
{
if(m_component!=nullptr)
{
m_component->Operation();
}
protected:
Componnent* m_component;
}
//ConcreteDecoratorA类
class ConcreteDecoratorA:public Decorator
{
public:
virtual void Operation()
{
m_component->Operation();//首先运行原Componnent的Operation(),再执行本类的功能,
//如addedState,相当于对原Component进行了装饰
addedState="New State";
cout<<"具体装饰对象A的操作"<<endl;
}
private:
string addedState;//本类的独有功能,以区别ConcreteDecoratorB
}
//ConcreteDecoratorB类
class ConcreteDecoratorB:public Decorator
{
public:
virtual void Operation()
{
m_component->Operation();//首先运行原Componnent的Operation(),再执行本类的功能,
//如addedState,相当于对原Component进行了装饰
AddedBehavior();
cout<<"具体装饰对象A的操作"<<endl;
}
private:
void AddedBehavior(){
}//本类的独有方法,以区别ConcreteDecoratorA
}
//测试代码如下
int main()
{
ConcreteComponent c=new ConcreteComponent();
ConcreteDecoratorA d1=new ConcreteDecoratorA();
ConcreteDecoratorB d2=new ConcreteDecoratorB();
d1->SetComponent(c);//装饰的方法是:首先用ConcreteComponent实例化对象c,然后用ConcreteDecoratorA的实例化对象d1来包装c,
d2->SetComponent(d1);//再用ConcreteDecoratorB的对象d2来包装d1,
d2->Operation();//最终执行d2的Operation()
delete c;
delete d1;
delete d2;
getchar();
return 0;
}
装饰模式是利用SetComponent来对对象进行包装的。这样每个装饰对象的实现就和如何使用这个对象分开了,每个装饰对象只关心自己的功能,不需要关信如何被添加到对象链当中。
注:这里的“实现”是指包装的内容,而“使用”是指包装。
3、装饰模式下的服饰搭配
如果只有一个ConcreteComponent类而没有抽象的Component类,那么Decorator类是可以是ConcreteComponent的一个子类。同样的道理,如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合成一个类。
这里没有必要有Component类,直接让服饰类Decorator继承人类ConcreteComponent。
代码如下:
//Person类(ConcreteComponent)
class Person
{
public:
Person(string name)
{
this->name=name;
}
virtual void Show()
{
cout<<"装扮的{0}"<<name<<endl;
}
private:
string name;
}
//服饰类(Decorator)
class Finery:public Person
{
public:
void Decorate(Person* component)
{
this->m_component=component;
}
virtual void Show()//重写Show(),实际执行的是Component的Show()
{
if(nullptr!=m_component)
m_component->Show();
}
protected:
Person* m_component;
}
//具体服饰类(ConcreteDecorator)
class TShirts:public Finery
{
public:
void Show()
{
cout<<"大T恤"<<endl;
m_component->Show();//运行Person的Show()
}
}
//具体服饰类(ConcreteDecorator)
class BigTrouser:public Finery
{
public:
void Show()
{
cout<<"垮裤"<<endl;
m_component->Show();//首先运行原Person的Show()
}
}
//其余类似,省略
测试代码如下:
int main()
{
Person xc=new Person("周周");
cout<<"第一种装扮:"<<endl;
Sneekers pqs=new Sneekers();
BigThrouser kk=new BigThrouser();
TShirts dtx=new TShirts();
pqs->Decorate(xc);
kk->Decorate(pqs);
dtx->Decorate(kk);
dtx->Show();
delete pqs;
delete kk;
delete dts;
cout<<"第二种装扮:"<<endl;
LeatherShoes px=new LeatherShoes();
Tie ld=new Tie();
Suit xz=new Suit();
px->Decorate(xc);
ld->Decorate(px);
xz->Decorate(ld);
xz->Show();
delete px;
delete ld;
delete xz;
delete xc;
getchar();
return 0;
}
结果显示:
第一种装扮:
大T恤 垮裤 破球鞋 装扮的周周
第二种装扮:
西装 领带 皮鞋 装扮的周周
可见,是用服饰Finery修饰人Person。
4、装饰模式总结
装饰模式是为已有功能动态地添加更多功能的一种方式。
当系统需要新功能的时候,是向旧的类中添加新的代码。这些新加的代码通常修饰了原有类的核心职责或主要行为,比如用西装或嘻哈服来修饰周周。但这种做法的问题在于,它们在主类中加入了新的字段,新的方法和新的逻辑,从而增加了主类的复杂度,就像起初的那个“人”类,而这些新加入的东西仅仅是为了满足一些只在某种特定情况下才会执行的特殊行为的需要。而装饰模式却提供了一个非常好的解决方案,它把每个要装饰的功能放在单独的类中,并让这个类包装它所要装饰的对象,因此,当需要执行特殊行为时,客户代码就可以在运行时根据需要有选择地、按顺序地使用装饰功能包装对象了。
装饰模式的优点总结如下:
把类中的装饰功能从类中搬移去除,这样可以简化原有的类;
有效地把类的核心职责和装饰功能区分开了。而且可以去除相关类中重复的装饰逻辑。
但是,使用装饰模式时要注意装饰顺序,比如加密数据和过滤词汇都可以是数据持久化前的装饰功能,但若先加密了数据再用过滤功能就会出问题,最理想的情况,是保证装饰类之间彼此独立,这样它们就可以以任意的顺序进行组合了。
七、为别人做嫁衣——代理模式
1、代理模式
代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。
假设如下场景:追求者想给一个女孩送礼物、送花等,但他不好意思出现,所以麻烦其他同学代送,这里其他同学就是代理者。
//Subject类,定义了RealSubject和Proxy的共用接口,这样就可以再任何使用RealSubject的地方都可使用Proxy
class Subject
{
public:
virtual void Request()=0;
}
//RealSubject类,定义Proxy所代表的真实实体
class RealSubject:public Subject
{
public:
void Request()
{
cout<<"真实的请求"<<endl;
}
}
//Proxy类,保存一个引用使得代理可以访问实体,并提供一个与Subject的接口相同的接口,这样代理就可以用来替代实体
class Proxy:public Subject
{
public:
void Request()
{
if(nullptr!=m_realSubject)
m_realSubject=new RealSubject();
m_realSubject->Request();
}
private:
RealSubject* m_realSubject;
}
测试代码如下:
int main()
{
Proxy* proxy= new Proxy();
proxy->Request();
delete proxy;
getchar();
return 0;
}
2、代理模式应用
代理模式的使用场合分为以下几种:
(1)远程代理
也就是为一个对象在不同的地址空间提供局部代表。这样可以隐藏一个对象存在于不同地址空间的事实。
例如在应用程序的项目中加入一个Web引用,引用一个WebService,此时会在项目中生成一个WebReference的文件夹和一些文件,其实它们就是代理,这就使得客户端程序调用代理就可以解决远程访问的问题。
(2)虚拟代理
根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象。这样可以达到性能的最优化,比如说你打开一个很大的HTML网页时,里面很可能有很多的文字和图片,但你还是可以很快打开它,此时你所看到的是所有的文字,但图片却是一张一张地下载后才能看到。那些未打开地图片框,就是通过虚拟代理替代了真实地图片,此时代理存储了真实图片的路径和尺寸。即浏览器当中是用代理模式来优化下载的。
(3)安全代理
用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候。
(4)智能指引
是指当调用真实的对象时,代理处理另外一些事。如计算真实对象的引用计数,这样当该对象没有引用时,可以自动释放它;或当第一次引用一个持久对象时,将它装入内存;或在访问一个实际对象前,检查是否已经锁定它,以确保其他对象不能改变它。它们都是通过代理在访问一个对象时附加一些内务处理。
代理其实就是在访问对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。
八、雷锋依然在人间——工厂方法模式
1、工厂方法模式实现
计算器的简单工厂模式结构图如下:
计算器的工厂方法模式结构图如下:
先构造一个工厂接口:
Interface IFactory
{
virtual Operation CreateOperation()=0;
}
然后加减乘除各建一个具体工厂去实现这个接口:
//加法类工厂
class AddFactory:public IFactory
{
public:
Operation CreateOperation()
{
return new OperationAdd();//返回加法类对象
}
}
//减法类工厂
class SubFactory:public IFactory
{
public:
Operation CreateOperation()
{
return new OperationSub();//返回减法类对象
}
}
//乘法类工厂
class MulFactory:public IFactory
{
public:
Operation CreateOperation()
{
return new OperationMul();//返回乘法类对象
}
}
//除法类工厂
class DivFactoryFactory:public IFactory
{
public:
Operation CreateOperation()
{
return new OperationDiv();//返回乘法类对象
}
}
客户端的实现是这样的:
IFactory* operFactory=new AddFactory();
Operation* oper=operFactory->CreateOperation();
oper->NumberA=1;
oper->NumberB=2;
double result=oper->GetResult();
2、简单工厂vs.工厂方法
简单工厂模式的最大优点在于工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。就像第一章提到的计算器,让客户端不用管该用哪个类的实例,只需要把"+"给工厂,工厂自动给出了相应的实例,客户端只要去做运算就可以了,不同的实例会实现不同的运算。但问题也就在这里,如果要加一个“求M数的N次方”的功能,是一定要给运算工厂类的方法里加“case”的条件分支的,修改原有的类,这就意味着,我们不但对扩展开放了,对修改也开放了,这样就违背了开放-封闭原则。
工厂方法模式,定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
“既然这个工厂类与分支耦合,那么我就对它下手,根据依赖倒转原则,我们把工厂类抽象出一个接口,这个接口只有一个方法,就是创建抽象产品的方法。然后,所有的要生产具体类的工厂,就去实现这个接口,这样搞,一个简单共产模式的工厂类,就变成了一个工厂抽象接口和多个具体生成对象的工厂,于是我们要增加“M数的N次方”的功能时,就不需要更改原有的工厂类了,只需要增加此功能的运算类和相应的工厂类就可以了。”
缺点:工厂方法模式是实现时,客户端需要决定实例化哪一个工厂来实现运算类,选择判断的问题还是存在的,也就是说,工厂方法把简单工厂的内部逻辑判断移到了客户端代码来进行。你想要加功能,本来是改工厂类的,而现在是修改客户端。
3、雷锋工厂
(1)用简单工厂模式实现雷锋行为
雷锋类,拥有扫地、洗衣、买米等方法
class leiFeng
{
public:
void Sweep()
{
cout<<"扫地"<<endl;
}
void Wash()
{
cout<<"洗衣"<<endl;
}
void BuyRice()
{
cout<<"买米"<<endl;
}
}
学雷锋的大学生
class Undergraduete:public LeiFeng
{
}
社区志愿者
class Volunteer:public LeiFeng
{
}
简单工厂类
class SimpleFactory
{
public:
static LeiFeng CreateLeiFeng(string type);
{
LeiFeng* result= nullptr;
switch(type)
{
case "学雷锋的大学生":
result=new Undergraduate();
break;
case "社区志愿者"
result=new Volunteer();
break;
}
}
return result;
}
测试代码:
int main()
{
LeiFeng* studentA=SimpleFactory::CreateLeiFeng("学雷锋的大学生");//三句重复的代码
studentA->BuyRice();
LeiFeng* studentB=SimpleFactory::CreateLeiFeng("学雷锋的大学生");
studentB->Sweep();
LeiFeng* studentC=SimpleFactory::CreateLeiFeng("学雷锋的大学生");
studentC->Wash();
getchar();
return 0;
}
(2)用工厂方法模式实现雷锋行为
雷锋工厂:
interface IFactory
{
virtual LeiFeng CreateLeiFeng()=0;
}
学雷锋的大学生工厂
class Undergraduete:public IFactory
{
public:
LeiFeng CreateLeiFeng()
{
return new Undergraduate();
}
}
学雷锋的社区志愿者工厂
class Volunteer:public IFactory
{
public:
LeiFeng CreateLeiFeng()
{
return new Volunteer();
}
}
测试代码:
int main()
{
IFactory* factory=new UndergraduateFactory();//要换成社区志愿者修改这里即可
LeiFeng* student=factory->CreateLeiFeng();
student.BuyRice();
student.Sweep();
student.Wash();
delete factory;
getchar();
return 0;
}
对比上述两种模式,可以发现,它们都是集中封装了对象的创建,使得要更换对象时,不需要做大的改动就可实现,降低了客户程序与产品对象的耦合。工厂模式是简单工厂模式的进一步抽象和推广。由于使用了多要性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。但缺点是由于每一加一个产品,就需要加一个产品工厂的类,增加了额外的开发量。
九、简历复印——原型模式
1、原型模式
原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需知道任何创建的细节。
原型类:
abstract class Prototype
{
public:
virtual Prototype* clone()=0; //抽象类关键就是有这样一个Clone方法
}
具体原型类:
class ConcretePrototype1:public Prototype
{
public:
ConcretePrototype1(string name):m_strName(name){
}
virtual Prototype* clone()
{
ConcretePrototype1* p=new ConcretePrototype1(m_strName);
*p=*this;
return *p;
}
void show()
{
cout<<m_strName<<endl;
}
private:
string m_mStrName;
}
class ConcretePrototype2:public Prototype
{
public:
ConcretePrototype2(string name):m_strName(name){
}
virtual Prototype* clone()
{
ConcretePrototype2* p=new ConcretePrototype2(m_strName);
*p=*this;
return p;
}
void show()
{
cout<<m_strName<<endl;
}
private:
string m_strName;
}
测试代码如下:
int main()
{
ConcretePrototype1 *p1=new ConcretePrototype1("1");
ConcretePrototype2 *p2=(ConcretePrototype2*)(p1->clone());
p1->show();//结果为1
p2->show();//结果为1
delete p1;
delete p2;
getchar();
return 0;
}
2、简历的原型实现
原型类:
class ICloneable
{
public:
virtual ICloneable* clone()=0;
}
简历类:
class Resume: public ICloneable
{
public:
Resume(string name)
{
this->m_strName=name;
}
//设置个人信息
void SetPersonalInfo(string sex,string age)
{
this->m_strSex=sex;
this->m_strAge=age;
}
//设置工作经历
void SetWorkExperience(string timeArea,string company)
{
this->m_strTimeArea=timeArea;
this->m_strCompany=company;
}
void show()
{
cout<<"个人信息:"<<m_strName<<m_strSex<<m_strAge<<endl;
cout<<"工作经历:"<<m_strTimeArea<<m_strCompany<<endl;
}
Resume* clone()
{
Resume* r=new Resume(m_strName); //深拷贝
*r=*this;
return r;
}
private:
string m_strName;
string m_strSex;
string m_strAge;
string m_strTimeArea;
string m_strCompany;
}
测试代码:
int main()
{
Resume* a=new Resume("a");
a->SetPersonalInfo("女","18");
a->SetWorkExperience("2019-2020","XXX公司");
Resume* b=(Resume*)(a->clone());//只需要调用Clone方法就可以实现简历的生成,并且可以再修改新简历的细节
b->SetPersonalInfo("女","20");
a->show();
b->show();
getchar();
return 0;
}
一般再初始化的信息不发生变化的情况下,克隆是最好的办法。这既隐藏了对象创建的细节,又对性能是大大的提高。
等于不用重新初始化对象,而是动态地获得对象运行时地状态。
3、浅复制与深复制
浅复制:被复制对象地所有变量都含有与原来地对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。
深复制:把因哟个对象的变量指向复制过的新对象,而不是原有的被引用的对象。
深复制要避免循环引用。
十、考题抄错也会白搭——模板方法模式
1、需求场合
“既然用来继承,并且肯定这个继承有意义,就应该要成为子类的模板,所有重复的代码都应该要上升到父类去,而不是让每个子类都去重复。”
当我们要完成某一细节层一致的一个过程或一系列步骤,但其个别步骤在更详细的层次上的实现可能不同时,我们通常考虑用模板方法来处理。
如每个学生需要抄题目然后作答,则对每个学生而言,所抄的题目是一样的,只是答案不同。
代码结构图如下:
2、模板方法模式
模板方法模式,定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
AbstractClass是抽象类,其实也就是一抽象模板,定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在响应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
class AbstractClass
{
public:
virtual void PrimitiveOperation1()=0;//一些抽象行为,放到子类去实现
virtual void PrimitiveOperation2()=0;
void TemplateMethod()//模板方法,给出了逻辑的骨架,而逻辑的组成是一些相应的抽象操作,它们都推迟到子类实现
{
PrimitiveOperation1();
PrimitiveOperation2();
cout<<""<<endl;
}
}
ConcreteClass,实现父类所定义的一个或多个抽象方法。每一个AbstractClass都可以有任意多个ConcreteClass与之对应,而每一个ConcreteClass都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
ConcreteClassA:public AbstractClass
{
public:
virtual void PrimitiveOperation1
{
cout<<"具体A类方法1的实现"<<<endl
}
virtual void PrimitiveOperation2
{
cout<<"具体A类方法2的实现"<<<endl
}
}
ConcreteClassB:public AbstractClass
{
public:
virtual void PrimitiveOperation1
{
cout<<"具体B类方法1的实现"<<<endl
}
virtual void PrimitiveOperation2
{
cout<<"具体B类方法2的实现"<<<endl
}
}
测试代码:
int main()
{
AbstractClass* c;
c=new ConcreteClassA();
c->TemplateMethod();//具体A类方法1的实现 具体A类方法2的实现
delete c;
c=new ConcreteClassB();
c->TemplateMethod();//具体B类方法1的实现 具体B类方法2的实现
delete c;
getchar();
return 0;
}
3、模板方法模式特点
模板方法是通过不变行为搬移到超类,去除子类中重复的代码来体现它的优势。
**模板方法提供了一个很好的代码复用平台。**有的时候,我们会遇到由一系列步骤构成的过程需要执行。这个过程从高层到底层看是相同的,但有些步骤的实现可能不同。这时候,我们通常就应该该考虑用模板方法模式了。
当不变和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。
十一、无熟人难办事?——迪米特法则
迪米特法则,也叫最少知识原则。
如果两个类不必直接彼此通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限,也就是说,一个类包装好自己的private状态,不需要让别的类知道的字段或行为就不要公开。
迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说,信息的隐藏促进了软件的复用。
十二、牛市股票还会亏钱?——外观模式
1、需求场合
原来客户需要决定买进卖出股票一、股票二、股票三、国债1和房地产1。而通过基金,客户只需要决定买进卖出基金即可,由基金决定买进卖出股票一、股票二、股票三、国债1和房地产1。
代码结构图如下:
2、外观模式
外观模式,为了系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
四个子系统的类:
class SubSystemOne
{
public:
void MethodOne()
{
cout<<"子系统方法一"<<endl;
}
}
class SubSystemTwo
{
public:
void MethodTwo()
{
cout<<"子系统方法二"<<endl;
}
}
class SubSystemTree
{
public:
void MethodTree()
{
cout<<"子系统方法三"<<endl;
}
}
class SubSystemFour
{
public:
void MethodFour()
{
cout<<"子系统方法四"<<endl;
}
}
外观类:需要了解所有的子系统的方法或树形,进行组合,以备外界调用
class Facade
{
public:
Facade()
{
one=new SubSystemOne();
two= new SubSystemTwo();
three=new SubSystemThree();
four=new SubSystemFour();
}
~Facade()
{
delete one;
delete two;
delete three;
delete four;
}
void MethodA()
{
cout<<"方法组A"<<endl;
one->MethodOne();
two->MethodTwo();
four->MethodFour();
}
void MethodB()
{
cout<<"方法组B"<<endl;
two->MethodTwo();
three->MethodThree();
}
private:
SubSystemOne* one;
SubSystemTwo* two;
SubSystemThree* three;
SubSystemFour* four;
}
测试代码如下:
int main()
{
Facade* facade=new Facade();
facade->MethodA();
facade->MethodB();
delete facade;
gatchar();
return 0;
}
外观模式完美地体现了依赖倒转原则和迪米特法则地思想。
3、何时使用外观模式
外观模式在什么时候使用最好呢?
分三个阶段来说。
首先,在设计初期阶段,应该要有意识的将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层和业务逻辑层、业务逻辑层和表示层的层与层之间建立外观Facade,这样就可以为复杂的子系统提供一个简单的接口,使得耦合大大降低。
其次,在开发阶段,子系统往往因不断的重构演化而变得越来越复杂,大多数的模式使用也都会产生很多很小的类,这本是好事,但也给外部调用它们的用户程序带来了使用上的困难,增加外观Facade可以提供一个简单的接口,减少它们之间的依赖。
第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求开发必须依赖于它。此时,用外观模式也是非常合适的。你可以为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Facade对象交互,Facade与遗留代码交互所有复杂的工作。
对于复杂难以维护的老系统,直接去改或扩展否有可能产生很多问题,分成两个小组,一个开发Facade与老系统的交互,另一个只要了解Facade的接口,直接开发新系统调用这些接口即可,确实可以减少很多不必要的麻烦。
十三、好菜每回味不同——建造者模式
1、建造者模式
在游戏程序里建造小人,这里建造小人的“过程”是稳定的,都需要头身手脚,而具体建造的“细节”是不同的,有胖有瘦有高有矮。但对于用户来讲,我才不管这些,我只想告诉你,我需要一个胖小人来游戏,于是你就创建一个给我就好了。
如果你需要将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示的意图时,我们需要应用一个设计模式,‘建造者模式’,又叫生成器模式。建造者模式可以将一个产品的内部表象与产品的生成过程分割开来,从而可以使一个建造过程生成具有不同的内部表项的产品对象。如果我们用了建造者模式,那么用户就只需指定需要建造的类型就可以得到它们,而具体建造的过程的细节就不需知道了。
其实建造者模式是逐步建造产品的,所以建造者的Builder类里的那些建造方法必须要足够普遍,以便为各种类型的具体建造者构造。
游戏中小人的建造者模式结构图:
因为PersonBuilder是一个抽象类,因而继承自它的子类必须实现其中所有的纯虚函数。
2、建造者模式解析
简单理解就是Builder中定义了创建Product各个部分的接口。ConcreteBuilder中具体实现了创建Product中的各个部分的接口,就是具体的建造者。Director是根据用户的需求构建Product的(具体怎么构建,怎么把Product中的各个部件构建起来。
**建造者模式主要用于创建一些复杂的对象,这些对象内部构建的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。**如建造游戏中的小人,建造过程是固定的,都需要建头、身体、左手、右手、左脚、右脚,但建造胖子和瘦子时,各过程的具体值不相同。
**建造者模式的好处就是使得建造代码与表示代码分离,由于建造者隐藏了该产品是如何组装的,所以若需要改变一个产品的内部表示,只需要再定义一个具体的建造者就可以了。**如建造游戏中小人,添加高个子和矮个子等。
说到这里建造者模式具体实现的功能与模板方法模式有点类似。比如可以有不同得ConcreteBuilder类实现不同得创建方式。这样从应用上模板方法模式和建造者模式实现的功能是很相似的。但是建造者模式的侧重点是在于Director的不同,可以有不同的呈现方式,而模板方法模式的侧重点是算法中每一步实现的不同。另外,它们的类之间的关系是不同的。模板方法是通过继承的方式来实现的,而建造者模式是通过组合的方式实现的。模板方法模式主要用于执行不同的算法,建造者模式主要用于构建对象。不过者两种模式其实都可以实现很多相似的功能。这也没什么,本来同样的功能的实现方式也是多种多样的。主要看需求。另外在实际写程序的时候也不必拘泥于某种设计模式,只要遵守相关的原则,使整个程序高效、稳定、易扩展、易维护就行。两者使用的场景也有区别。
3、建造者模式基本代码
Product类——产品类,由多个部件组成:
class Product
{
public:
void Add(string part)
{
m_vec.push_back(part);
}
void show()
{
cout<<"产品创建"<<endl;
for(auto it=m_vec.begin();it<m_vec.end();it++) //列举所有的产品部件
cout<<"*it"<<endl;
}
private:
vector<string> m_vec;
}
Builder类——抽象建造者类,确定产品由两个部件PartA和PartB组成,并声明一个得到产品建造后结果的方法GetResult.
class Builder
{
public:
virtual void BuilderPartA()=0;
virtual void BuilderPartB()=0;
virtual Product* GetResult()=0;
}
ConcreteBuilder1类——具体建造者类
class ConcreteBuilder1:public Builder
{
public:
//Product()
//{
//m_product=new Product;
//}
//Product()
//{
//if(nullptr!=m_product)
//delet m_product;
//}
virtual void BuilderPartA()
{
m_product->Add("部件A");
}
virtual void BuilderPartB()
{
product->Add("部件B");
}
Product* GetResult()
{
return m_product;
}
private:
Product* m_product;
}
ConcreteBuilder2类——具体建造者类
class ConcreteBuilder2:public Builder
{
public:
//Product()
//{
//m_product=new Product;
//}
//Product()
//{
//if(nullptr!=m_product)
//delet m_product;
//}
virtual void BuilderPartA()
{
m_product->Add("部件X");
}
virtual void BuilderPartB()
{
product->Add("部件Y");
}
Product* GetResult()
{
return m_product;
}
private:
Product* m_product;
}
Director类——指挥者类
class Director
{
public:
bulid(Builder* builder)
{
bulider->BuilderPartA();//用来指挥建造过程
bulider->BuliderPartB();
}
}
测试代码:
int main()
{
Director* director=new Director();
Builder* b1=new ConcreteBuilder1();
Builder* b2=new ConcreteBuilder2();
director->bulid(b1);
Product* p1=b1.GetResult();
p1->show();
getchar();
return 0;
}
建造者模式是在当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方法时适用的模式。
十四、老板回来,我不知道——观察者模式
原来是通知者类中声明观察者类,观察者类中声明观察者类,也就是两者都与具体的类相耦合。现将将观察者行为抽象出来,形成抽象观察者;将通知者抽象出来,形成抽象通知者。这样,便是具体的类与抽象的类耦合。
1、观察者模式
观察者模式又叫发布-订阅模式,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
Subject类,可翻译为主题或抽象通知者,一般用一个个抽象类或者一个接口实现。它把所有对观察者对象的引用保存在一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。代码如下:
class Subject
{
public:
//增加观察者
virtual void Attach(Observer* observer)=0;
//移除观察者
virtual void Detach(Observer* observer)=0;
//通知
virtual void Notify()=0;
virtual void chagestate(string str)
{
m_strState=str;
Notify();
}
protected:
string m_strState;
}
Observer类,抽象观察者,为所有的具体观察者定一个一个接口,在得到主题的通知时更新自己。这个接口就叫做更新接口。抽象观察者一般用一个抽象类或者一个接口实现。更新接口通常包含一个Update()方法,这个方法叫做更新方法。代码如下:
class Observer
{
public:
virtual void Update()=0;
}
ConcreteSubject 类,叫做具体主题胡具体通知者,将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色通常用一个具体子类实现。代码如下:
class ConcreteSubject:public Subject
{
public:
ConcreteSubject(){
m_strState="";}
~ConcreteSubject(){
m_observers.clear()}
//增加观察者
void Attach(Observer* observer)
{
m_observers.push_back(observer);
}
//移除观察者
void Detach(Observer* observer);
{
auto it=find(observer.begin(),observer.end(),observer);
if(it!=observer.end())
m_observers.erase(it);
}
//通知
void Notify()
{
for(auto it=m_observers.begin();it!=m_observers.end();it++)
{
cout<<m_strState<<endl;
(*it)->Update();
}
}
void ChangeState(string str)
{
m_strState=str;
Notify();
}
private:
vector<Observer* > m_observers;
}
ConcreteObserver类,具体观察者类,实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。具体观察者角色可以保存一个指向具体对象的引用。具体观察者角色通常用一个具体子类实现。代码如下:
class ConcreteObserver1:public Observer
{
public:
virtual void Update()
{
cout<<"ConcreteObserver1知道了"<<endl;
}
}
class ConcreteObserver2:public Observer
{
public:
virtual void Update()
{
cout<<"ConcreteObserver2知道了"<<endl;
}
}
测试代码如下:
int main()
{
ConcreteSubject* s=new concreteSubject();
ConcreteObserver1* b1=new ConcreteObserver1();
ConcreteObserver2* b2=new ConcreteObserver1();
s->Attach(b1);
s->Attach(b2);
s->ChangeState("老板来了");
s->Detach(b2);
delete s;
delete p2;
delete p1;
getchar();
return 0;
}
2、观察者模式特点
**将一个系统分割成一系列相互写作的类有一个很不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。**而观察者模式的关键对象是主题Subject和观察者Observer,一个Subject可以有任意数目的依赖它的Observer,一旦Subject的状态发生了改变,所有Observer都可以得到通知。Subject发出通知时并不需要知道它是谁的观察者,也就是说,具体观察者是谁,它根部不需要知情。而任何一个具体观察者不知道也不需要知道其他观察者的存在。
在何时需要使用观察者模式:
当一个对象的改变需要同时改变其他对象的时候,而且它不知道具体有多少对象有待改变时,应该考虑使用观察者模式。但一个抽象模型有两个方面,其中一方面依赖于另一方面,这时用观察者模式可以将这两者封装在独立的对象中使它们各自独立地改变和复用。
总的来说,观察者模式所做的工作其实就是在解除耦合。让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响另一边的变化。(比如增加通知者等)
现实编程中,具体的观察者完全有可能是没有任何关系的类,但它们都需要根据通知者的通知来做出Update()的操作,所以让它们都实现下面这样的一个接口就可以。
interface Observer
{
void Update();
}
3、观察者模式的不足
尽管已经用了依赖倒转原则,但是"抽象通知者"还是依赖“抽象观察者”,也就是说,万一没有了抽象观察者这样的接口,我这通知的功能就完不成了。另外就是每个具体观察者,它不一定是“更新”的方法要调用。
如果通知者和观察者之间根本就相互不知道,由客户端来决定通知谁,就好了。
4、事件委托
观察者模式也有不足,观察者模式需要观察者需要实现相同的接口。但是如果已经写好的类或者第三方的类库则就没办法实现该功能了。所以可以稍稍改进一下,就是把Subject类中的关于观察者的列表修改为函数指针的列表。示例码如下:
Subject类改为:
class Subject
{
public:
//增加观察者
virtual void Attach(void (*p)())=0;
//移除观察者
virtual void Detach(void (*p)())=0;
//通知
virtual void Notify()=0;
virtual void chagestate(string str)
{
m_strState=str;
Notify();
}
protected:
string m_strState;
}
ConcreteSubject类改为:
class ConcreteSubject:public Subject
{
public:
ConcreteSubject(){
m_strState="";}
~ConcreteSubject(){
m_observers.clear()}
//增加观察者
void Attach(void (*p)())
{
m_observers.push_back(p);
}
//移除观察者
void Detach(void (*p)());
{
auto it=find(observer.begin(),observer.end(),observer);
if(it!=observer.end())
m_observers.erase(it);
}
//通知
void Notify()
{
for(auto it=m_observers.begin();it!=m_observers.end();it++)
{
cout<<m_strState<<endl;
(*it)();
}
}
void ChangeState(string str)
{
m_strState=str;
Notify();
}
private:
vector<void (*)() > m_observers;
}
ConcreteObserver类改为:
class ConcreteObserver1:public Observer
{
public:
static void Update()
{
cout<<"ConcreteObserver1知道了"<<endl;
}
}
class ConcreteObserver2:public Observer
{
public:
static void Update()
{
cout<<"ConcreteObserver2知道了"<<endl;
}
}
测试代码如下:
int main()
{
ConcreteSubject* s=new concreteSubject();
ConcreteObserver1* b1=new ConcreteObserver1();
ConcreteObserver2* b2=new ConcreteObserver1();
s->Attach(ConcreteObserver1::Update);
s->Attach(ConcreteObserver2::Update);
s->ChangeState("老板来了");
s->Detach(ConcreteObserver2::Update);
delete s;
delete p2;
delete p1;
getchar();
return 0;
}
委托就是一种引用方法的类型,一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值。委托可以看做是对函数的抽象,是函数的“类”,委托的实例将代表一个具体的函数。
一个委托可以搭载多个方法,所有方法被依次唤起。更重要的是,它可以使得委托对象所搭载的方法并不需要属于同一个类。
委托对象所搭载的所有方法必须具有相同的原形和形式,也就是拥有相同的参数列表和返回值类型。
十五、就不能不换DB吗?——抽象工厂模式
从一个数据库换到另一个数据库,这两个数据库的使用不同。
1、抽象工厂模式
抽象工厂模式,提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
AbstractProductA和AbstractProductB是两个抽象产品,之所以抽象,是因为它们都有可能有两种不同的实现,而ProductA1和ProductA2和ProductB1、ProductB2就是对这两个抽象产品的具体分类的实现。IFactory是一个抽象工厂接口,它里面应该包含所有产品创建的抽象方法。而ConcreteFactory1和ConcreteFactory2就是具体的工厂了。
通常是在运行时刻再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。
举个例子吧,就拿键盘和鼠标来说吧,键盘和鼠标就是一些列的类。而键盘鼠标又有微软的和联想的区别。所有有了微软的工厂和联想的工厂。键盘和鼠标具体的创建由工厂来负责,而具体创建微软的还是联想的则由客户端来指定,而客户端根本不用知道鼠标和键盘这些类。此例的示例代码如下:
// AbstractFactoryModel.h文件
#pragma once
#include <iostream>
// 键盘
class KeyBoard
{
public:
virtual void show() = 0;
};
// 微软的键盘
class KeyBoardMicro : public KeyBoard
{
public:
void show()
{
std::cout << "微软的键盘" << std::endl;
}
};
// 联想的键盘
class KeyBoardLenovo : public KeyBoard
{
public:
void show()
{
std::cout << "联想的键盘" << std::endl;
}
};
// 鼠标
class Mouse
{
public:
virtual void show() = 0;
};
class MouseMicro : public Mouse
{
public:
void show()
{
std::cout << "微软的鼠标" << std::endl;
}
};
class MouseLenovo : public Mouse
{
public:
void show()
{
std::cout << "联想的鼠标" << std::endl;
}
};
// 工厂
class Factory
{
public:
virtual KeyBoard * createKeyBoard() = 0;
virtual Mouse * createMouse() = 0;
};
// 微软的工厂
class FactoryMicro : public Factory
{
public:
KeyBoard * createKeyBoard()
{
return new KeyBoardMicro();
}
Mouse * createMouse()
{
return new MouseMicro();
}
};
// 联想的工厂
class FactoryLenovo : public Factory
{
public:
KeyBoard * createKeyBoard()
{
return new KeyBoardLenovo();
}
Mouse * createMouse()
{
return new MouseLenovo();
}
};
测试代码:
#include <iostream>
#include "AbstractFactoryModel.h"
int main()
{
using namespace std;
// 抽象工厂模式
Factory * p = new FactoryMicro();
KeyBoard * pKeyBoard = p->createKeyBoard();
Mouse * pMouse = p->createMouse();
pKeyBoard->show();
pMouse->show();
delete pMouse;
delete pKeyBoard;
delete p;
p = new FactoryLenovo();
pKeyBoard = p->createKeyBoard();
pMouse = p->createMouse();
pKeyBoard->show();
pMouse->show();
delete pMouse;
delete pKeyBoard;
delete p;
getchar();
return 0;
}
2、抽象工厂模式的优点与缺点
最大的好处便是易于交换产品系列,由于具体工厂类,例如IFactory factory=new AccessFactory()在一个应用中只需要在初始化的时候出现一次,这使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。
它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。
缺点是增加产品时,需要增加新的产品类,不同工厂对该产品的处理类;修改抽象工厂,具体工厂。即改动很大。
3、用简单工厂来改进抽象工厂
用DataAccess来决定使用哪个数据库来访问表项,即决定使用哪个工厂来生产产品。这样客户端只需要决定使用哪个产品而不需要关心该产品在哪个工厂生产,达到了产品和工厂解耦的目的。
缺点是当需要增加工厂时,需要在DataAccess中每个产品中添加该工厂。
4、用反射+抽象工厂的数据访问程序
利用反射技术来克服抽象工厂模式的不足。
格式:
Assembly.Load(“程序集名称”).CreateInstance(“命名空间.类名称”);
用字符串变量代替原来switch-case中写死的字符串,比较灵活。
所有在用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。
5、用反射+配置文件实现数据库访问程序
十六、无尽加班何时休——状态模式
“方法很长,而且有很多的判断分支,这也就意味着它的责任郭大路。无论是任何状态,都需要通过它来改变,这实际上是很糟糕的,违背了开放-封闭原则”。
面向对象设计其实就是希望做到代码的责任分解。
可以把这些分支放在单独的类中,这样分支的改变只会影响本类,不会影响其他类。
1、状态模式
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。 当然,如果这个状态判断很简单,那就没必要用"状态模式"了。
在以上UML图中,Context中有一个状态的引用(C++中是指针),当该状态发生变化时,Context中的状态不同,响应的操作会调用不同对象的Handle方法。而在ConcreteState中会判断当前状态是否与自身状态相同(每一个状态一个类),如果相同则调用自身的Handle方法,如果不同则修改Context中State的引用,并调用相关的Handle方法。所以通用的状态模式的UML图是有问题的,其实State类是依赖于Context类的。也就是说其UML图应该是下图:
State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为。
class State
{
public:
virtual void Handle(Context* context)=0;
}
ConcreteState类,具体状态,每一个子类实现一个与Context的一个状态相关的行为。
class ConcreteStateA:public State
{
public:
void Handle(Context* context)//设置ConcreteStateA的下一状态时ConcreteStateB
{
cout<<"ConcreteStateA"<<endl;
context->setState(new ConcreteStateB());
}
}
class ConcreteStateB:public State
{
public:
void Handle(Context* context)//设置ConcreteStateB的下一状态时ConcreteStateA
{
cout<<"ConcreteStateB"<<endl;
context->setState(new ConcreteStateA());
}
}
Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态。
class Context
{
public:
Context(State* state)//定义Context的初始状态
{
m_state=state;
}
State* getState()//获取当前状态
{
return m_state;
}
void setState(State* state)//设置当前状态
{
if(nullptr!=m_state) delete m_state;
m_state=state;
}
void Requst()//对请求做处理,并设置下一状态
{
m_state->Handle(this);
}
private:
State* m_state;
}
测试代码:
int main()
{
Context* c=new Context(ConcreteStateA); //设置Context的初始状态为ConcreteStateA
//不同的请求,同时更改状态
c->Request();//输出为ConcreteStateA
c->Request();//输出为ConcreteStateB
c->Request();//输出为ConcreteStateA
c->Request();//输出为ConcreteStateB
getchar();
return 0;
}
2、状态模式的好处与用处
状态模式的好处是将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。
就是将特定的状态相关的行为都放入一个对象中,由于所有与状态相关的代码都存在于某个ConcreteState中,所以通过定义新的子类可以很容易地增加新的状态和转换。
这样做地目的就是为了消除庞大的条件分支语句,大的分支判断会使得它们难以修改和扩展。
状态模式通过把各种状态转移逻辑分到State的子类之间,来减少相互间的依赖。
在以下情况可以考虑使用状态模式:
当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。
另外如果业务需求某项业务有多个状态,通常都是一些美剧常量,状态的变化都是依靠大量的分支判断语句来实现,此时应该考虑将每一种业务状态定义为一个State的子类。这样这些对象就可以不依赖于其他对象而独立变化了,如果有一天客户改变需求,增加或减少业务状态流程,都比较容易。
还有一个问题,就是状态模式的UML类图和策略模式的UML类图非常的相似。那么我们就很有必要区分一下这两个模式。其实也很好区分,这两者的主要区别并不是类结构图上的不同,更多的则是类的实现的不同。策略模式中是在不同情况下调用不同的算法。而状态模式侧重于根据自身状态的不同做出不同的响应,而且状态模式中最重要的一点是状态可以被状态类更改。比如当前状态做完相应的工作后可能会把状态修改为另一个状态。而这个区别也是UML图中的唯一一个不同所造成的区别。因为状态模式中的State是依赖于Context的。
一个人一天的工作状态随着工作时间的变化而变化,在不同的状态下,其行为不同。
十七、在NBA我需要翻译——适配器模式
1、适配器模式
适配器模式(Adapter),将一个类的接口转换成客户希望的另外一个接口。 Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适配器模式用于解决:简单地说,就是需要的东西就在面前,但却不能使用,而短时间又无法改造它,于是我们就像办法适配它。
如电源适配器,只要是点,不管多少伏,都能把电源变成需要的电压。
如不同语言间的翻译也是适配器。
系统的数据和行为都正确,但接口不符时,我们应该考虑用适配器,目的是使控制范围之内的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况。
包括类适配器模式和对象适配器模式两种类型。前者通过多重继承对一个接口于另一个接口进行匹配。这里主要讲的是对象适配器。
Target(这是客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口)代码如下:
class Target
{
public:
virtual void Request()
{
cout<<"普通请求!"<<endl;
}
}
Adaptee(需要适配的类),代码如下:
class Adaptee
{
public:
void SpecificRequest()
{
cout<<"特殊请求!"<<endl;
}
}
Adapter(通过在内部包装一个Adaptee对象,把源接口转换成目标接口)代码如下:
class Adapter:public Target
{
public:
Adapter()
{
m_adaptee=new Adaptee();
}
~Adapter()
{
if(nullptr!=m_adaptee)
delete m_adaptee;
}
void Request()
{
m_adaptee->SpecificRequest();//这样就可有把表面上调用Request()方法变成实际调用SpecificRequest()
}
private:
Adaptee* m_adaptee; //建立一个私有的Adaptee对象
}
测试代码如下:
int main()
{
Target* t=new Adapter();
t->Request();//对客户端来说,调用的就是Target的Request()
getchar();
return 0;
]
2、何时使用适配器模式
在想使用一个已经存在的类,但如果它的接口,也就是它的方法和你的要求不相同时,就应该考虑用适配器模式。
两个类所做的事情相同或相似,但具有不同的接口时要使用它。而且由于类都共享同一个接口,使得客户代码可以统一调用同一个接口就行了。这样就可以更简单、更直接、更紧凑。
如不同的软件维护人员使得软件的功能类似而接口不同时。
要在双方都不太容易修改的时候再使用适配器模式适配,而不是一有不同时就使用它。
也有在设计之处需要考虑用适配器模式的时候,如某公司设计一系统是考虑使用第三方开发组件,而这个组件的接口与我们自己的系统接口时不相同的,而我们也完全没有必要为了迎合它们而改动自己的接口,此时尽管是在开发的设计阶段,也是可以考虑用适配器模式来解决接口不同的问题。
十八、如果再回到从前——备忘录模式
1、备忘录模式
备忘录:在不破外封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
Originator(发起人):负责创建一个备忘录Memento,用以记录当前时刻它的内部状态,并可使用备忘录恢复内部状态。Originator可根据需要决定Memento存储Originator的哪些内部状态。
Memento(备忘录):负责存储Originator对象的内部状态,并可防止Originator以外的其他对象访问备忘录Memento。备忘录有两个接口,Caretaker只能看到备忘录的窄接口,它只能将备忘录传递给其他对象。Originator能够看到一个款接口,允许它访问返回到先前状态所需的所有数据。
Caretaker(管理者):负责保存好备忘录Memento,不能对备忘录的内容进行操作或检查。
当需要保存全部信息时,用clone的方式来实现Memento的状态保存可能是更好的办法,但是如果是这样的话,使得我们相当于对上层应用开放了Originator的全部(public)接口,这对于保存备份有时候是不适合的。
但当我们需要保存的并不是全部信息,而只是部分,那么就应该有一个独立的备忘录类Memento,它只拥有需要保存的信息的属性。
2、备忘录模式基本代码
发起人(Originator)类:
class Originator
{
public:
Memento* CreateMemento()//创建备忘录,将当前需要保存的信息导入并实例化出一个Memento对象
{
return (new Memento(m_state));
}
void RecoverMemento(Memento* memento)//恢复备忘录,将Memento导入并实例化出一个Memento对象
{
memento->m_state=m_state;
}
void Show()
{
cout<<"state="<<m_state<<endl;
}
string GetState()
{
return m_state;
}
void SetState(string state)
{
m_state=state;
}
private:
string m_state;
}
备忘录(Memento)类:
class Memento
{
public:
Memento(string state)//构造方法,将相关数据导入
{
m_state=state;
}
string GetState()
{
return m_state;
}
private:
string m_state;
}
管理者(Caretaker)类:用来管理备忘录
class Caretaker
{
public:
Caretaker():m_momento(nullptr)
{
}
~Caretaker()
{
if(nullptr!=m_momento)
delete m_momento;
}
void GetMomento()
{
return m_momento;
}
void SetMomento(Momento* m)
{
m_momento=m;
}
private:
Momento* m_momento;
}
测试代码:
int main()
{
Originator* o=new Originator();
o->SetState("test1"); //Originator的初始状态,状态属性为test1
o->Show();
Caretaker* c=new Caretaker();
c->m_momento=o->CreateMemento();
o->SetState("test2"); //Originator改变状态属性为test1
o->Show();
o->RecoverMemento(c->m_momento);//恢复原始初始状态
o->Show();
getchar()
return 0;
}
Memento模式比较适用于功能比较复杂,但需要维护或 记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分,Originator可以根据保存的Memento信息还原到前一状态。
如果在某个系统中使用命令模式时,需要实现命令的撤销功能,那么命令模式可以使用备忘录模式来存储可撤销操作的状态。
有时一些对象的内部信息必须保存在对象以外的地方,但是必须要由对象自己读取,这时,使用备忘录模式可以把复杂的对象内部信息对其他的对象屏蔽起来,从而可以恰当地保持封装的边界。
当角色的状态改变的适合,有可能这个状态无效,这时候就可以使用暂时存储起来的备忘录将状态复原。
备忘录模式的缺点是,当需要保存的状态数据很大多,在资源消耗上,备忘录对象会非常耗内存。故备忘录模式并不是用得越多越好。
十九、分公司=一部分——组合模式
1、组合模式
组合模式,将对象组成属性结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
Component为组合中的对象声明接口,在适当情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component的子部件。代码如下:
class Component
{
public:
Component(string name)
{
m_name=name;
}
virtual void Add(Component* c)=0;
virtual void Remove(Component* c)=0;//通常都是用Add和Remove方法来提供增加或移除树叶或树枝的功能
virtual void Disolay(int depth)=0;
protected:
string m_name;
}
Leaf在组合中表示叶节点对象,叶节点没有子节点
class Leaf:public Component
{
public:
Leaf(string name):m_name(name)
{
}
void Add(Component* c)
{
cout<<"cannot add to a leaf"<<endl;
}
void Remove(Component* c)
{
cout<<"cannot add to a leaf"<<endl;
}
void Display(int depth)
{
cout<<m_name<<depth<<endl;
}
}
Composite定义有枝结点行为,用来存储子部件,在Component接口中实现与子部件有关的操作,比如增加Add和删除Remove。
class Composite:public Component
{
public:
Composite(string name):m_name(name){
]
~Composite()
{
if(!m_vec.empty())
m_vec.clear();
]
void add(Component* c)
{
m_vec.push_back(c);
}
void add(Component* c)
{
m_vec.erase(c);
}
void Display(int depth)
{
for(auto it=m_vec.begin();it!=m_vec.end();it++)
{
it->display(depth);
}
}
private:
vector<shared_ptr<Component>> m-vec;//一个子对象集合用来存储其下属的枝节点和叶子节点
]
测试代码如下:
int main()
{
Composite* root=new Composite("总部");
root->add(new Leaf("总部财务部门"));
root->add(new Leaf("总部人力资源部门"));
Composite* com=new Composite("成都分部");
com->add(new Leaf("成都分布财务部门"));
com->add(new Leaf("成都分布人力资源部门"));
root->add(com);
root->Diaplay(1);//root是树根,有两个叶子节点和一个树枝节点,com又有两个叶子节点
getchar();
return 0;
}
2、透明方式与安全方式
树可能有无数的分枝,但反复用Composite就可以实现树状结构了。
Leaf类当中也有Add和Remove,树叶本不可以再长出分枝。这种方式叫透明方式,也就是说在Component中声明所有用来管理子对象的方法,其中包括Add、Removed等。这样实现Component接口的所有子类都具备了Add和Remove。这样做的好处就是叶子节点和枝节点对于外界没有区别,它们具备完全一致的行为接口。但问题也很明显,因为Leaf类本身不具备Add()、Remove()方法的功能,所以实现它是没有意义的。
如果不想在Leaf类当中用Add和Remove方法,那么就需要安全方式,也就是在Component接口中不去声明Add和Remove方法,那么子类Leaf也就不需要去实现它,而是在Composite声明中所有用来管理子类对象的方法,这样做就不会出现刚才提到的问题,不过由于不够透明,所以树叶和树枝类将不具有相同的接口,客户端的调用需要做相应的判断,带来了不便。
3、何时使用组合模式
需求中是体现部分与整体的层次结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就因该考虑使用组合模式了。
例如,自定义控件,也就是把一些基本的控件组合起来,通过编程写成一个定制控件,比如用两个文本框和一个按钮可以写一下自定义的登录控件。
4、组合模式的好处
组合模式这样就定义了包含人力资源部和财务部这些基本对象和分公司、办事处等组合对象的类层次结构。基本对象可以被组合成更负责的组合对象,而这个组合对象又可以被组合,这样不断地递归下去,客户代码中国,任何用到基本对象地地方都可以使用组合对象了。
用户是不用关心到底处理一个叶节点还是处理一个组合组件,也就用不着为定义组合而写一些选择判断语句了。
简单地说,就是组合模式让客户可以一致地使用组合结构和单个对象。
二十、想走?可以!先买票——迭代器模式
迭代器模式,提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
当你需要访问一个聚集对象,而且不管这些对象是什么都需要遍历的时候,你就应该考虑用迭代器模式。当需要对聚集有多种方式遍历时,可以考虑用迭代器模式。
为遍历不同的聚集结构提供如开始、下一个、是否结束、当前哪一项等统一的接口。
现在很多高级语言已经把这个模式做在语言当中了,如C#、JAVA、C++等,如 iterator。
Iterator类迭代器抽象类
template<typename T>
class Iterator
{
public:
virtual T First()=0; //开始对象
virtual T Next()=0; //下一个对象
virtual bool IsDone()=0; //判断是否到结尾
virtual T CurrentItem()=0; //当前对象等抽象方法
}
ggregate聚集抽象类:
class Aggregate
{
public:
virtual Iterator CreateIterator()=0; //创建迭代器
}
ConcreteAggregate具体聚集类,继承Aggregate
template<typename T>
class ConcreteAggregate:public Aggregate
{
public:
Iterator CreateIterator()
{
return new ConcreteAggregate();
}
int GetCount()
{
return m_items.size(); //返回聚集总个数
}
T GetItem(int index)//获得索引对应的值
{
return m_items[index];
}
void SetItem(int index ,T value)//设置指定索引值
{
m_items[index]=value;
}
private:
vector<T> m_items; //用于存放聚合对象
}
ConcreteIterator具体迭代器类,继承Iterator
template<typename T>
class ConcreteIterator:public Iterator
{
public:
ConcreteIterator(ConcreAggregate* aggregate)//初始化时将具体的聚集对象传入
{
m_aggregate=aggregate;
}
T First()//得到聚集的第一个对象
{
return m_aggregate[0];
}
T Next()//得到下一个聚集对象
{
T ret=NULL;
m_current++;
if(m_current<m_aggregate.GetCount())
{
ret=m_aggregate[m_current];
}
return ret;
}
bool IsDone()//判断当前是否遍历到结尾,到结尾返回true
{
return m_current>=m_aggregate->GetCount()?true:false;
}
T CurrentItem()//返回当前的聚集对象
{
return m_aggregate[m_current];
}
private:
ConcreteAggregate* m_aggregate;
int m_current;
}
测试代码:
int main()
{
ConcreteAggregate *a=new ConcreteAggregate();//公交车,即聚集对象
a[0]="1";
a[0]="2";
a[0]="3";
a[0]="4";
a[0]="5";
a[0]="6";//新上来的乘客,即对象数组
Iterator* i=new ConcreteIterator(a);//售票员出场,先看好了上车的哪些人,即声明了迭代器对象
auto item=i->First();//从第一个乘客开始
while(!i->IsDone())
{
cout<<i->CurrentItem()<<"请买票"<<endl;//对前面的乘客告知请买票
i->Next();//下一乘客
}
}
这里为什么要用具体的迭代器ConcreteIterator来实现抽象的Iterator呢?是因为当需要对聚集有多种方式遍历(即不一定从头到尾,也可以从尾到头)时,可以考虑用迭代器模式。
增加一种遍历方式,只需增加一个继承自Iterator的类,然后重写其中的方法即可。
迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不是暴露集合的内部结构,又可以让外部代码透明地访问集合内部的数据。
迭代器模式在访问数组、集合、列表等数据时,尤其是数据库操作时,是非常普遍的应用,但由于它太普遍了,所以各种高级语言都对它进行了封装,所以反而给人感觉此模式本身不太常用了。
二十一、有些类也需要计划生育——单例模式
1、单例模式
单列模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。
Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetInstance是一个静态方法,主要负责创建自己的唯一实例。
class Singleton
{
private:
Singleton()//构造方法让其private,这就堵死了外界利用new创建此类实例的可能
{
}
public:
static Singleton GetInstance()//此方法是获得本类实例的唯一全局访问点
{
if(nullptr==m_instance)//若实例不存在,则new一个新的实例,否则返回已有的实例
{
m_instance=new Singleton();
}
return m_instance;
}
private:
static Singleton* m_instance;
}
Singleton* Singleton::m_instance=nullptr;
测试代码如下:
int main()
{
Singleton s1=new Singleton::GetInstance();
Singleton s2=new Singleton::GetInstance();
if(s1==s2)
{
cout<<"两个对象是相同实例"<<endl
}
getchar();
return 0;
}
单例模式因为Singleton类封装它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问。
2、多线程时的单例
多线程的程序中,多个线程同时,注意是同时访问Singleton类,调用GetInstance()方法,会有可能造成创建多个实例的。
为解决上述问题,可以给进程一把锁来处理。lock是确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程视图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
class Singleton
{
private:
Singleton()
{
}
static Singleton GetInstance()
{
m_mutex.lock();
if(nullptr==m_instance)
{
m_instance=new Singleton();
}
m_mutex.unlock();
return m_instance;
}
private:
static Singleton* m_instance;
static mutex m_mutex;
}
Singleton* Singleton::m_instance=nullptr;
mutex Singleton::m_mutex;
这段代码使得对象实例由最先进入的那个线程创建,以后在进入时不会再去创建对象实例了。由于有了lock,就保证了多线程环境下的同时访问也不会造成多个实例的生成。
但这会存在一个问题,就是每次调用GetInstance方法时都需要lock。下面介绍的双重锁将对此做改良。
3、双重锁定
双重锁定(Double-Check Locking):不用让线程每次都加锁,而只是在实例化未被创建的时候再加锁。同时也能保证多线程的安全。
class Singleton
{
private:
Singleton()
{
}
static Singleton GetInstance()
{
if(nullptr==m_instance)
{
m_mutex.lock();
if(nullptr==m_instance)
m_instance=new Singleton();
m_mutex.unlock();
}
return m_instance;
}
private:
static Singleton* m_instance;
static mutex m_mutex;
}
Singleton* Singleton::m_instance=nullptr;
mutex Singleton::m_mutex;
4、静态初始化
C#与公共语言运行库也提供了一种“静态初始化“方法,这种方法不需要开发人员显示地编写线程安全代码,即可解决多线程环境下它是不安全的问题。
做法如下:
将Singleton定义为阻止派生,然后为Singleton定义私有的 static Singleton* m_instance=new Singleton();
也可以在客户端使用静态初始化:
在使用线程前先调用一个GetInstance方法,则使用线程时已经存在Singleton的实例,故不用加锁也不会再次进入GetInstance中生成Singleton的实例。
**饿汉式,,即静态初始化的方式,它是类一加载就实例化的对象,所以要提前占用系统资源,然而懒汉式,又会面临着多线程访问的安全性问题,需要做双重锁定这样的处理才可以保证安全。 **所以到底使用哪一种方式,取决于实际的需求。
二十二、手机软件何时统一——桥接模式
1、合成/聚合复用原则
对象的继承关系是在编译时就定义好了,所以无法在运行时改变从父类继承的实现。子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。
合成/聚合原则:尽量使用合成/聚合,尽量不要使用类继承。
合成(或组合)和聚合都是关联的特殊种类。聚合表示一种弱的拥有关系,体现的是A对象可以包含B对象,但是B对象不是A对象的一部分;合成则是一种强拥有关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。
合成/聚合复用原则的好处是,优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。
继承是一种强耦合的结构。父类变,子类就必须要变。所以在使用继承时,一定要在是”is-a"的关系时再考虑使用,而不是任何时候都去使用。
2、桥接模式
桥接模式,将抽象部分和它的实现部分分离,使它们可以独立地变化。
这里的抽象与它的实现分离,并不是说,让抽象类与其派生类分离,因为这没有任何意义。实现指的是抽象类和它的派生类用来实现自己的对象。如让”手机“就可以按照品牌来分类,也可以按照功能来分类。
由于实现的方式有多种,桥接模式的核心意图就是把这些实现独立出来,让它们各自地变化。这就使得每种实现地变化不会影响其他实现,从而达到应对变化地目的。
3、桥接模式基本代码
Implememtor类:
class Implememtor
{
virtual void Operation()=0;
}
和ConcreteImplememttorB等派生类:
class ConcreteImplememttorA:public Implememtor
{
public:
void Operation()
{
cout<<"具体实现A的方法执行"<<endl;
}
}
class ConcreteImplememttorA:public Implememtor
{
public:
void Operation()
{
cout<<"具体实现B的方法执行"<<endl;
}
}
Abstraction类
class Abstraction
{
protected:
Implememtor m_implememtor;
virtual void SetImplememtor(Implememtor* implememtor)
{
m_implememtor=implememtor;
}
virtual void Operation()
{
m_implememtor->Operation();
}
}
RefinedAbstraction类
class RefinedAbstraction
{
public:
void Operation()
{
m_implememtor->Operation();
}
}
测试代码如下:
int main()
{
Implememtor* i=new ConcreteImplememttorA();
Abstraction* a=new RefinedAbstraction();
a->SetImplememtor(i);
i->Operation();
getchar();
return 0;
}
**实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。**也就是说,在我们发现需要多角度去分类实现对象,而值用继承会造成大量的类增加,不能满足开放-封闭原则时,就应该考虑用桥接模式了。
只要正真深入理解了设计原则,很多设计模式其实就是原则的应用而已。
二十三、烤羊肉串引来的思考——命令模式
1、命令模式
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
Command类,用来声明执行操作的接口
class Command
{
public:
Command(Receiver* r):m_r(r)
{
}
virtual void Execute()=0;
protected:
Receiver* m_r;
}
ConcreteCommand类,将一个接收者对象绑定于一个动辄,调用接收者相应的操作,以实现Execute。
class ConcreteCommand:public Command
{
public:
ConcreteCommand(Receiver* r):m_r(r){
}
void Execute()
{
r->Action();
}
}
Invoker类,要求该命令执行这个请求:
class Invoker
{
public:
void SetCommand(Command* c)
{
m_c=c;
}
void ExecuteCommand()
{
m_c->Execute();
}
private:
Command* m_c;
}
Receiver类,知道如何实施与执行一个与请求先关的操作,任何类都可能作为一个接收者:
class Receiver
{
public:
void Action()
{
cout<<"执行请求!"<<endl;
}
}
测试代码如下:
int main()
{
Receiver* r=new Receiver();
Command* c=new ConcreteCommand(r);
Invoker* i=new Invoker();
i->SetCommand(c);
i->ExecuteCommand();
delete r;
delete c;
delete i;
getchar();
return 0;
}
2、命令模式作用
命令模式的优点如下:
第一,它能较容易地设计一个命令队列;
第二,在需要的情况下,可以较容易地将命令记入日志;
第三,允许接受请求地以访决定是否要否决请求;
第四,可以容易地实现对请求地撤销和重做;
第五,由于加紧新的具体命令类不影响其他的类,因此增加新的具体命令类很容易;
第六,命令模式把请求一个操作的对象与指导怎么执行一个操作的对象分割开。
敏捷开发原则高告诉我们,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构实现这个模式并不困难,只有在真正需要如撤销/恢复操作等功能时,把原来的代码重构为命令模式才有意义。
二十四、加薪非要老总批?——职责链模式
1、职责链模式
职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
这里发出请求的客户端并不知道这当中的哪一个对象最终处理这个请求,这样系统的更改可以在不影响客户端的情况下动态地重新组织和分配责任。
Handler类,定义一个处理请示的接口:
class Handler
{
public:
virtual void SetSuccessor(Handler* successor)
{
m_successor=successor;
}
virtual void HandleRequest(int request)=0;//处理请求的抽象方法
protected:
Handler* m_successor;
}
ConcreteHandler类,具体处理者类,处理它所负责的请求,可访问它的后继者,如果可处理该请求,就处理之,否则就将该请求转发给它的后继者。
class ConcreteHandler1:public Handler
{
public:
void HandleRequest(int request)
{
if(request>=0&&request<10)//10到20,处理此请求
{
cout<<"ConcreteHandler1 process the request"<<endl;
}
else if(nullptr!=m_successor)
{
m_successor->HandleRequest(request);//转移到下一位
}
else
cout<<"Cannot process the request"<<endl;
}
}
class ConcreteHandler2:public Handler
{
public:
void HandleRequest(int request)
{
if(request>=10&&request<20)
{
cout<<"ConcreteHandler2 process the request"<<endl;
}
else if(nullptr!=m_successor)
{
m_successor->HandleRequest(request);
}
else
cout<<"Cannot process the request"<<endl;
}
}
class ConcreteHandler3:public Handler
{
public:
void HandleRequest(int request)
{
if(request>=20&&request<30)
{
cout<<"ConcreteHandler3 process the request"<<endl;
}
else if(nullptr!=m_successor)
{
m_successor->HandleRequest(request);
}
else
cout<<"Cannot process the request"<<endl;
}
}
测试代码如下:
int main()
{
Handler* h1=new ConcreteHandler1();
Handler* h2=new ConcreteHandler2();
Handler* h3=new ConcreteHandler3();
h1->SetSuccessor(h2);//设置职责链上家与下家
h2->SetSuccessor(h3);
h1->HandleRequest(5);
h1->HandleRequest(15);
h1->HandleRequest(25);
h1->HandleRequest(60);
delete h1;
delete h2;
delete h3;
getchar();
return 0;
}
2、职责链的好处
这当中最关键的是当客户提交一个请求时,请求是沿链传递直至有一个ConcreteHandler对象负责处理它。
这样做的好处是请求者不用管哪个对象来处理,反正该请求会被处理。
这就使得接收者和发送者都没有对方的明确信息,且链中的对象自己也并不知道链的结构。结果是职责链可简化为对象的相互连接,它们仅需保持一个指向其后继的引用,而不需保持它所有的候接收者的引用。这也就大大降低了耦合度。
由于是在客户端来定义链的结构,也就是说,可以随时地增加或修改处理一个请求地结构。增强了给对象指派职责地灵活性。
不过,一个请求极有可能到了链地末端都得不到处理,或者因为没有正确配置而得不到处理。为解决这个问题,可以在最后的处理者处理所有的请求。
二十五、世界需要和平——中介者模式
尽管将一个系统分割成许多对象通常可以增加其可复用性,但是对象间相互连接的激增又会降低其可复用性了。因为大量的连接使得一个对象不可能在没有其他对象的支持下工作,系统表现为一个不可分割的整体,所以,对系统的行为进行任何较大的改动就十分困难了。
1、中介者模式
中介者模式,用一个中介对象来封装一系列的对象的交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间地交互。
Colleague叫做抽象同事类,而ConreteColleague是具体同事类,每个具体同时只知道自己地行为,而不了解其他同时类的行为地情况,但它们却都认识中介者对象,Mediator是抽象中介者,定义了同事对象到中介者对象的接口,ConreteMediator是具体中介者对象,实现抽象类的方法,它需要知道所有具体同事类,并从具体同事接收消息,向具体同事对象发出命令。
Mediator类 抽象中介者:
class Mediator
{
public:
virtual void Send(string message,Colleague* l)=0;//定义一个抽象的发送消息方法,得到同事对象和发送消息
}
ConreteMediator类 具体中介者:
class ConreteMediator:public Mediator
{
public:
void SetColleague1(Colleague* l)
{
m_colleague1=l;
}
void SetColleague2(Colleague* l)
{
m_colleague2=l;
}
void Send(string message,Colleague* l)
{
if(l==m_colleague1)
{
m_colleague2->Notify(message);
}
if(l==m_colleague2)
{
m_colleague1->Notify(message);
}
}
private:
ConreteColleague1* m_colleague1;
ConreteColleague2* m_colleague2;
}
Colleague类,抽象同事类:
class Colleague
{
public:
Colleague(Mediator* m)
{
m_mediator=m;
}
protected:
Mediator* m_mediator;
}
ConcreteColleague1和ConcreteColleague2等各种同事对象
class ConcreteColleague1:public Colleague
{
public:
ConcreteColleague1(Mediator* m):m_mediator(m)
{
}
void Send(string message)
{
m_mediator->Send(message,this);//发送消息时通常时是中介者发送出去的
}
void Notify(string message)
{
cout<<"同事1得到消息:"<<message<<endl;
}
}
class ConcreteColleague2:public Colleague
{
public:
ConcreteColleague2(Mediator* m):m_mediator(m)
{
}
void Send(string message)
{
m_mediator->Send(message,this);
}
void Notify(string message)
{
cout<<"同事2得到消息:"<<message<<endl;
}
}
测试代码如下:
int main()
{
Mediator* m=new ConreteMediator();
Colleague* c1=new ConcreteColleague1(m);
Colleague* c2=new ConcreteColleague1(m);//让两个具体同事类认识中介者对象
m->SetColleague(c1);
m->SetColleague(c2);//让中介者认识各个具体同事类
c1->Send("ConcreteColleague1");
c2->Send("ConcreteColleague2");//具体同事类对象的发送消息都是通过中介者转发
getchar();
return 0;
}
2、中介者模式优缺点
中介者模式很容易在系统中应用,也很容易在系统中误用。当系统出现了"多对多"交互复杂的对象群时,不要基于使用中介者模式,而要先反思你的系统在设计上是不是合理。
其优点如下:
Mediator的出现减少了各个Colleague的耦合,使得可以独立地改变和复用各个Colleague类和Mediator;
由于把对象如何写作进行了抽象,将中介作为一个独立地概念并将其封装在一个对象中,这样关注地对象就从对象各自本身地行为转移到它们之间地交互上来,也就是站在一个更宏观地角度去看待系统;
缺点如下:
由于ConcreteMediator控制了集中化,于是就把交互复杂性变为了中介者地复杂性,这就使得中介者会变得比任何一个ConreteColleague都复杂。
中介者模式地优点来自集中控制,其缺点也是它。
中介者模式一般应用于一组对象以定义良好但是复杂的方式进行通信的场合,以及想定制一个分布在多个类中的行为,而又不想生成太多的子类的场合。
二十六、项目多也别傻做——享元模式
1、享元模式
享元模式,运用共享技术有效地支持大量细粒度的对象。面向对象技术可以很好地解决一些灵活性或扩展性问题,但在很多情况下需要在系统中增加类和对象的个数。当对象数量太多时,将导致运行代价过高,带来性能下降等问题。享元模式正式为了解决这依赖问题而诞生的。享元模式通过共享技术实现相同或相似对象的重用,示意图如下(我们可以公用一个Hello world对象,其中字符串“Hello !world”为内部状态,可共享:字体颜色为外部状态,不可共享,由客户端设定)。
在享元模式中可以共享的相同内容成为内部状态,而那些需要外部环境来设置的不能共享的内容称为外部状态,**其中外部状态和内部状态是相互独立的,外部状态的变化不会引起内部状态的变化。**由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。也就是说,享元模式的本质是分离与共享:分离变与不变,并且共享不变。把一个独享的状态分为内部状态和外部状态,内部状态即是不变的,外部状态时变化的;然后通过共享不变的部分,达到减少对象数量并节约内存的目的。
在享元模式中通常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(用于存储具有相同内部状态的享元对象)。在享元模式中,共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也成为细粒度对象。比如在C++中的常量字符串。python中的小整数,应该都是这种模式。其UML图如下:
class Flyweight
{
public:
virtual void Operation(int n) = 0;
};
class ConcreteFlyweight_0 : public Flyweight
{
public:
void Operation(int n)
{
std::cout << "ConcreteFlyweight_0:" << n << std::endl;
}
};
class ConcreteFlyweight_1 : public Flyweight
{
public:
void Operation(int n)
{
std::cout << "ConcreteFlyweight_1:" << n << std::endl;
}
};
class ConcreteFlyweight_2 : public Flyweight
{
public:
void Operation(int n)
{
std::cout << "ConcreteFlyweight_2:" << n << std::endl;
}
};
class UnsharedConcreteFlyweight : public Flyweight
{
public:
void Operation(int n)
{
std::cout << "UnsharedConcreteFlyweight:" << n << std::endl;
}
};
// 享元工厂
class FlyweightFactory
{
private:
std::map<int, Flyweight *> m_map;
public:
FlyweightFactory()
{
m_map.insert(std::pair<int, Flyweight *>(0, new ConcreteFlyweight_0()));
m_map.insert(std::pair<int, Flyweight *>(1, new ConcreteFlyweight_1()));
m_map.insert(std::pair<int, Flyweight *>(2, new ConcreteFlyweight_2()));
}
~FlyweightFactory()
{
for (auto it = m_map.begin(); it != m_map.end(); it++)
{
delete it->second;
}
m_map.clear();
}
Flyweight * getFlyweight(int n)
{
return m_map[n];
}
};
测试代码如下:
int main()
{
using namespace std;
// 享元模式
int externState = 0;
FlyweightFactory * p = new FlyweightFactory();
Flyweight * f0 = p->getFlyweight(0);
f0->Operation(externState++);
Flyweight * f1 = p->getFlyweight(1);
f1->Operation(externState++);
Flyweight * f2 = p->getFlyweight(2);
f2->Operation(externState++);
Flyweight * f3 = p->getFlyweight(0);
f3->Operation(externState++);
delete p;
getchar();
return 0;
}
测试结果如下图:
2、享元模式的应用
如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;
还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑享元模式。
因为使用了享元模式,所以有了共享对象,实例总数就大大减少了,如果共享的对象越多,存储节约也就越多,节约量随着共享状态的增多而增大。
如下:
string str1="test";
string str2="test";
str1和str2是相同的实例。第一次创建了字符串对象str1,第二次创建一个相同的字符串str2时只是把它的引用指向"test",这样就实现了“test“在内存中的共享。
缺点:
使用享元模式需要维护一个记录了系统已有的所有享元的列表,而这本身需要耗费资源,另外享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序队的逻辑复杂化。因此,应当在有足够多的对象实例可供共享时才值得使用享元模式。
二十七、其实你不懂老板的心——解释器模式
1、解释器模式
解释器模式,给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
解释器模式需要解决的是,如果一个特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
与其为每一个特定需求都写一个算法函数,不如使用一种通用的搜索算法来解释执行一个正则表达式,该正则表达式定义了待匹配字符串的集合。而所谓解释器模式,正则表达式就是它的一种应用,解释器为正则表达式定义了一个文法,如何表示一个特定的正则表达式,以及如何解释这个表达式。
在解释器模式中每一种表达式对应一种表达式解释类,比如TerminalExpression和NonterminalExpression。Context类是表达式内容,在客户端中决定是那种类型的表达式,从而创建不同的表达式独享来解释该语句。
Context,包含解释器之外的一些全局信息:
class Context
{
private:
std::string m_strInput;
std::string m_strOutput;
public:
void setExpression(std::string str)
{
m_strInput = str;
}
};
class Expression
{
public:
virtual void Interpret(Context * context) = 0;
};
class TerminalExpression : public Expression
{
public:
void Interpret(Context * context)
{
std::cout << "TerminalExpression!" << std::endl;
}
};
class NonterminalExpression : public Expression
{
public:
void Interpret(Context * context)
{
std::cout << "NonterminalExpression!" << std::endl;
}
};
int main()
{
// 解释器模式
Context * pContext = new Context();
pContext->setExpression("Expression......");
Expression * pNon = new NonterminalExpression();
Expression * p = new TerminalExpression();
// 根据Expression中的内容判断采用那种表达式解析
pNon->Interpret(pContext);
p->Interpret(pContext);
delete p;
delete pNon;
delete pContext;
getchar();
return 0;
}
2、解释器模式的好处
通常当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。
只要是可以用语言来描述的,都可以应用解释器模式,如正则表达式、浏览器、机器人命令等。
好处:
**用了解释器模式,就意味着可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,你可以使用继承来改变或扩展该文法。**也比较容易地实现文法,因为定义抽象语法树中各个节点地类地实现大体类似,这些都易于直接编写。
不足:
解释器模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法很可能难以管理和维护。建议当文法非常复杂时,使用其他地技术如语法分许程序或编译器生成器来处理。
二十八、男人和女人——访问者模式
1、访问者模式
访问者模式,表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
访问者模式适用于数据结构相对稳定的系统。它把数据结构和作用与结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。
访问者模式的目的是要把处理从数据结构分离出来。很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,因为访问者模式使得算法操作的增加变得容易。反之,如果这样的系统的数据结构对象易于变化,经常要有新的数据对象增加进来,就不适合使用访问者模式。
通常ConcreteVisitor可以单独开发,不必跟ConreteElementA或ConreteElementB写在一起。正因为这样,ConcreteVisitor能提高ConreteElement之间的独立性,如果把一个处理动作设计成ConreteElementA和ConreteElementB的方法,每次想新增”处理“以扩充功能时就得去修改ConreteElementA和ConreteElementB了。
访问者模式的缺点其实也就是使得增加新的数据结构变得困难了。
2、访问者模式基本代码
class Element
{
protected:
std::string m_strName;
public:
Element(std::string str)
{
m_strName = str;
}
std::string getName()
{
return m_strName;
}
virtual void Accept(Visitor * visitor) = 0;
};
class ConcreteElementA : public Element
{
public:
ConcreteElementA(std::string str) : Element(str) {
}
void Accept(Visitor * visitor);
};
class ConcreteElementB : public Element
{
public:
ConcreteElementB(std::string str) : Element(str) {
}
void Accept(Visitor * visitor);
};
// 访问者
class Visitor
{
public:
virtual void VisitConcreteElementA(ConcreteElementA * p) = 0;
virtual void VisitConcreteElementB(ConcreteElementB * p) = 0;
};
class ConcreteVisitorA : public Visitor
{
public:
void VisitConcreteElementA(ConcreteElementA * p)
{
std::cout << "ConcreteVisitorA 访问了" << p->getName() << std::endl;
}
void VisitConcreteElementB(ConcreteElementB * p)
{
std::cout << "ConcreteVisitorA 访问了" << p->getName() << std::endl;
}
};
class ConcreteVisitorB : public Visitor
{
public:
void VisitConcreteElementA(ConcreteElementA * p)
{
std::cout << "ConcreteVisitorB 访问了" << p->getName() << std::endl;
}
void VisitConcreteElementB(ConcreteElementB * p)
{
std::cout << "ConcreteVisitorB 访问了" << p->getName() << std::endl;
}
};
void ConcreteElementA::Accept(Visitor * visitor)
{
visitor->VisitConcreteElementA(this);
}
void ConcreteElementB::Accept(Visitor * visitor)
{
visitor->VisitConcreteElementB(this);//二次分派。
}
// 聚集类
class ObjectStructure
{
private:
std::vector<Element *> m_vec;
public:
~ObjectStructure()
{
for (auto it = m_vec.begin(); it != m_vec.end(); it++)
{
delete *it;
}
m_vec.clear();
}
void add(Element * p)
{
m_vec.push_back(p);
}
void accept(Visitor * visitor)
{
for (auto it = m_vec.cbegin(); it != m_vec.cend(); it++)
{
(*it)->Accept(visitor);
}
}
};
int main()
{
using namespace std;
// 访问者模式
ObjectStructure * p = new ObjectStructure();
p->add(new ConcreteElementA("A"));
p->add(new ConcreteElementB("B"));
ConcreteVisitorA * pVisitorA = new ConcreteVisitorA();
ConcreteVisitorB * pVisitorB = new ConcreteVisitorB();
p->accept(pVisitorA);
p->accept(pVisitorB);
delete pVisitorA;
delete pVisitorB;
delete p;
getchar();
return 0;
}
二十九、设计模式总结
由于面向对象设计的复杂性,所以我们希望能做出应对变化,提高复用性的设计方案,而设计模式就能帮助我们做到这样的结果。通过复用已经公认的设计,我们能够在解决问题时避免前任所犯的种种错误,可以从学习他人的经验中获益,用不着为那些总是会重复出现的问题再次设计解决方案。
开放封闭原则;
依赖倒转原则;
可复用、可维护、可扩展、灵活性好。