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

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

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

如今假设咱们要实现一个图形管理系统,其中 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 派生类,咱们如今只须要“增长”新文件,而没必要“修改”旧文件。spa

附上测试代码:设计

#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 语句说再见了。

鉴于自身水平有限,文章中有不免有些错误,欢迎你们指出。也但愿你们能够积极留言,与笔者一块儿讨论编程的那些事。指针

相关文章
相关标签/搜索