设计模式 - 访问者模式

2022-05-03 14:17:18 浏览数 (1)

访问者模式

将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。

简单的说,就是已经存在稳定的数据类。由于外部需求,需要访问特定的类成员。希望在不改变原数据类接口,仅通过增加外部模块实现需求。此模式,是行为模式中最复杂的一种模式。

意义

此模式主要用于在存在多个同类型的数据类情况下,统一对这些数据类某个成员属性的访问方式。有助于将数据代码与业务代码解耦,可在不修改数据类的情况下自由增加访问方式业务。

访问者类图访问者类图

应用场景

上述分析了,此模式多用于存在多个同类型数据类,只访问这些类某个成员属性。例:电脑管家检测电脑,要一项一项检测,先功能检测、再驱动检测。其中功能检测,只检测各配件(GPU、声卡)的功能是否正常。驱动检测,只检测各配件驱动是否正常。(具体如何检测的,这里不做关注)

分析

由上述电脑管家检测场景,可构建类图:

电脑检测场景类图电脑检测场景类图

数据源类: GPU(CPartGpu)、声卡(CPartSoundCard),两者可抽象出基类电脑组件(CComputePartBase);

访问类: 功能访问(CVisitorFunction)、驱动访问(CVisitorDriver),两者可抽象访问基类(CVisitorBase);

管理类: 电脑管家(CSafeMgr)。

注: 在最初的访问者模式类图没有管理类的角色,这里为了方便客户端使用接口,才增加此类。实际场景中,只要运用到访问者模式思想即可,没有必要参照其实现方式生搬硬套。

源码实现

编程环境

  1. 编译环境: Linux环境
  2. 语言: C 语言
  3. 编译命令: make

工程结构

代码语言:c 复制
Visitor/
├── compute_part_base.h
├── main.cc
├── Makefile
├── part_gpu.cc
├── part_gpu.h
├── part_sound_card.cc
├── part_sound_card.h
├── safe_manager.cc
├── safe_manager.h
├── visitor_base.h
├── visitor_driver.cc
├── visitor_driver.h
├── visitor_function.cc
└── visitor_function.h
  • compute_part、part_gpu、part_sound_card: 源数据类封装。
  • vistor_base、vistor_driver、visitor_function: 对源数据类的访问方式类。
  • safe_manager: 管理类
  • main.cc: 客户端代码,程序入口
  • Makefile: 编译工具

源数据接口

  1. 电脑配件基类class CComputePartBase { public: CComputePartBase() {} virtual ~CComputePartBase() {} virtual std::string GetName() = 0; virtual void Accept(CVisitorBase *visitor) = 0; };
  2. 配件基类声明纯虚函数Accept(), 依赖CVisitorBase,主要为外部访问类提供入口。通过此接口,各个访问类就可以触发自己内部的检测方式。
  3. GPU配件class CPartGpu : public CComputePartBase { public: explicit CPartGpu(std::string name, int driver, int function); ~CPartGpu(); std::string GetName(); int CheckDriver(); int CheckFunction(); void Accept(CVisitorBase *visitor); private: std::string mName; int mDriver; int mFunction; };

其内部实现了Accept()接口,看下Accept()做了什么事情。

代码语言:c 复制
void CPartGpu::Accept(CVisitorBase *visitor)
{
    if (NULL != visitor) {
        visitor->VisitGpu(this);
    } else {
        GPU_LOGE("visitor is NULL!n");
    }
}
  • 可看到Accept内部调用了CVisitorBase,并将自身数据传参至其成员函数VisitGpu()。如此一来,CVisitorBase就能够访问CPartGpu对象的成员了。undefined
  • 现已将CPartGpu对象指针传递给CVisitorBase,至于其访问哪些数据,则由CVisitorBase派生的各个子类来具体实现。即不同业务的子类根据自身需求,访问CPartGpu不同的成员属性。
  • CPartSoundCard逻辑与CPartGpu大体相似,这里不做分析。

访问者接口

  1. 访问者基类class CVisitorBase { public: virtual std::string GetName() = 0; virtual int VisitGpu(CPartGpu *gpu) = 0; virtual int VisitSoundCard(CPartSoundCard *soundCard) = 0; };class CVisitorDriver : public CVisitorBase { public: CVisitorDriver(); ~CVisitorDriver(); std::string GetName(); int VisitGpu(CPartGpu *gpu); int VisitSoundCard(CPartSoundCard *soundCard); private: std::string mName; };驱动访问者是具体的访问者类,其会实现VisitGpu、VisitSoundCard但是其只关心Gpu和SoundCard的驱动属性。而功能访问者只关心Gpu和SoundCard的功能属性。
  2. 访问者基类统一访问配件的接口,在CComputePartBase的Accept()接口的实现中会被使用到。
  3. 一种类型的配件会对应一个Visit接口。在硬件已知的情况下,这些接口基本会固定无需修改。大多数情况下,不会在硬件固定的情况下,还会增加意料之外的配件。
  4. 驱动访问者

查看下VisitGpu()、VisitSoundCard()接口的实现:

代码语言:c 复制
int CVisitorDriver::VisitGpu(CPartGpu *gpu)
{
    if (gpu->CheckDriver() <= 0) {
        DRV_LOG("%s of Gpu Failed!n", this->GetName().c_str());
    } else {
        DRV_LOG("%s of Gpu Success!n", this->GetName().c_str());
    }        
    
    return 0;
}

int CVisitorDriver::VisitSoundCard(CPartSoundCard *soundCard)
{
    if (NULL == soundCard) {
        DRV_LOGE("soundCard is NULL!n");
    }

    if (soundCard->CheckDriver() <= 0) {
        DRV_LOG("%s of SoundCard Failed!n", this->GetName().c_str());
    } else {
        DRV_LOG("%s of SoundCard Success!n", this->GetName().c_str());
    }

    return 0;
}
  • 这两个接口都是通过具体的配件对象来访问其内部的成员属性。即访问者类通过配件类提供的接口访问配件内部信息,通过这些属性可做一些业务逻辑。实现数据与业务解耦,数据放在配件类,业务放在访问者内或者更外层。

管理类

代码语言:c 复制
typedef enum
{
    ITEM_DRIVER = 0,
    ITEM_FUNCTION,
    ITEM_ALL,
}ECheckItem;

class CSafeMgr
{
public:
    CSafeMgr();

    ~CSafeMgr();

    void AddPartArray(CComputePartBase *);

    void Accept(CVisitorBase *visitor);

    void CheckItem(ECheckItem item);

private:
    CPartGpu         *mpGpu;
    CPartSoundCard   *mpSoundCard;
    CVisitorDriver   theDriverCheck;
    CVisitorFunction theFunctionCheck;
    std::vector<CComputePartBase *> mPartArray; 
};

电脑管家就是管理类的作用,主要用于整理所有源数据对象与访问者对象。实现各个场景接口,为客户端提供简单易用的接口。

具体实现:

代码语言:c 复制
void CSafeMgr::AddPartArray(CComputePartBase *pPart)
{
    if (NULL == pPart) {
        MGR_LOGE("pPart is NULL!n");
        return ;
    }

    mPartArray.push_back(pPart);
}

void CSafeMgr::Accept(CVisitorBase *visitor)
{
    vector<CComputePartBase *>::iterator it;

    for (it = mPartArray.begin(); it != mPartArray.end();   it) {
        (*it)->Accept(visitor);
    }
}

void CSafeMgr::CheckItem(ECheckItem item)
{
    switch (item)
    {
        case ITEM_DRIVER:
            Accept(&theDriverCheck);
        break;

        case ITEM_FUNCTION:
            Accept(&theFunctionCheck);
        break;

        case ITEM_ALL:
            Accept(&theDriverCheck);
            Accept(&theFunctionCheck);
        break;

        default:
            MGR_LOGW("No this item!n");
        break;
    } 
}
  • CSafeMgr将配件对象注册到mPartArray,当需要检测指定项时,便依次变量所用配件的Accept(Item)接口,完成所有配件该项检测。
  • CSafeMgr只是为了方便客户端使用配件类和访问者类。实际访问者模式的设计中没有此角色,可参考意义上的类图。

客户端代码

代码语言:txt复制
int main(int argc, char *argv[])
{
    CSafeMgr theSafeMgr;
    theSafeMgr.CheckItem(ITEM_ALL);

    return 0;
}

因为设计了CSafeMgr接口,main代码就简单易懂,只是运用CSafeMgr检测指定项即可。其无需关注有多少配件和多少测试项,只需关注结果。

总结

  • 满足单一职责原则。访问者模式将数据与业务解耦,将同一行为的不同内容移植特定的类。
  • 满足开闭原则。 你以引入在不同类对象上执行的新行为,且无需对这些类做出修改。
  • 为了解决各个模块头文件相互引用问题,这里多次使用C 的前置声明。是一种解决这类问题比较实用的方法。
  • 每增加一个行为要增加多个类,加入这些功能是不需要的,清理起来也是比较复杂的。因此在使用访问者模式前,先审视是否有必要。
  • 访问者模式的实现相对来说很复杂,因为涉及到各个类的双向交互。同时领略访问者的思想,选择恰当的实现方式即可。

0 人点赞