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 方法,我们引入了类似访问者模式的概念,这使得在保持基类和子类之间的边界清晰的前提下,客户端可以根据需要灵活地实现操作。

目录
相关文章
|
设计模式 JavaScript 前端开发
JavaScript设计模式--访问者模式
【10月更文挑战第1天】
399 124
|
设计模式 XML JSON
【设计模式】【行为型模式】访问者模式(Visitor)
一、入门 什么是访问者模式? 访问者模式(Visitor Pattern)是一种行为设计模式,允许你将算法与对象结构分离。通过这种方式,可以在不改变对象结构的情况下,向对象结构中的元素添加新的操作。
437 10
|
机器学习/深度学习 存储 算法
基于 C++ 布隆过滤器算法的局域网上网行为控制:URL 访问过滤的高效实现研究
本文探讨了一种基于布隆过滤器的局域网上网行为控制方法,旨在解决传统黑白名单机制在处理海量URL数据时存储与查询效率低的问题。通过C++实现URL访问过滤功能,实验表明该方法可将内存占用降至传统方案的八分之一,查询速度提升约40%,假阳性率可控。研究为优化企业网络管理提供了新思路,并提出结合机器学习、改进哈希函数及分布式协同等未来优化方向。
393 0
|
设计模式 算法 Java
【再谈设计模式】访问者模式~操作对象结构的新视角
  访问者模式是一种行为设计模式,旨在解决对象结构与操作逻辑的耦合问题。在软件系统开发中,当面临复杂的对象结构(如多种类型对象组成的树形或图形结构),且需要对这些对象执行不同操作时,传统方式将操作直接写在对象类中会导致类职责过多,不利于维护和扩展。而访问者模式通过将操作与对象结构分离,允许在不改变现有对象结构的情况下定义新操作,元素接受访问者访问,访问者定义对不同类型元素的操作逻辑,从而为应对这种复杂情况提供了有效的解决方案。
367 0
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
905 4
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP开发领域,设计模式是解决常见问题的高效方案集合。它们不是具体的代码,而是一种编码和设计经验的总结。单例模式作为设计模式中的一种,确保了一个类仅有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的基本概念、实现方式及其在PHP中的应用。
单例模式在PHP中的应用广泛,尤其在处理数据库连接、日志记录等场景时,能显著提高资源利用率和执行效率。本文从单例模式的定义出发,详细解释了其在PHP中的不同实现方法,并探讨了使用单例模式的优势与注意事项。通过对示例代码的分析,读者将能够理解如何在PHP项目中有效应用单例模式。
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
378 2
|
JavaScript 前端开发 Java
通过Gtest访问C++静态、私有、保护变量和方法
通过Gtest访问C++静态、私有、保护变量和方法
502 1
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
269 1
|
设计模式 缓存 算法
Java设计模式-访问者模式(22)
Java设计模式-访问者模式(22)
343 0

热门文章

最新文章