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

鑑於自身水平有限,文章中有難免有些錯誤,歡迎大家指出。也希望大家可以積極留言,與筆者一起討論編程的那些事。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章