设计模式之桥梁模式

2022-06-21 15:35:48 浏览数 (1)

也许你在很多的代码中见过成员叫做_pImpl或者xxxImpl, 然后一个方法的实现为如下代码所示,比如方法为DoSomething中调用了成员的同名方法DoSomething()(有时候也是会有额外的一些处理代码),这个时候可能就是使用了设计模式之桥梁模式Bridge Pattern)。

代码语言:javascript复制
XXXImpl* YourClass::impl() const
{
  return _pImpl;
}
void YourClass::DoSomething() const
{
  return _pImpl->DoSomething();
}

桥梁模式

维基百科的定义为如下所示,即将抽象和实现解耦。

decouple an abstraction from its implementation so that the two can vary independently

光看这一句话还是比较晦涩的,难道说的抽象与实现解耦就是这样子? AbstractInterface 是抽象的接口,其子类ClassAClassB继承后实现的具体类。很多时候这样的抽象-实现是足够应付一些场景的,但是桥梁模式不仅仅是这一层的抽象与实现,而是会将AbstractInterfaceClassAClassB中的实现做进一步的功能剥离。为什么需要做进一步的功能剥离呢?

我们先来看看桥梁模式的类图:

现在的类图比之前的类图中AbstractInterface 多出了一个聚合(Aggregation)关系的AbstractImplementor, 这个就是桥梁模式的关键之处,这个桥梁就是AbstractInterface中聚合的m_pImpl. 为什么要多出这一部分呢?但是有时候对于DoSomething的实现可能也会发生变化,可以有多种实现,比如DoSomething实现的是数据保存操作,那么有可能对于sqlite, SQLServer或者Mysql的实现都是不同的。而将实现部分剥离出来AbstractImplementor有助于对实现部分的变化做扩展, 比如有新的实现可以重新继承AbstractImplementor。而这个实现部分,可以在运行的时候比如根据你的配置文件或者运行时候的逻辑去选择特定的AbstractImplementor, 上图我们通过的是AbstractInterface 的构造函数参数去传递你所需要的AbstractImplementor对象。这也就是本人所理解的桥梁模式的意义。

所以桥梁模式应该会比较常见于可能发生变化的一些实现,比如队列,数据库操作,或者数据传输(比如Poco库的socket封装)等等。个人认为将实现部分剥离出来也是比较契合单一职责原则的。

例子

这里举个例子来理解桥梁设计模式。有一个接口想实现如下: 搜索数据库员工表中,员工年龄最大的5个人。比如SQLServer返回的数量使用TOP关键字,而MySQL SQL语句返回的数量使用LIMIT关键字,实现会有差异。桥梁模式对这个应用场景的实现的类图如下:

其中EmployeeSearcherLog相对于继承的父类EmployeeSearcher对方法SearchTop5OldestEmployee增加了一个输出employee信息数据功能。

实现的代码如下所示:

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <string>
#include <memory>

class Employee
{
public:
  unsigned int GetAge() const { return m_age; };
  const std::string& GetName() const { return m_name; };
private:
  unsigned int m_age = 0;
  std::string m_name = "Unknown Name";
};


class EmployeeSearcherImplementor{
public:
  virtual std::vector<Employee> SearchTop5OldestEmployee() = 0;
};

class EmployeeSearcherImplementorMySQL : public EmployeeSearcherImplementor 
{
public:
  virtual std::vector<Employee> SearchTop5OldestEmployee()
  {
    std::cout << "EmployeeSearcherImplementorMySQL::SearchTop5OldestEmployee" << std::endl;
    std::string sql = "SELECT name, age FROM employee ORDER BY age DESC LIMIT 5";
    std::vector<Employee> employees;
    //Get the employees from MYSQL
    //...
    return employees;
  }
};

class EmployeeSearcherImplementorSQLServer : public EmployeeSearcherImplementor 
{
public:
  virtual std::vector<Employee> SearchTop5OldestEmployee()
  {
    std::cout << "EmployeeSearcherImplementorSQLServer::SearchTop5OldestEmployee" << std::endl;
    std::string sql = "SELECT TOP 5 name, age FROM employee ORDER BY age DESC";
    std::vector<Employee> employees;
    //Get the employees from SQLServer
    //...
    return employees;
  }
};

class EmployeeSearcher
{
public:
  EmployeeSearcher(EmployeeSearcherImplementor* pImpl) : m_pImpl(pImpl) { };
  virtual ~EmployeeSearcher() { ; };
  EmployeeSearcherImplementor* Impl() const { return m_pImpl; };
  virtual std::vector<Employee> SearchTop5OldestEmployee()
  {
    std::cout << "EmployeeSearcher::SearchTop5OldestEmployee" << std::endl;
    return Impl()->SearchTop5OldestEmployee();
  }
private:
  EmployeeSearcherImplementor * m_pImpl;
};

class EmployeeSearcherLog : public EmployeeSearcher
{
public:
  EmployeeSearcherLog(EmployeeSearcherImplementor* pImpl) : EmployeeSearcher(pImpl) { };
  virtual ~EmployeeSearcherLog() { ; };
  virtual std::vector<Employee> SearchTop5OldestEmployee()
  {
    std::cout << "EmployeeSearcherLog::SearchTop5OldestEmployee" << std::endl;
    auto employees = EmployeeSearcher::SearchTop5OldestEmployee();
    std::cout << "Employ searched number: " << employees.size() << std::endl;
    for (auto&& employee : employees)
    {
      std::cout << "Employ name: " << employee.GetName() << std::endl;
      std::cout << "Employ age: " << employee.GetAge() << std::endl;
    }
    return employees;
  }
};

enum class SQLType
{
  SQLServer,
  MySQL
};

int main()
{
  ////This type you can read from config
  SQLType sqlType = SQLType::MySQL; 
  std::unique_ptr<EmployeeSearcherImplementor> pimpl;
  if (sqlType == SQLType::SQLServer)
    pimpl = std::make_unique<EmployeeSearcherImplementorSQLServer>();
  else if (sqlType == SQLType::MySQL)
    pimpl = std::make_unique<EmployeeSearcherImplementorMySQL>();

  if (!pimpl)
    return -1;
  
  std::unique_ptr<EmployeeSearcher> searcher = std::make_unique<EmployeeSearcherLog>(pimpl.get());
  auto employees = searcher->SearchTop5OldestEmployee();
  //Do something else
  //......

  return 0;
}

Pimpl风格的比较

在之前的一篇文章<<C 类库隐藏私有属性和方法的两种方式>>中提到Pimpl风格,是不是和桥梁模式有些类似。他们实现方式比较类似,但是也有重要的两点区别:

  1. 目的不同。Pimpl风格主要是为了减少编译的依赖,并且隐藏了类的实现细节。而桥梁模式考虑的是将实现类(XImplementor)独立出来,通过聚合的方式,让抽象类(XAbstraction)在运行时可以根据程序逻辑选择指定的实现类(XImplementor)对象,并利用其进行方法的具体实现,这样做也便于扩展不同的实现类(XImplementor的子类)。
  2. 使用方式不同。Pimpl模式中暴露给外部调用的类在编译阶段已经确定关联的实现类。而桥梁模式是抽象类(XAbstraction)可以在运行时候根据程序的逻辑选用实现类(XImplementor)。

总结

在写这篇文章的时候有几点小小的感悟:

  1. 当你看多了优秀开源软件的实现方式或者优秀的同事代码的时候,即使当时是不理解的,但是在你以后写代码的时候不自觉的就会受到正确的方法指引。这个和学习一个新的语言差不多(包括英语),先不一定要理解你所学习的语法甚至语义背后的原理,但当你你学多了说/写多了,自然而言也就掌握了。
  2. 很多编程语法一般人能够比较快的掌握,可是如何编制出优美的代码就需要不断的练习。这也有点像织毛衣,同样是几根编制针和毛线球,有的人能编制出美丽夺目的毛衣,有的人编出来甚至合身都很难。这些都是要在不断地练习和挫折中,领悟其技巧。
  3. 这篇文章的内容,比较简单。但是将其编写为文章的过程中却花费了不少时间: 思考和寻找桥梁模式应用,参阅书籍和文章以及编写文章。本人也想过,写一个看似自己已经明白的知识,同样的时间本也可以去学习更多的新知识,但为什么要这么做?因为之前看到过的费曼技巧有一条大致意思是: 教会别人才是真正懂得。

参考

  1. 秦小波的<<设计模式之禅>>
  2. Wikipedia: Bridge pattern
  3. <<When to use the Bridge pattern and how is it different from the Adapter pattern?>>: https://stackoverflow.com/questions/319728/when-to-use-the-bridge-pattern-and-how-is-it-different-from-the-adapter-pattern

0 人点赞