對象工廠(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 語句說再見了。

鑑於自身水平有限,文章中有不免有些錯誤,歡迎你們指出。也但願你們能夠積極留言,與筆者一塊兒討論編程的那些事。指針

相關文章
相關標籤/搜索