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

目录
打赏
0
0
1
0
229
分享
相关文章
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
44 0
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
112 0
【设计模式】【结构型模式】外观模式(Facde)
一、入门 什么是外观模式? 一种结构型设计模式,通过为子系统中的一组接口提供一个统一的高层接口(称为外观),来简化客户端与复杂子系统的交互过程。其本质是建立抽象层来隔离复杂度。 为什么要有外观模式?
89 9
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
114 12
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
98 16
|
4月前
|
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
237 6
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问