C++ 设计模式实战:外观模式和访问者模式的结合使用,派生类访问基类的私有子系统

简介: C++ 设计模式实战:外观模式和访问者模式的结合使用,派生类访问基类的私有子系统

外观模式,即 Facade Pattern。
外观模式是一种结构型设计模式,它提供了一个统一的接口来访问一个子系统的一组接口。外观将一个复杂的子系统与客户端代码分开,从而降低了子系统使用的复杂程度。
访问者模式(Visitor pattern)的概念
访问者模式是用于在不更改对象结构的前提下,为一个对象增加的操作。
访问者模式使您能够将相应的操作逻辑从基类中分离出来,并允许使用传递的回调来编写紧密相关的处理代码,使整个系统更具模块化和灵活性。

原有架构

基类源码

#pragma once
#include <vector>
#include <map>
#include <string>
#include <future>
namespace Conti {
class BaseConvertClass {
public:
    BaseConvertClass(){}
    virtual void clear();
    virtual bool ConvertData(const InputDataPtr&input_data, OutputDataPtr &output_data);
    virtual bool canParallel(void);
    //Quasi-observer mode:
    //Using std::function, we can realize the instantiation of parameterized subclass, delay the instantiation of subclass, and register as subclass in differential implementation.
    void registerBase(const std::string& id, std::function<BaseConvertClass*()> getInstanceFunction) {
        BaseConvertClass* instance = getInstanceFunction();
        if (instance != nullptr) {
            m_ProtocolBasesMap[id] = instance;
        }
    }
   //... 其他操作
private:
  // 使用std::map来存储子类
  std::map<std::string, BaseConvertClass*> m_ProtocolBasesMap;
};
}

架构背景

  1. BaseConvertClass抽象基类 BaseConvertClass,它为所有派生类提供了一个通用的接口。提到它包含一些需要在子类中实现的方法,如:clearConvertDatacanParallel
  2. 子类实现:继承 BaseConvertClass,这些子类具体实现了基类中的方法,在其中增加自己的业务逻辑。

以下是原有架构的 UML 类图简化示例:

+-----------------+                       +------------------+ |  BaseConvertClass  |  <|--      +------------------+ |   <<abstract>>  
|                         | ConvertIHBC          |
+-----------------+                       +------------------+ | - m_ProtocolBasesMap |                       |                          
| |                                   | ------------                  
| ----- (其他子类)
+-----------------+                       +------------------+                   ... | + clear()                 |                       | | +
ConvertData()      |                       | | + canParallel()       
|                       |-------------|-----+--------------
+-----------------+                        |                           |  ConverterXYZ |
                                                    +------------------+ ```
在这个图中,您可以看到原有架构包含一个抽象基类 `BaseConvertClass`,以及一些从基类派生的转换类,如
`ConvertIHBC` 和 `ConverterXYZ`(表示其他可能的子类)。每个子类实现了 `BaseConvertClass`
中声明的方法。其中,`m_ProtocolBasesMap` 是一个包含子类实例的映射,用于在基类中管理各个子类。

然而,这种架构存在子类管理问题

我们通过创建从BaseConvertClass继承的各种子类来实现具体的数据转换任务。每个子类都针对特定的数据转换需求实现了基类中定义的方法。然而,随着不同数据转换类型的增加,我们可能会遇到以下问题:

  1. 处理多个子类的复杂性:在main函数或应用程序的其他部分中,我们需要为每个数据转换子类编写单独的代码块,这将导致代码冗余和组织混乱。对于每个子类,我们需要单独实例化、注册,并调用相应的方法。
  2. 维护困难:对于每个新增的子类,我们都需要手动添加到程序中的相关部分。若某个子类的实现发生变化,找到并修改跟该子类相关的代码变得相当困难。代码的耦合度较高,不利于维护。
  3. 可扩展性问题:手动处理多个子类会影响可扩展性,因为添加、修改或删除子类时我们需要在多个地方做出相应的调整。如果遇到多个子类需要相互依赖或有特定执行顺序的情况,代码可读性和管理难度进一步加大。

面对这些问题,我们需要寻求更为清晰和可维护的解决方案,例如引入ConvertManager类。在下一部分中,我将详细说明引入ConvertManager类解决了这些问题的方法,并使整体程序设计变得更加高效。

引入 ConvertManager

为了解决多子类管理问题,我们引入一个集中管理器类 ConvertManager,它负责整合和管理所有数据转换子类。

ConvertManager 类详解:

ConvertManagerBaseConvertClass 的一个子类,继承了其方法。这样做的目的是让 ConvertManager 与其他子类具有相同的接口,同时扩展其功能,用于管理所有的子类实例。

ConvertManager 的主要职责:

  1. 在构造函数中注册所有子类实例:在 ConvertManager 的构造函数中,我们通过调用 registerBase 方法注册所有数据转换子类实例。这样,我们只需要在这个地方添加和维护子类的注册。
ConvertManager() {
    registerBase("ConvertIHBC", ConvertIHBC::getInstance);
    // 注册其他子类...
}
  1. 组织和管理子类实例的方法调用:BaseConvertClass 有一个名为 ConvertDataAll 的方法,它通常用于调用子类实例的 ConvertData 方法。ConvertManager 继承了原始的 ConvertDataAll 方法,可以轻松地组织和管理子类实例的调用。

如何在 main 函数中创建 ConvertManager 对象

修改后的架构让我们能够在 main 函数中只创建一个 ConvertManager 对象,实现更简洁、集中的数据转换操作。

#include "convert_manager.h"
int main() {
    Conti::ConvertManager manager;
    // 使用 manager 调用 BaseConvertClass 及其子类的方法。
    // manager.clear(...);
    // manager.ConvertData(...);
    // manager.ConvertDataAll(...);
    return 0;
}

使用 ConvertManager 类的优势

引入 ConvertManager 类带来了以下优势:

  1. 代码组织更清晰ConvertManager 类的作用是将所有子类实例的创建、注册、和管理集中在一个地方。这使得代码更清晰可读。
  2. 易于扩展:如果要增加新的子类,只需在 ConvertManager 的构造函数中注册即可。我们不需要在多个地方修改代码适应新子类。
  3. 更易于维护:由于所有子类实例的创建和管理都在 ConvertManager 中进行,所以在任何子类发生变化时,代码更容易修改和维护。

通过这个详细介绍,我们可以清楚地看到引入 ConvertManager 类对原有架构的改进。这使得我们可以更方便地处理多种数据转换子类,同时保持代码的整洁和易于维护。

遇到的问题和解决方案

遇到的问题

为了提高代码的组织性和可维护性。基类 (BaseConvertClass) 负责定义公共接口,而 ConvertManager 负责让用户可以轻松地使用和管理这些接口。这种分层结构让整个程序更容易扩展、添加新功能,并在可能的情况下减少错误。这种设计思路遵循了计算机编程中的 单一职责原则 (SRP),即每个类应该只负责一项职责。

虽然在基类BaseConvertClass中声明std::map<std::string, BaseConvertClass*> m_ProtocolBasesMap是合适的,因为它要存储所有子类的实例。

但是将m_ProtocolBasesMap作为基类的私有成员,意味着不允许子类直接访问它。您可以通过在基类中提供用于注册、遍历和访问子类的公开方法来解决此问题。

解决方案

这里的一个解决方案是提供一个遍历类似于ConvertDataAllclearAll的回调方法,您可以根据需要实现 ConvertManager 或其他子类。以下是修改后的BaseConvertClass代码:

class BaseConvertClass {
    // ... (其他部分代码不变)
    // 添加一个使用回调遍历 m_ProtocolBasesMap 的公开方法
    void forEachRegisteredBase(std::function<void(const std::string&, BaseConvertClass*)> callback) {
        for (const auto& pair : m_ProtocolBasesMap) {
            callback(pair.first, pair.second);
        }
    }
    // ... (其他部分代码不变)
};

现在,您可以在ConvertManager中使用这个新的 forEachRegisteredBase 方法,例如:

void ConvertDataAll(const InputDataPtr& input_data, OutputDataPtr& output_data) {
    forEachRegisteredBase([&](const std::string& id, BaseConvertClass* base) {
        // 在这里执行操作,例如调用其他方法或处理子类数据
    });
}

详细解释

通过使用lambda表达式并捕获局部变量,forEachRegisteredBase 完美地在回调函数中传递了 idbase。让我们逐步解释这个过程。

首先,当您调用 forEachRegisteredBase 时,您需要提供一个接受 const std::string&BaseConvertClass* 参数的回调函数。在这种情况下,我们使用了lambda表达式:

[&](const std::string& id, BaseConvertClass* base) {
    // 在这里执行操作
}

[&] 是捕获列表。它捕获当前范围内的所有变量(例如 input_dataoutput_data)。捕获列表使我们能够在 lambda 表达式内访问和使用这些变量。

现在,我们再来看一下 forEachRegisteredBase 的实现:

void forEachRegisteredBase(std::function<void(const std::string&, BaseConvertClass*)> callback) {
    for (const auto& pair : m_ProtocolBasesMap) {
        callback(pair.first, pair.second);
    }
}

在这里,forEachRegisteredBase 接收一个 std::function 类型的参数,该参数表示一个接受 const std::string&BaseConvertClass* 参数的函数。在forEachRegisteredBase 方法内部,我们遍历了 m_ProtocolBasesMap,对于其中的每一个元素(std::pair<std::string, BaseConvertClass*> 类型),我们调用了 callback 函数并传入 pair.first 作为第一个参数(id),及 pair.second 作为第二个参数(base)。

在我们的例子中,对于 ConvertDataAll 方法,我们调用了 forEachRegisteredBase 并传入了一个 lambda 表达式。这个 lambda 表达式将在这个函数内接收 idbase 作为参数,并执行您需要的任何操作。

void ConvertDataAll(const InputDataPtr& input_data, OutputDataPtr& output_data) {
    forEachRegisteredBase([&](const std::string& id, BaseConvertClass* base) {
        // 在这里执行操作,例如调用其他方法或处理子类数据
    });
}

总之,forEachRegisteredBase 内部循环调用所传入的回调函数,并且在每次调用时将相应的 idbase 作为参数传递。在 ConvertDataAll 方法中,我们传递了一个 lambda 表达式作为回调函数,这个表达式捕获了所需的局部变量并接收了 idbase,以便您在其中执行相应的操作。

设计模式的收获

这个设计模式结合了外观模式和访问者模式的特点。

  1. 外观模式:
    BaseConvertClass 充当一个外观,它将不同子类的操作封装成更简单、更高级的接口。BaseConvertClass 本身不实现数据转换的具体操作,而是将这些操作委托给各个子类。这种设计的目的是简化客户端代码,让客户端可以使用统一的方式来操作和访问多种不同的数据转换子类。
  2. 访问者模式
    通过向 BaseConvertClass 添加一个名为 forEachRegisteredBase 的方法,并将一个函数作为参数,我们为所有已注册子类提供了一个统一的访问机制。这个方法允许客户端在不改变基类和子类的实现的情况下,在基类的上下文中灵活地实现和迭代操作。

因此,这个设计模式实际上是外观模式和访问者模式的结合。客户端仅需与一个简化接口(BaseConvertClass)交互,从而实现对一组子类的访问和操作。同时,通过添加 forEachRegisteredBase 方法,我们引入了类似访问者模式的概念,这使得在保持基类和子类之间的边界清晰的前提下,客户端可以根据需要灵活地实现操作。

目录
相关文章
|
1天前
|
编译器 C++
【C++】类和对象(下)
【C++】类和对象(下)
|
1天前
|
编译器 C++
【C++】类和对象(中)(2)
【C++】类和对象(中)(2)
|
1天前
|
存储 编译器 C++
【C++】类和对象(中)(1)
【C++】类和对象(中)(1)
|
1天前
|
存储 编译器 C语言
【C++】类和对象(上)
【C++】类和对象(上)
|
7天前
|
设计模式 Java 数据库
小谈设计模式(2)—简单工厂模式
小谈设计模式(2)—简单工厂模式
|
7天前
|
设计模式 API
【设计模式】适配器和桥接器模式有什么区别
【设计模式】适配器和桥接器模式有什么区别
10 1
|
7天前
|
设计模式
【设计模式】张一鸣笔记:责任链接模式怎么用?
【设计模式】张一鸣笔记:责任链接模式怎么用?
11 1
|
7天前
|
设计模式 uml
【设计模式】建造者模式就是游戏模式吗?
【设计模式】建造者模式就是游戏模式吗?
11 0
|
7天前
|
设计模式 Java uml
【设计模式】什么是工厂方法模式?
【设计模式】什么是工厂方法模式?
8 1
|
7天前
|
设计模式 uml
【设计模式】一文搞定简单工厂模式!
【设计模式】一文搞定简单工厂模式!
8 2