对象工厂(1)---和万恶的 switch 说再见

当系统中存在某抽象基类中有很多具体子类,一个简单实用的策略是创建对象的逻辑封装到一个工厂方法中。这样,可以在不影响客户端代码的情况下扩展具体子类。

但是一个低质量的实现(比如像下面的代码,使用了 switch 语句),会导致编译的高耦合以及扩展的高成本,通过阅读 《modern c++ design》一书,看到了一个比较优雅的解决方法。

现在假设我们要实现一个图形管理系统,其中 Shape 是抽象基类,声明如下:

class Shape {
public:
  virtual void Save(std::ofstream &out_file) = 0;
  virtual void Read(std::ifstream &in_file) = 0;
  virtual ~Shape() { }
};

Shape::Save() 接口将图形存储到本地文件中(其实该接口是一个不好的设计,其参数应该是一个可写入对象即可,无需是一个ofstream)。Shape::Read() 接口从文件中恢复出图形中的所有信息。存储图形的策略是在文件头部存入一个 int ,代表图形的类型,ShapeFactory 负责通过这个 type 来创建适当的 Shape。

Drawing 类负责将一个 Shape 对象存储到本地文件或者从文件中恢复出来。其声明如下:

#include "shape.h"

class Drawing {
public:
  Drawing(Shape *p) : p_shape_(p) { }
  void Save(std::ofstream &out_file);
  Shape *Load(std::ifstream &in_file);
private:
  Shape *p_shape_;
};

一个直观的 ShapeFactory 实现可能如下:

#include "shape_types.h"

class ShapeFactory {
public:
  Shape *CreateShape(int type) {
    switch (tyep) {
    case line_type:
      return new Line();
    case circle_type:
      return new Circle();
    default:
      throw std::runtime_error("Unknown type");
    }
  }
};

各种代表子类的 type 定义于 shape_types.h 头文件中。但是这样的实现引入了 switch 语句,其让系统的扩充变得举步维艰。试想我们现在想为系统中加入一个新的子类 Rectangle,我们需要做什么?

  1. 实现 Rectangle 类(这是任何一个解法都必须的步骤)
  2. 修改 shape_types.h, 为 Rectangle 在其中添加一个独一无而的 rectangle_type 
  3. 修改 ShapeFactory::CreateShape() 接口的实现,加入新的 case
  4. 恭喜,你总算为你的系统扩展了一个图形子类

这样的扩展成本显然是难以让众多挑剔的程序猿(媛)满意的,但是最大的问题是其违反了程序设计原则(开闭原则),是的,现在是时候向代码中万恶的 switch 宣战了。

函数指针可以成为我们的得力臂助,通过引入一个从 type 到函数指针的索引,我们可以消除 switch 语句,这个索引在这里我们选择了 map,在这个例子中可能有人会觉得 vector 是个更好的选择,但是我觉得 vector 需要连续的下标,并且在查找速度上有问题(虽然一个系统不太可能存在数量多到无法忽视的子类)。让我们来看加强版的 ShapeFactory:

class ShapeFactory {
public:
  typedef Shape *(*CreateFn)();
private:
  typedef std::map<int, CreateFn> CreateFnMap;
public:
  bool RegisterShape(int shape_id, CreateFn);
  bool UnregisterShape(int shape_id);
  Shape *CreateShape(int shape_id) const;
private:
  CreateFnMap fn_map_;
};

通过 RegisterShape() 和 UnregisterShape() 实现动态添加/删除系统中支持的子类。而最终,每个具体子类的创建逻辑都放在了单独的 CreateFn 中。其可能是类似下面的简单代码:

Shape *CreatLine() {
  return new Line();
}

也可以是包含大量复杂逻辑的创建函数(当然,这里可以通过把 CreateFn 的类型改为 std::function 提供更多的扩展性)。ShapeFactory 的具体实现比较直白:

#include "shape.h"

bool ShapeFactory::RegisterShape(int shape_id, CreateFn fn) {
  return fn_map_.insert(std::make_pair(shape_id, fn)).second;
}

bool ShapeFactory::UnregisterShape(int shape_id) {
  return fn_map_.erase(shape_id) == 1;
}

Shape *ShapeFactory::CreateShape(int shape_id) const {
  auto it = fn_map_.find(shape_id);
  if (it == fn_map_.end()) {
    throw std::runtime_error("Unknown Shape ID");
  }
  return (it->second)();
}

现在每个 class 之间做到了隔绝,每个图形的 type 可以不需要保存在一个公共的头文件中,为了防止不同的图形类型的 type 重复,导致 Register 失败,我们还体贴的为 RegisterShape 返回一个 bool 值,在 Register 失败的时候会返回 false 来通知调用者。

我们将所有的职责从某个集中点(switch语句)转义到了每个具体类中,它要求为每个类别对工厂进行注册。如果要定义新的 Shape 派生类,我们现在只需要“增加”新文件,而不必“修改”旧文件。

附上测试代码:

#include "shape.h"
#include "circle.h"
#include "line.h"
#include "drawing.h"
#include <fstream>

ShapeFactory g_factory;

Shape *CreateLine() {
  return new Line();
}

Shape *CreateCircle() {
  return new Circle();
}

template<class S>
void Test(S shape) {
  using namespace std;
  ofstream f("tmp");
  S s;
  Drawing dr(&s);
  dr.Save(f);
  f.close();
  ifstream f2("tmp");
  Shape *p = dr.Load(f2);
  delete p;
}

int main() {
  g_factory.RegisterShape(line_type, CreateLine);
  g_factory.RegisterShape(circle_type, CreateCircle);
  Test(Line());
  Test(Circle());
  g_factory.UnregisterShape(line_type);
  Test(Line());
}

输出:

Line::Read()
Circle::Read()
libc++abi.dylib: terminating with uncaught exception of type std::runtime_error: Unknown Shape ID
[1]    22666 abort      ./a.out

运行结果与预期完全一致,至此,我们可以在工厂函数中对 switch 语句说再见了。

鉴于自身水平有限,文章中有难免有些错误,欢迎大家指出。也希望大家可以积极留言,与笔者一起讨论编程的那些事。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值