対象工場(1)---万悪のスウィッチにさよなら

4562 ワード

システムに抽象ベースクラスに多くの特定のサブクラスが存在する場合、単純で実用的なポリシーは、オブジェクトを作成する論理をファクトリメソッドにカプセル化することです.これにより、クライアントコードに影響を及ぼさずに特定のサブクラスを拡張することができる.
しかし、低品質の実装(例えば、次のコードのように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を追加したいと思っていますが、何をする必要がありますか?
  • Rectangleクラスを実装する(これはいずれの解法にも必要なステップである)
  • 修正shape_types.h,Rectangleに一意のrectangle_を追加するtype 
  • ShapeFactory::CreateShape()インタフェースの実装を修正し、新しいcase
  • を追加
  • おめでとうございます.やっとシステムにグラフィックサブクラス
  • を拡張しました.
    このような拡張コストは明らかに多くの文句を言うプログラム猿(媛)を満足させることは難しいが、最大の問題はプログラム設計原則(開閉原則)に違反していることだ.はい、コードの中で万悪のswitchに宣戦布告する時だ.
    関数ポインタは、typeから関数ポインタへのインデックスを導入することで、switch文を削除することができます.このインデックスはここでmapを選択しました.この例ではvectorがより良い選択だと思っている人もいるかもしれませんが、vectorには連続的な下付きが必要だと思います.検索速度に問題があります(無視できないほど多くのサブクラスが存在する可能性はありませんが).強化版のShapeFactoryを見てみましょう.
    class ShapeFactory {
    public:
      typedef Shape *(*CreateFn)();
    private:
      typedef std::map 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が失敗することを防止するために、Register Shapeにbool値を返し、Registerが失敗したときにfalseに戻って呼び出し者に通知します.
    我々はすべての職責をある集中点(switch文)から各特定のクラスにエスケープし、各カテゴリに工場を登録する必要があります.新しいShape派生クラスを定義するには、古いファイルを「修正」することなく、新しいファイルを「追加」する必要があります.
    テストコードを添付します.
    #include "shape.h"
    #include "circle.h"
    #include "line.h"
    #include "drawing.h"
    #include 
    
    ShapeFactory g_factory;
    
    Shape *CreateLine() {
      return new Line();
    }
    
    Shape *CreateCircle() {
      return new Circle();
    }
    
    template
    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文にさよならを言うことができます.
    自分のレベルが限られていることを考えると、文章の中に間違いがあるのは避けられないので、皆さんの指摘を歓迎します.皆さんも積極的にメッセージを残して、筆者と一緒にプログラミングのことを議論してほしいです.