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

目录
相关文章
|
24天前
|
设计模式 JavaScript 前端开发
JavaScript设计模式--访问者模式
【10月更文挑战第1天】
29 3
|
2月前
|
设计模式 C# 开发者
C#设计模式入门实战教程
C#设计模式入门实战教程
|
2月前
|
设计模式 缓存 算法
Java设计模式-访问者模式(22)
Java设计模式-访问者模式(22)
|
3月前
|
前端开发 开发者 开发框架
JSF与Bootstrap,打造梦幻响应式网页!让你的应用跨设备,让用户爱不释手!
【8月更文挑战第31天】在现代Web应用开发中,响应式设计至关重要,以确保不同设备上的良好用户体验。本文探讨了JSF(JavaServer Faces)与Bootstrap框架的结合使用,展示了如何构建响应式网页。JSF是一个基于Java的Web应用框架,提供丰富的UI组件和表单处理功能;而Bootstrap则是一个基于HTML、CSS和JavaScript的前端框架,专注于实现响应式设计。通过结合两者的优势,开发者能够更便捷地创建自适应布局,提升Web应用体验。然而,这种组合也有其局限性,如JSF组件库较小和较高的学习成本等,因此在选择开发框架时需综合考虑具体需求和应用场景。
45 0
|
3月前
|
设计模式 前端开发 开发者
Angular携手Material Design:探索设计模式下的UI组件开发之道——从按钮到对话框的全面实战演示
【8月更文挑战第31天】在现代Web应用开发中,Angular框架结合Material Design设计原则与组件库,显著提升了用户界面的质量与开发效率。本文通过具体代码示例,详细介绍如何在Angular项目中引入并使用Material Design的UI组件,包括按钮、表单和对话框等,帮助开发者快速构建美观且功能强大的应用。通过这种方式,不仅能提高开发效率,还能确保界面设计的一致性和高质量,为用户提供卓越的体验。
29 0
|
3月前
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
37 1
|
3月前
|
设计模式 存储 Java
掌握Java设计模式的23种武器(全):深入解析与实战示例
掌握Java设计模式的23种武器(全):深入解析与实战示例
|
4月前
|
设计模式 C++
C++一分钟之-设计模式:工厂模式与抽象工厂
【7月更文挑战第14天】设计模式是解决软件设计问题的通用方案。工厂模式与抽象工厂模式是创建型模式,用于对象创建而不暴露创建逻辑。工厂模式推迟实例化到子类,但过度使用会增加复杂性。抽象工厂则创建相关对象族,但过度抽象可能造成不必要的复杂度。两者均应按需使用,确保设计灵活性。代码示例展示了C++中如何实现这两种模式。
41 3
|
4月前
|
设计模式 安全 C++
C++一分钟之-C++中的设计模式:单例模式
【7月更文挑战第13天】单例模式确保类只有一个实例,提供全局访问。C++中的实现涉及线程安全和生命周期管理。基础实现使用静态成员,但在多线程环境下可能导致多个实例。为解决此问题,采用双重检查锁定和`std::mutex`保证安全。使用`std::unique_ptr`管理生命周期,防止析构异常和内存泄漏。理解和正确应用单例模式能提升软件的效率与可维护性。
45 2
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。

热门文章

最新文章