访问者模式,从双十一购物开始说起

2022-07-24 16:14:17 浏览数 (1)

引言

欧耶!这周Jungle的作业终于做完了!作业是什么呢?就是完成一个习题册。Jungle做完之后,得让家长检查习题册并签字;第二天交到学校,组长得初步检查作业是否做完、家长是否签字,然后老师会评阅作业是否正确,并给出评分。

就是这么一个习题册,这是经了多少人的手啊!

  1. Jungle——完成习题册上的题;
  2. Jungle家长——检查儿子的作业,并在习题册上签字;
  3. 组长——初步检查Jungle的习题册是否完成;
  4. 老师——评阅习题册,给出评分。

同样一个对象(习题册),不同的人都去访问它,并且访问的方式不同,Jungle是为了完成作业,Jungle爸爸是为了签字,组长是为了检查Jungle是否完成,而老师是为了评分。 每一个人都扮演了访问者的角色。

什么?访问者

01

访问者模式简介

类似于上述的习题册,软件设计中也需要这样的类似于习题册的对象结构,不同的对象对应不同的处理。设计模式中,访问者模式就是为了以不同的方式来操作复杂的对象结构

访问者模式是一种较为复杂的行为型设计模式,具有访问者和被访问元素两个主要的角色。被访问的元素常常有不同的类型,不同的访问者可以对它们提供不同的访问方式。被访问元素通常不是单独存在,而是以集合的形式存在于一个对象结构中,访问者可以遍历该对象结构,以逐个访问其中的每一个元素。

访问者模式:表示一个作用于某对象结构中的各个元素的操作。访问者模式让用户可以在不改变各元素的前提下定义作用于这些元素的新操作。

02

访问者模式结构

访问者模式的结构相对较复杂,角色有如下几个:

  • Visitor(抽象访问者):抽象类,声明了访问对象结构中不同具体元素的方法,由方法名称可知该方法将访问对象结构中的某个具体元素;
  • ConcreteVisitor(具体访问者):访问某个具体元素的访问者,实现具体的访问方法;
  • Element(抽象元素):抽象类,一般声明一个accept()的方法,用于接受访问者的访问,accept()方法常常以一个抽象访问者的指针作为参数;
  • ConcreteElement(具体元素):针对具体被访问的元素,实现accept()方法;
  • ObjectStructure(对象结构):元素的集合,提供了遍历对象结构中所有元素的方法。对象结构存储了不同类型的元素对象,以供不同的访问者访问。

访问者模式的UML结构图如下:

从上图和前述可以看出,访问者模式中有两个层次结构:

  • 访问者的层次结构:抽象访问者和具体访问者,不同的具体访问者有不同的访问方式(visit()方式);
  • 被访问元素的层次结构:抽象元素和具体元素,不同的具体元素有不同的被访问方式(accept()方式)

正是由于有这两个层次结构,在增加新的访问者时,不必修改已有的代码,通过继承抽象访问者即可实现扩展,符合开闭原则,系统扩展性较好。但是在增加新的元素时,既要修改抽象访问者类(增加访问新增元素方法的声明),又要修改具体访问者(增加新的具体访问者类),不符合开闭原则。

访问者模式的示例代码如下:

代码语言:javascript复制
#ifndef __DEMO_H__
#define __DEMO_H__

// 抽象访问者 Visitor
class Visitor
{
public:
  virtual void visit(ConcreteElementA*) = 0;
  virtual void visit(ConcreteElementB*) = 0;
};

// 具体访问者 ConcreteVisitor
class ConcreteVisitor :public Visitor
{
public:
  // 实现一种针对特定元素的访问操作
  void visit(ConcreteElementA*){
    // 元素A的访问操作代码
  }
  void visit(ConcreteElementB*){
    // 元素B的访问操作代码
  }
};

// 抽象元素
class Element
{
public:
  // 声明抽象方法,以一个抽象访问者的指针作为函数参数
  virtual void accept(Visitor*) = 0;
};

// 具体元素
class ConcreteElement :public Element
{
public:
  void accept(Visitor* visitor){
    visitor->visit(this);
  }
};

// 对象结构
class ObjectStructure
{
public:
  //  提供接口接受访问者访问
  void accept(Visitor* visitor){
    // 遍历访问对象结构中的元素
    for (){
      elementList[i]->accept(visitor);
    }
  }
  void addElement(){}
  void removeElement(){}
private:
  lsit<Element*>elementList;
};

#endif

03

访问者模式代码实例

Jungle作为一名顾客,去超市购物,加入购物车的商品包括两种苹果和两本书,结账时收银员需要计算各个商品的的价格。本例Jungle采用访问者模式来模拟该过程。

本例中,客户Jungle和收银员都会去访问商品,但关心的地方不同:Jungle关心的是苹果和书的单价、品牌等,收银员关注的是商品的价格。因此,客户Customer和收银员Cashier是具体访问者,而苹果Apple和书Book是具体被访问元素;而购物车则是对象结构。本例的UML图如下:

3.1.元素类

3.1.1.抽象元素

代码语言:javascript复制
// 抽象元素
class Element
{
public:
  Element(){};
  virtual void accept(Visitor*) = 0;
  void setPrice(int iPrice){
    this->price = iPrice;
  }
  int getPrice(){
    return this->price;
  }
  void setNum(int iNum){
    this->num = iNum;
  }
  int getNum(){
    return num;
  }
  void setName(string iName){
    this->name = iName;
  }
  string getName(){
    return this->name;
  }
private:
  int price;
  int num;
  string name;
};

3.1.2.具体元素Apple

代码语言:javascript复制
// 具体元素:Apple
class Apple :public Element
{
public:
  Apple();
  Apple(string name, int price);
  void accept(Visitor*);
};

实现:

代码语言:javascript复制
Apple::Apple(){
  setPrice(0);
  setNum(0);
  setName("");
}
Apple::Apple(string name, int price){
  setPrice(price);
  setNum(0);
  setName(name);
}

void Apple::accept(Visitor* visitor){
  visitor->visit(this);
}

3.1.3.具体元素Book

代码语言:javascript复制
// 具体元素:Book
class Book :public Element
{
public:
  Book();
  Book(string name, int price);
  void accept(Visitor*);
};

实现:

代码语言:javascript复制
Book::Book(){
  setPrice(0);
  setNum(0);
  setName("");
}

Book::Book(string iName, int iPrice){
  setPrice(iPrice);
  setNum(0);
  setName(iName);
}

void Book::accept(Visitor* visitor){
  visitor->visit(this);
}

3.2.访问者

3.2.1.抽象访问者

代码语言:javascript复制
// 抽象访问者
class Visitor
{
public:
  Visitor(){};
  // 声明一组访问方法
  virtual void visit(Apple*) = 0;
  virtual void visit(Book*) = 0;
};

实现:

代码语言:javascript复制
Customer::Customer(){
  this->name = "";
}

Customer::Customer(string iName){
  this->name = iName;
}

void Customer::setNum(Apple* apple, int iNum){
  apple->setNum(iNum);
}
void Customer::setNum(Book* book, int iNum){
  book->setNum(iNum);
}

void Customer::visit(Apple* apple){
  int price = apple->getPrice();
  printf("  %s t单价: t%d 元/kgn", apple->getName().c_str(), apple->getPrice());
}

void Customer::visit(Book* book){
  int price = book->getPrice();
  string name = book->getName();
  printf("  《%s》t单价: t%d 元/本n", book->getName().c_str(), book->getPrice());
}

3.2.2.具体访问者Customer

代码语言:javascript复制
// 具体访问者:顾客
class Customer :public Visitor
{
public:
  Customer();
  Customer(string iName);
  void setNum(Apple*, int);
  void setNum(Book*, int);
  void visit(Apple* apple);
  void visit(Book* book);
private:
  string name;
};

实现:

代码语言:javascript复制
Customer::Customer(){
  this->name = "";
}
 
Customer::Customer(string iName){
  this->name = iName;
}
 
void Customer::setNum(Apple* apple, int iNum){
  apple->setNum(iNum);
}
void Customer::setNum(Book* book, int iNum){
  book->setNum(iNum);
}
 
void Customer::visit(Apple* apple){
  int price = apple->getPrice();
  printf("  %s t单价: t%d 元/kgn", apple->getName().c_str(), apple->getPrice());
}
 
void Customer::visit(Book* book){
  int price = book->getPrice();
  string name = book->getName();
  printf("  《%s》t单价: t%d 元/本n", book->getName().c_str(), book->getPrice());
}

3.2.3.具体访问者Cashier

代码语言:javascript复制
class Cashier :public Visitor
{
public:
  Cashier();
  void visit(Apple* apple);
  void visit(Book* book);
};

实现:

代码语言:javascript复制
Cashier::Cashier(){

}

void Cashier::visit(Apple* apple){
  string name = apple->getName();
  int price = apple->getPrice();
  int num = apple->getNum();
  int total = price*num;
  printf("  %s 总价:%d 元n", name.c_str(), total);
}

void Cashier::visit(Book* book){
  int price = book->getPrice();
  string name = book->getName();
  int num = book->getNum();
  int total = price*num;
  printf("  《%s》 总价:%d 元n", name.c_str(), total);
}

3.3.购物车ShoppingCart

代码语言:javascript复制
class ShoppingCart
{
public:
  ShoppingCart(){}
  void addElement(Element* element){
    printf("  商品名:%s, t数量:%d, t加入购物车成功!n", element->getName().c_str(), element->getNum());
    elementList.push_back(element);
  }
  void accept(Visitor* visitor){
    for (int i = 0; i < elementList.size(); i  ){
      elementList[i]->accept(visitor);
    }
  }
private:
  vector<Element*>elementList;
};

3.4.客户端代码示例及结果

代码语言:javascript复制
#include "Element.h"
#include "Visitor.h"
#include "ShoppingCart.h"
#include <Windows.h>
 
int main()
{
  Apple *apple1 = new Apple("红富士苹果", 7);
  Apple *apple2 = new Apple("花牛苹果", 5);
  Book *book1 = new Book("红楼梦", 129);
  Book *book2 = new Book("终结者", 49);
 
  Cashier* cashier = new Cashier();
  Customer* jungle = new Customer("Jungle");
  jungle->setNum(apple1, 2);
  jungle->setNum(apple2, 4);
  jungle->setNum(book1, 1);
  jungle->setNum(book2, 3);
 
  ShoppingCart* shoppingCart = new ShoppingCart();
  shoppingCart->addElement(apple1);
  shoppingCart->addElement(apple2);
  shoppingCart->addElement(book1);
  shoppingCart->addElement(book2);
 
  printf("nn");
  shoppingCart->accept(jungle);
 
  printf("nn");
  shoppingCart->accept(cashier);
 
  printf("nn");
  system("pause");
  return 0;
}

上述代码运行结果如下:

上述代码资源见https://github.com/FengJungle/DesignPattern

04

总结

访问者模式的结构相对较复杂,在实际应用中使用频率较低。如果系统中存在一个复杂的对象结构,且不同的访问者对其具有不同的操作,那么可以考虑使用访问者模式。访问者模式的特点总结如下:

优点:

  • 增加新的访问者很方便,即增加一个新的具体访问者类,定义新的访问方式,无需修改原有代码,符合开闭原则;
  • 被访问元素集中在一个对象结构中,类的职责更清晰,利于对象结构中元素对象的复用;

缺点:

  • 增加新的元素类很困难,增加新的元素时,在抽象访问者类中需要增加一个对新增的元素方法的声明,即要修改抽象访问者代码;此外还要增加新的具体访问者以实现对新增元素的访问,不符合开闭原则;
  • 破坏了对象的封装性,访问者模式要求访问者对象访问并调用每一个元素对象的操作,那么元素对象必须暴露自己的内部操作和状态,否则访问者无法访问。

0 人点赞