首页 > 编程笔记 > C++笔记 阅读:13

C++外观模式及其实现(非常详细)

外观模式的主要目的是提供一个统一的接口,用于访问多个子系统。外观模式隐藏了子系统的复杂性,使客户端可以更简单地使用子系统。

外观模式通常包含一个外观类,该类对客户端与子系统的各种功能进行交互和协调。客户端通过调用外观类提供的方法来使用子系统的功能。

外观模式可以简化接口,客户端不需要了解子系统的复杂性和内部实现细节,只需通过外观类提供的简单接口便可以使用子系统的功能;解耦系统,外观模式将客户端与子系统解耦,使客户端与子系统之间的耦合度降低;提高可维护性,通过外观类封装子系统,当子系统内部发生变化时,只需修改外观类而不影响客户端。

外观模式通常用于当一个复杂子系统有多个接口且这些接口之间存在依赖关系时,可以使用外观模式对这些接口进行封装,提供了一个统一的接口给客户端使用;当客户端与子系统的交互较复杂时,使用外观模式可以简化客户端的调用过程;当需要对子系统进行重构时,外观模式可以扮演一个中间层的角色,减少重构对客户端的影响。

传统外观模式

外观模式的结构包括 3 个核心组件,即外观类(Facade)、客户端(Client)和子系统(SubSystem),如下图所示:


图 1 传统外观模式UML简图

C++11元编程下的结构设计

本问中外观模式的实现采用两种不同的方式实现:
以多重继承实现的结构如下图所示:


图 2 C++11多重继承实现的结构

通过可变模板参数递归展开以实现多重继承。多重继承可以不用考虑接口一致性,使用时接口方法可以很灵活地进行处理,但是使用的多重继承容易出现名字冲突,以及菱形继承等情况。在编译过程中使用模板展开的方式逐个检查所有子部件是否都是继承与给定的接口。

使用组合方式处理的情况如下图所示:


图 3 C++11模板组合方式处理的情况

多个子部件通过继承一个接口并实现接口函数。类内部使用 std::vector 记录不同子部件对象指针。使用组合方式需要所有的子部件都继承自一个接口,虽然接口访问相对来讲灵活性变差了,但避免了在多重继承中出现名字冲突等问题。

C++外观模式的实现和解析

代码主要分成 3 部分:
需要使用的 STL 模块如下:
#include<type_traits >
#include<functional >
两种实现类型的选择是在编译期完成的,选择了多重继承方式在最后的程序中就只有多重继承的方式,否则就只有组合方式。方式的选择是通过宏 FACADE_USE_INHERIT 实现类型的选择,当 FACADE_USE_INHERIT 为 1 时使用多重继承的方式,否则使用组合方式,在默认情况下使用组合方式。

FACADE_USE_INHERIT 宏的定义可以在自己的编译工程文件中配置,例如在 GCC 中通过添加 -DFACADE_USE_INHERIT=0 实现。这样在编译时就可使用组合方式实现,默认的宏定义的代码如下:
//designM/facade.hpp
#if !defined(FACADE_USE_INHERIT)
#  define FACADE_USE_INHERIT (0)
#endif

多重继承使用可变模板递归展开的方式使模板参数每展开一次继承一个子部件。在最后一次继承的时候实现一个 run() 函数,最终使用这个 run() 函数进行应用开发,这个 run() 函数就是外观模式对外暴露出来的处理接口,代码如下:
//designM/facade.hpp
#if FACADE_USE_INHERIT == 1
//定义外观模板类
template<typename retType,typename...subTypes > struct facade{};

//逐步展开模板参数,每次展开都对子部件执行一次继承操作
template<typename retType,typename subType1,typename...subTypes >
struct facade<retType,subType1,subTypes... >:
    public subType1,public facade<retType,subTypes...{};

最后一个展开的是在这个模板类中暴露出来的接口函数 run(),以模板函数的方式实现,Func_t 是回调函数类型,这个参数最终在业务代码中实现并明确其类型。func是回调函数,将这个接口暴露以方便在业务代码中进行使用和控制。run() 函数实际很简单,主要调用了回调函数,代码如下:
//designM/facade.hpp
template<typename retType >
struct facade<retType >
{
    //模板参数Func_t是回调函数的类型,Args是要传递给回调函数的参数类型
    //回调函数
    template<typename Func_t,typename...Args >
    retType run(Func_t func,Args&&...args)
    {
        //这里需要注意回调函数中的第1个参数是this,通过这个指针方便
        //在回调函数中使用facade< >模板类
        return func(this,std::forward<Args >(args)...);
    }
};
#else
这里的 run() 函数还可以修改为纯虚函数,具体的内容可以在业务程序中实现其算法。这个虚函数的方法在本节所解析的代码中并没有实现,读者可以根据自己的需要自行实现。

下面是以组合方式实现的代码部分,首先在 facade_private__ 名字空间中定义接口类型检查模块,这部分内容仅仅在编译期用到,接口检查元函数将用于在各个子系统类型展开时进行检查,判断是否是通过合法继承所来的子类型。使用 facade_private__ 命名空间约束的目的是提供一个封装,限制 TYPE_CHK_HELPER__ 仅用当前模块,代码如下:
//designM/facade.hpp
namespace facade_private__{
//定义接口检查元函数
    template< typename itfcType,typename...implType >
    struct TYPE_CHK_HELPER__{};
    //通过模板的递归展开,每次展开时执行一次类型检查操作
    template< typename itfcType,typename implType1,typename...implType >
    struct TYPE_CHK_HELPER__< itfcType,implType1,implType... >
    {
        //使用is_base_of检查继承关系
        static_assert(std::is_base_of< itfcType,implType1 >::value,"");
    };
    //结束递归操作
    template< typename itfcType >
    struct TYPE_CHK_HELPER__<itfcType > {};
}

使用宏来方便地定义接口,主要分为 3 个不同作用的宏,分别用于定义开始、定义接口和定义结束,其本质是利用宏提供一个方便的接口,以便定义一个结构体,并在这些结构体中声明若干个虚函数,代码如下:
//designM/facade.hpp

//定义接口开始
#define FACADE_START_DECLARE_SUB_ITFC(name)\
struct name{\
    virtual~name(){}
//定义接口结束
#define FACADE_END_DECLARE_SUB_ITFC };

上面的两个宏用来声明结构体和关闭结构体声明,代码如下:
FACADE_START_DECLARE_SUB_ITFC(stAbc)
宏展开后的代码如下:
struct stAbc{
virtual ~stAbc(){}

下面的宏用来声明接口函数。宏参数...是可变的宏参数,在宏定义体内可以使用 __VA_ARGS__ 进行展开,通过此方式可以方便地实现不同种参数的应用,代码如下:
#define FACADE_ADD_ITFC(RET,NAME,...)\
virtual RET NAME(__VA_ARGS__) = 0;

例如下面的使用方法:
FACADE_ADD_ITFC(int,setData,int,float)
宏展开后的代码如下:
virtual int setData(int,float) = 0;
具体可以参考后面的示例。虽然在模块中提供了以宏的方式定义接口的功能,但是读者仍然可以使用C++语言提供的本身类或者结构体定义的方法实现自己的接口。

模板类 facade 是外观模式的主体部分。模板参数 itfcType 是接口类型,subTypes 是针对接口类型的具体实现的子系统数据类型,代码如下:
//designM/facade.hpp
template< typename itfcType,typename...subTypes >
class facade
{
public:
    using itfc_t = typename std::remove_pointer< typename std::decay< itfcType >::type >::type;
    using iterator = typename std::vector< itfc_t * >::iterator;
public:

INIT_HELPER__ 是辅助子部件初始化的模块。通过模板递归逐步地展开 std::tuple,以便将子系统的对象指针保存到 std::vector 中,代码如下:
//designM/facade.hpp
//模板参数N用来控制递归过程,当N ==0时递归结束
template< int N,typename tupleParams >
struct INIT_HELPER__
{
    //使用静态函数init__()从std::tuple中读取内容,然后保存到std::vector中
    static void init__(std::vector< itfc_t * >&vec,tupleParams&t)
    {
        vec[N- 1] = std::get< N - 1 >(t);
        //递归调用以完成子系统展开
        INIT_HELPER__< N - 1,tupleParams >::init__(vec,t);
        }
    };
    //INIT_HELPER__递归的终止操作
    template< typename tupleParams >
    struct INIT_HELPER__<0,tupleParams >{
        static void init__(std::vector< itfc_t * >&,tupleParams&){}
    };
protected:
    std::vector< itfc_t * > m_subs__;

m_chker__[0] 是利用 0 长数组不分配内存的特点,在编译的过程中会逐步展开所有的 subTypes,并在展开的过程里逐一检查每个子系统的继承关系,如此设计既能够保证编译期的检查任务,又不至于在运行期增加 CPU 和内存的负担,代码如下:
//designM/facade.hpp
facade_private__::TYPE_CHK_HELPER__<itfc_t,subTypes... > m_chker__[0];
public:
    facade(){}
    facade(std::vector< itfc_t * > subs):m_subs__(subs){}

利用 std::tuple 先记录可变参数,然后利用 INIT_HELPER__ 模板将可变参数展开到 std::vector 中。通过这种方式能够容易地实现和子系统数量相同参数数量的构造函数。

sizeof...(subTypes) 用于获取子系统的数量,这个参数在 INIT_HELPER__ 中需要用到控制递归展开的次数,代码如下:
//designM/facade.hpp
facade(subTypes * ...subs):m_subs__(sizeof...(subTypes))
{
    std::tuple< subTypes * ... > t = std::make_tuple(subs...);
    INIT_HELPER__< sizeof...(subTypes),decltype(t) >::init__(m_subs__,t);
}
virtual~facade(){}

下面实现子系统的增删操作,用来在运行中灵活地进行控制。在 erase() 函数中使用了 iterator 迭代器,相关获取迭代的代码没有列出来,读者可以参考具体的代码内容,代码如下:
//designM/facade.hpp
template< typename tSubType >
void add(tSubType * obj){
    facade_private__::TYPE_CHK_HELPER__<itfc_t,subType >
    m_chker__[0];
    m_subs__.push_back(obj);
}
void erase(iterator it){
    m_subs__.erase(it);
}
void erase(iterator b,iterator e){
    m_subs__.erase(b,e);
}

run() 函数用来提供给外部业务代码使用,也就对应外观模式的外观两个字。run() 函数支持灵活的回调函数配置和灵活的参数传入,分别使用模板参数 Func_t 和 Params 在编译期明确,代码如下:
//designM/facade.hpp
    template< typename Func_t,typename...Params >
    void run(Func_t fun,Params&&...args)
    {
        for(int i = 0;i< m_subs__.size();i ++){
             if(m_subs__[i]! = nullptr){
                 fun(m_subs__[i],std::forward<Params >(args)...);
             }
        }
    }
};
#endif
组合方式需要针对所有的子系统遍历并传递给外部进行处理。通过遍历才能分别访问所有的对象,并且存在固定的先后顺序。这一点和多重继承的方式有明显的区别。

两种实现方式各有利弊,采用模板递归继承的方法在编译期的任务多于使用组合方式,在运行期初始化时不需要经过逐步初始化动作,但是因为使用了多重继承,所以容易造成名字冲突,以及多重继承的情况;多重继承后调用 run() 函数时第 1 个参数需要传入对象指针以方便使用外观对象,这是因为外观对象继承了多个不同的子系统而这些子系统在处理方法中可能要用到。在组合方式实现的版本中则直接传入了子系统对象的指针。

C++外观模式应用示例

根据外观模式的实现情况,下面分别使用两种不同的模式演示具体应如何使用外观模式模块。

在下面的示例程序中使用多重继承的方式结合了两个类(SubSystem1、SubSystem2)进行了实例化。在 main() 函数中创建了一个 MathFacade 对象,然后使用 run() 函数来执行操作,需要定义头文件和宏变量,代码如下:
//facade.cpp
#include<functional >
#include<iostream >
//多重继承通过宏FACADE_USE_INHERIT控制
#define FACADE_USE_INHERIT 1
#include"designM/facade.hpp"

using namespace wheels;
using namespace dm;

定义子系统类 SubSystem1 和 SubSystem2,在第 1 个子系统类中实现了加法;在第 2 个子系统类中实现了乘法操作,代码如下:
//facade.cpp
struct SubSystem1
{
    int add(int a,int b){
        std::cout<< "add result:"<< a + b<< std::endl;
        return a + b;
    }
};

struct SubSystem2
{
    int multiply(int a,int b)  {
        std::cout<< "multiply result:"<< a * b<< std::endl;
        return a * b;
    }
};

使用模板特化的方式创建外观类,第 1 个模板参数是接口的返回值,SubSystem1 和 SubSystem2 分别是两个子系统,代码如下:
//facade.cpp
using MathFacade = facade<int,SubSystem1,SubSystem2 >;
int main()
{
    //创建外观类对象
    MathFacade fac;

使用外观类对象调用子系统接口,先执行加法,后执行乘法,最后将两个结果相加并返给外部程序,代码如下:
    //facade.cpp
    int result1 = fac.run([](facade<int > * f,int a,int b)->int{
        MathFacade * f1 = static_cast< MathFacade * >(f);
        int a1 = f1->add(a,b);
        int a2 = f1->multiply(a,b);
        return a1 + a2;
    },2,3);
    //输出结果
    std::cout<< "final Result:"<< result1<< std::endl;
    return 0;
}
上面程序的运行结果如下:

add result:5
multiply result:6
final Result:11


下面的示例使用接口继承的方式实现,并利用 FACADE_START_DECLARE_SUB_ITFC 系列宏定义接口,需要的头文件如下:
//facade2.cpp
#include<iostream >
#include<functional >
#include<tuple >
#include<type_traits >
#include<vector >

#include"designM/facade.hpp"
using namespace wheels;
using namespace dm;

定义子接口和子接口实现类,接口的名字是 MySubInterface,其中分别定于两个接口,即 subInterfaceFunction 和 subInterfaceFloat,用于声明接口的代码如下:
FACADE_START_DECLARE_SUB_ITFC(MySubInterface)
    FACADE_ADD_ITFC(void,subInterfaceFunction,int);
    FACADE_ADD_ITFC(void,subInterfaceFloat,int,float);
FACADE_END_DECLARE_SUB_ITFC()

将上面的宏定义展开后的代码如下:
struct MySubInterface{
    virtual~MySubInterface(){}
    virtual void subInterfaceFunction(int) = 0;
    virtual void subInterfaceFloat(int,float) = 0;
};

接下来对接口分别实现了两个子系统 MySubInterfaceAImpl 和 MySubInterfaceBImpl,这两个子系统必须从 MySubInterface 继承并实现所有的接口,代码如下:
//子接口实现类 A
struct MySubInterfaceAImpl : public MySubInterface
{
    virtual void subInterfaceFunction(int arg) override
    {
        std::cout << "MySubInterfaceAImpl::subInterfaceFunction "
                  << "called with argument: "
                  << arg << std::endl;
    }
    virtual void subInterfaceFloat(int a, float b) override
    {
        std::cout << "MySubInterfaceAImpl::subInterfaceFloat: "
                  << a << "," << b << std::endl;
    }
};

struct MySubInterfaceBImpl : public MySubInterface
{
    virtual void subInterfaceFunction(int arg) override
    {
        std::cout << "MySubInterfaceBImpl::subInterfaceFunction "
                  << "called with argument: "
                  << arg << std::endl;
    }

    virtual void subInterfaceFloat(int a, float b) override
    {
        std::cout << "MySubInterfaceBImpl::subInterfaceFloat: "
                  << a << "," << b << std::endl;
    }
};

在 main() 函数中分别实例化了两个子系统对象 subInterfaceA、subInterfaceB 和一个外观对象 facade,并且在外观对象中添加了两个子对象的指针。在 run() 函数中通过回调函数实际使用了 Lambda 函数,分别执行两个子系统对象接口方法,代码如下:
int main()
{
    //创建子接口实现类的对象
    MySubInterfaceAImpl subInterfaceA;
    MySubInterfaceBImpl subInterfaceB;

    //创建外观对象,并将子接口实现类的对象传入
    facade<MySubInterface,MySubInterfaceAImpl,MySubInterfaceBImpl >
       facade(&subInterfaceA,&subInterfaceB);

    auto fun = [](MySubInterface * arg,int a,float b){
        arg->subInterfaceFunction(a);
        arg->subInterfaceFloat(a,b);
    };
    //调用外观对象的run函数,并传入执行函数和参数
    facade.run(fun,123,13.4);
    return 0;
}
上述代码的运行结果如下:

MySubInterfaceAImpl::subInterfaceFunction called with argument:123
MySubInterfaceAImpl::subInterfaceFloat:123,13.4
MySubInterfaceBImpl::subInterfaceFunction called with argument:123
MySubInterfaceBImpl::subInterfaceFloat:123,13.4

相关文章