文章目录
今天讲解设计模式中的行为型模式中的访问者模式、中介者模式、解释器模式
访问者模式
访问者模式(Visitor Pattern)是一种行为设计模式,其目的是将数据结构与数据操作分离,使得在不修改已有程序代码的情况下,可以添加新的操作。这种模式通过定义一个访问者类,来改变一个元素类的执行算法。访问者模式使得你能够在不改变元素类的前提下,定义作用于这些元素的新操作
操作频繁变化:当系统的数据结构相对稳定,但操作经常变化时,使用访问者模式可以使这些操作易于修改和扩展。
需要对复合对象执行多种不相关的操作:当需要对一组对象结构执行很多不相关的操作时,而你又希望避免这些操作"污染"这些对象的类。
区分多种类型的元素:当一个对象结构包含多种类型的元素,并且你希望对这些元素执行一些依赖于其具体类型的操作时。
cpp
//收费人员访问者子类
class Visitor_Salesman : public Visitor
{
public:
virtual void Visit_elm(Food* pelem)
{
float tmpprice = pelem->GetPrice();
cout << "售货员累计商品"" << pelem->GetName() << ""的价格:" << tmpprice << endl;
m_totalcost += tmpprice;
}
//返回总费用
float getTotalCost()
{
return m_totalcost;
}
private:
float m_totalcost = 0.0f; //总费用
};
//顾客访问者子类
class Visitor_Customer : public Visitor
{
public:
virtual void Visit_elm(Food* pelem)
{
cout << "顾客将商品"" << pelem->GetName() << ""放入收纳袋" << endl;
}
};
int main()
{
cout<<"Visitor Test!"<<endl;
Visitor_Salesman visitor1;
Bread bread1;
Milk milk1;
//各个元素子类调用Accept接受访问者的访问
bread1.Accept(&visitor1);
milk1.Accept(&visitor1);
cout << "售货员收取了我:" << visitor1.getTotalCost() << " 元" << endl;
Visitor_Customer visitor2;
bread1.Accept(&visitor2);
milk1.Accept(&visitor2);
cout<<"finish!"<<endl;
return 0;
}
优点
增加新操作容易:可以通过添加新的访问者类来增加新的操作,这符合开闭原则,允许系统易于扩展和维护。
聚合操作:访问者模式使得可以将相关的操作集中到一个访问者中,而不是分散在各个元素类中,这有助于组织和集中管理相关操作,减少系统的复杂性。
累积状态:访问者可以在访问元素时累积状态,而不需要将这些状态存储在元素之中,这有助于避免元素类变得臃肿,同时可以轻松地添加新的累积逻辑。
缺点
破坏封装:访问者模式通常需要元素暴露一些原本应为私有的实现细节给访问者,这违反了面向对象的封装原则。
难以维护:如果经常添加新的元素类,每次添加都需要修改所有访问者类以添加新的访问操作,这可能导致代码难以维护。
复杂度增加:使用访问者模式会增加系统的复杂度,学习和理解系统的难度增加,特别是在具有大量元素和访问者的系统中。
中介者模式
中介者模式(Mediator Pattern)就是定义一个中介对象来封装系列对象之间的交互。终结者使各个对象不需要显示的相互调用 ,从而使其耦合性松散,而且可以独立的改变他们之间的交互。
中介者模式适用于需要协调多个对象之间交互的场景,例如:
航空管制系统:协调飞机、航空公司、机场等之间的通信和协作。
交易系统:在金融领域协调银行、金融机构、客户等之间的资金转移。
买房中介:作为买卖双方之间的桥梁和调解者,确保交易的顺利进行。
cpp
enum PersonType
{
COOK,
DEVELOPER,
TESTER,
INSPECTOR,
WORKER
};
string getPersonTypeInfo(PersonType t)
{
string s="WORKER";
switch(t)
{
case COOK:
s="COOK";
break;
case DEVELOPER:
s="DEVELOPER";
break;
case TESTER:
s="TESTER";
break;
case INSPECTOR:
s="INSPECTOR";
break;
}
return s;
}
class ChatRoom {
public:
virtual void broadcast(string from, string msg) = 0;
virtual void message(string from, string to, string msg) = 0;
};
class Person {
public:
Person(int id,string n) : m_id(id),m_name(n) {}
string name(){return m_name;};
void say(string msg) const {
if(m_room==nullptr)return;
cout << m_name << " say:" << msg << endl;
m_room->broadcast(m_name, msg);
}
void say(string to, string msg) const {
if(m_room==nullptr)return;
cout << m_name << " say to " <<to<<":"<< msg << endl;
m_room->message(m_name, to, msg);
}
void setRoom(ChatRoom* room){m_room=room;};
virtual void receive(string from, string msg){};
virtual void showInfo(){cout << "ID:"<<m_id <<" " << m_name << endl;};
virtual PersonType getPersonType(){return WORKER;};
virtual ~Person(){};
protected:
string m_name;
int m_id;
ChatRoom* m_room{ nullptr };
vector<string> m_chat_log;
};
class ChatUser :public Person {
public:
ChatUser(int id,string n) : Person(id,n) {}
void receive(string from, string msg) override {
string s{ " from " + from + ": \"" + msg + "\"" };
cout << "ID:"<<m_id <<" " << m_name << " receive message" << s << endl;
m_chat_log.emplace_back(s);
}
};
class GoogleChat: public ChatRoom
{
vector<Person*> m_people;
public:
void broadcast(string from, string msg) override {
for (auto p : m_people)
if (p->name() != from)
p->receive(from, msg);
}
void join(Person* p) {
string join_msg = p->name() + " joins the chat";
cout<<join_msg<<endl;
broadcast("room", join_msg);
p->setRoom(this);
m_people.push_back(p);
}
void message(string from, string to, string msg) override{
for(vector<Person *>::iterator it=m_people.begin();it!=m_people.end();)
{
if((*it)->name() == to)
{
(*it)->receive(from, msg);
break;
}
else
{
++it;
}
}
}
};
int main()
{
cout<<"Mediator Test!"<<endl;
GoogleChat room;
Person* john=new ChatUser(1,"John");
Person* jane=new ChatUser(2,"Jane");
room.join(john);
room.join(jane);
john->say("hi");
jane->say("oh, hey john");
Person* simon=new ChatUser(3,"Simon");
room.join(simon);
simon->say("hi everyone!");
jane->say("Simon", "glad you found us, simon!");
cout<<"finish!"<<endl;
return 0;
}
优点
减少对象间的直接依赖:中介者模式通过引入一个中介者对象来封装多个对象之间的交互,使得对象之间的通信需要通过中介者来完成,从而减少了对象之间的直接依赖关系,降低了系统的耦合度。
提高系统的可维护性和扩展性:通过中介者模式,可以将复杂的网状关系简化为星状关系,使得系统的维护和扩展变得更加容易。当需要添加新的对象或修改现有对象的交互时,只需修改中介者对象,而不需要修改所有相关的对象。
集中管理通信逻辑:中介者模式使得通信逻辑集中在中介者对象中,各个对象可以专注于各自的业务处理逻辑,而不需要关心通信的具体实现细节,简化了代码结构。
缺点
性能问题:由于所有的通信都需要通过中介者对象进行,可能会增加一定的处理时间,尤其是在高并发的情况下,中介者对象可能会成为性能瓶颈1。
调试和维护难度:由于所有的通信逻辑都集中在中介者对象中,当中介者对象出现问题时,可能会影响整个系统的正常运行,增加了调试和维护的难度1。
过度依赖中介者:如果过度依赖中介者对象进行通信,可能会导致系统对中介者的依赖过重,一旦中介者出现问题,整个系统可能会受到影响1。
解释器模式
解释器模式(Interpreter Pattern)就是描述了如何为简单的语言定义一个语法,如何在该语言中表示一个句子,以及如何解释这些句子。这种模式通常被用于开发编程语言解释器或简单的脚本引擎
在以下情况下可以考虑使用解释器模式:
(1) 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
(2) 一些重复出现的问题可以用一种简单的语言来进行表达。
(3) 一个语言的文法较为简单。
(4) 执行效率不是关键问题。
cpp
//小表达式(节点)父类
class Expression
{
public:
Expression(int num, char sign) :m_dbg_num(num), m_dbg_sign(sign) {} //构造函数
virtual ~Expression() {} //做父类时析构函数应该为虚函数
public:
//解析语法树中的当前节点
virtual int interpret(map<char, int> var) = 0; //#include <map>,map容器中的键值对用于保存变量名及对应的值
public:
//以下两个成员变量是为程序跟踪调试时观察某些数据方便而引入
int m_dbg_num; //创建该对象时的一个编号,用于记录本对象是第几个创建的
char m_dbg_sign; //标记本对象的类型,可能是个字符v代表变量(终结符表达式),也可能是个加减号(非终结符表达式)
};
//-----
//变量表达式(终结符表达式)
class VarExpression :public Expression
{
public:
VarExpression(const char& key, int num, char sign) :Expression(num, sign) //构造函数
{
m_key = key;
}
virtual int interpret(map<char, int> var)
{
return var[m_key]; //返回变量名对应的数值
}
private:
char m_key; //变量名,本范例中诸如a、b、c、d都是变量名
};
//------
//运算符表达式(非终结符表达式)父类
class SymbolExpression :public Expression
{
public:
SymbolExpression(Expression* left, Expression* right, int num, char sign) :m_left(left), m_right(right), Expression(num, sign) {} //构造函数
Expression* getLeft() { return m_left; }
Expression* getRight() { return m_right; }
protected:
//左右各有一个操作数
Expression* m_left;
Expression* m_right;
};
//加法运算符表达式(非终结符表达式)
class AddExpression :public SymbolExpression
{
public:
AddExpression(Expression* left, Expression* right, int num, char sign) :SymbolExpression(left, right, num, sign) {}//构造函数
virtual int interpret(map<char, int> var)
{
//分步骤拆开写,方便理解和观察
int value1 = m_left->interpret(var); //递归调用左操作数的interpret方法
int value2 = m_right->interpret(var); //递归调用右操作数的interpret方法
int result = value1 + value2;
return result; //返回两个变量相加的结果
}
};
//减法运算符表达式(非终结符表达式)
class SubExpression :public SymbolExpression
{
public:
SubExpression(Expression* left, Expression* right, int num, char sign) :SymbolExpression(left, right, num, sign) {}//构造函数
virtual int interpret(map<char, int> var)
{
int value1 = m_left->interpret(var);
int value2 = m_right->interpret(var);
int result = value1 - value2;
return result; //返回两个变量相减的结果
}
};
//分析---创建语法树(表达式树)
Expression* analyse(string strExp) //strExp:要计算结果的表达式字符串,比如"a-b+c+d"
{
stack<Expression*> expStack;//#include <stack>,这里用到了栈这种顺序容器
Expression* left = nullptr;
Expression* right = nullptr;
int icount = 1;
for (size_t i = 0; i < strExp.size(); ++i)//循环遍历表达式字符串中的每个字符
{
switch (strExp[i])
{
case '+':
//加法运算符表达式(非终结符表达式)
left = expStack.top(); //返回栈顶元素(左操作数)
++i;
right = new VarExpression(strExp[i], icount++, 'v'); //v代表是个变量节点
//在栈顶增加元素
expStack.push(new AddExpression(left, right, icount++, '+')); //'+'代表是个减法运算符节点
break;
case '-':
//减法运算符表达式(非终结符表达式)
left = expStack.top(); //返回栈顶元素
++i;
right = new VarExpression(strExp[i], icount++, 'v');
expStack.push(new SubExpression(left, right, icount++, '-')); //'-'代表是个减法运算符节点
break;
default:
//变量表达式(终结符表达式)
expStack.push(new VarExpression(strExp[i], icount++, 'v'));
break;
} //end switch
} //end for
Expression* expression = expStack.top(); //返回栈顶元素
return expression;
}
void release(Expression* expression)
{
//释放表达式树的节点内存
SymbolExpression* pSE = dynamic_cast<SymbolExpression*>(expression); //此处代码有优化空间(不使用dynamic_cast),留给读者思考
if (pSE)
{
release(pSE->getLeft());
release(pSE->getRight());
}
delete expression;
}
int main()
{
string strExp = "a-b+c+d"; //将要求值的字符串表达式
map<char, int> varmap;
//下面是给字符串表达式中所有参与运算的变量一个对应的数值
varmap.insert(make_pair('a', 7)); //类似于赋值语句a = 7
varmap.insert(make_pair('b', 9)); //类似于赋值语句b = 9
varmap.insert(make_pair('c', 3)); //类似于赋值语句c = 3
varmap.insert(make_pair('d', 2)); //类似于赋值语句d = 2
Expression* expression = analyse(strExp); //调用analyse函数创建语法树
int result = expression->interpret(varmap); //调用interpret接口求解字符串表达式的结果
cout << "字符串表达式\"a - b + c + d\"的计算结果为:" << result << endl; //输出字符串表达式结果
//释放内存
release(expression);
return 0;
}
优点:
(1) 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
(2) 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
(3) 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。
(4) 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合"开闭原则"。
缺点 :
(1) 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。
(2) 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
尽量不要在重要模块中使用解释器模式,因为维护困难。在项目中,可以使用脚本语言来代替解释器模式。
总结
到这里,所有设计模式都介绍完成了,其实虽然看着很多,但很多模式其实区别都不大,例如桥接和策略模式,外观和模版方法等 他们实现上都非常接近,只有细微不同,之所以名字不同,只是他们侧重点或实现的功能不同。大部分模式归结到一点就是,抽象一个接口类,通过接口再去实现和封装具体功能。