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

目录
相关文章
|
4月前
|
C++
C++ 语言异常处理实战:在编程潮流中坚守稳定,开启代码可靠之旅
【8月更文挑战第22天】C++的异常处理机制是确保程序稳定的关键特性。它允许程序在遇到错误时优雅地响应而非直接崩溃。通过`throw`抛出异常,并用`catch`捕获处理,可使程序控制流跳转至错误处理代码。例如,在进行除法运算或文件读取时,若发生除数为零或文件无法打开等错误,则可通过抛出异常并在调用处捕获来妥善处理这些情况。恰当使用异常处理能显著提升程序的健壮性和维护性。
84 2
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
90 4
|
2月前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
283 11
|
1月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
3月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
168 1
|
4月前
|
存储 算法 C++
C++ STL应用宝典:高效处理数据的艺术与实战技巧大揭秘!
【8月更文挑战第22天】C++ STL(标准模板库)是一组高效的数据结构与算法集合,极大提升编程效率与代码可读性。它包括容器、迭代器、算法等组件。例如,统计文本中单词频率可用`std::map`和`std::ifstream`实现;对数据排序及找极值则可通过`std::vector`结合`std::sort`、`std::min/max_element`完成;而快速查找字符串则适合使用`std::set`配合其内置的`find`方法。这些示例展示了STL的强大功能,有助于编写简洁高效的代码。
57 2
|
3月前
|
JavaScript 前端开发 Java
通过Gtest访问C++静态、私有、保护变量和方法
通过Gtest访问C++静态、私有、保护变量和方法
97 0
|
3月前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
301 0
|
4月前
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
|
5月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
【7月更文挑战第28天】在 Android 开发中, NDK 让 Java 与 C++ 混合编程成为可能, 从而提升应用性能。**为何选 NDK?** C++ 在执行效率与内存管理上优于 Java, 特别适合高性能需求场景。**环境搭建** 需 Android Studio 和 NDK, 工具如 CMake。**JNI** 构建 Java-C++ 交互, 通过声明 `native` 方法并在 C++ 中实现。**实战** 示例: 使用 C++ 计算斐波那契数列以提高效率。**总结** 混合编程增强性能, 但增加复杂性, 使用前需谨慎评估。
153 4