Self-Registering Objects


はじめに

複数のメソッドを入力データに基づいて切り替える場合、今まではファクトリパターンを適用して動的に切り替えできるようにしていました。ただこの方法はメソッド数が多いと大変な上、新しいメソッドを追加するたびにファクトリ内部の切り替え処理を書き換える必要があり、スケーラブルではありません。

何かいい方法ないかなと探していたところ、self-registrationという手法があることを知り、便利だなと思ったので紹介します。このパターンって有名なんでしょうかね。

参考サイト

Bartek's coding blog: Factory With Self-Registering Types
Programming Blog: Abstract Factory Step-by-Step Implementation in C++

連立一次方程式を解くソルバーを例としてパターンを適用します。

パターン適用前(ファクトリ+ビルダーパターン)

ソルバークラス

Solver.hpp
class Solver {
public:
  virtual Vector Solve(Matrix& A, Vector& b) const = 0;
  virtual ~Solver() {}
};
PartialPivLuSolver.hpp
#include "Solver.hpp"

class PartialPivLuSolver : public Solver {
public:
  virtual Vector Solve(Matrix& A, Vector& b) const override {
    // Ax = bを部分ピボット選択付きLU分解法で解く
    // ... //
    return x;
  }
  virtual ~PartialPivLuSolver() {}
};
FullPivLuSolver.hpp
#include "Solver.hpp"

class FullPivLuSolver : public Solver {
public:
  virtual Vector Solve(Matrix& A, Vector& b) const override {
    // Ax = bをフルピボット選択付きLU分解法で解く
    // ... //
    return x;
  }
  virtual ~FullPivLuSolver() {}
};

ソルバービルダークラス

SolverBuilder.hpp
#include <memory>
#include "Solver.hpp"

class SolverBuilder {
public:
  virtual std::unique_ptr<Solver> Create() const = 0;
  virtual ~SolverBuilder() {}
};

ソルバービルダーの具象クラスは今回の場合処理が共通しているのでテンプレート化しています。

SolverBuilderTemplate.hpp
#include "Solver.hpp"
#include "SolverBuilder.hpp"

template <typename T>
class SolverBuilderBase : public SolverBuilder {
public:
  virtual std::unique_ptr<Solver> Create() const override {
    return std::make_unique<T>();
  }
  virtual ~SolverBuilderBase() {}
};

ソルバーファクトリクラス

SolverFactory.hpp
#include <string>
#include "PartialPivLuSolver.hpp"
#include "FullPivLuSolver.hpp"
#include "SolverBuilderTemplate.hpp"

class SolverFactory {
public:
  std::unique_ptr<Solver> Create(const std::string& type) const {
    if (type == "PartialPivLu") {
      return SolverBuilderBase<PartialPivLuSolver>().Create();
    } else if (type == "FullPivLu") {
      return SolverBuilderBase<FullPivLuSolver>().Create();
    } else {
      // エラー処理
      // ... //
      return std::unique_ptr<Solver>(nullptr);
    }
  }
};

使い方

SolverFactory factory;
// std::string typeはファイル等から読み取ったとする。
auto solver = factory.Create(type);
// Matrix A, Vector b
auto x = solver->Solve(A, b);

パターン適用後(ファクトリ+ビルダー+シングルトン+Self-Registrationパターン)

ソルバークラスは同じなので省略します。

ソルバービルダークラス

SolverBuilderクラスは変わりません。SolverBuilderクラスの具象クラスのコンストラクタにおいて、自身をファクトリクラスに登録する処理を追加します。また各具象クラスを静的変数としてソースファイル内に記述します。こうすることで、具象クラスのインスタンスが作成されると自動的にソルバーファクトリへ登録されるようになります。

SolverBuilderTemplate.hpp
#include "Solver.hpp"
#include "SolverBuilder.hpp"
#include "SolverFactory.hpp"

template <typename T>
class SolverBuilderBase : public SolverBuilder {
public:
  // インスタンス作成時に自身をファクトリへ登録する
  SolverBuilderBase(const std::string& key) {
    SolverFactory::Instance().Register(key, this);
  }

  virtual std::unique_ptr<Solver> Create() const override {
    return std::make_unique<T>();
  }
  virtual ~SolverBuilderBase() {}
};
SolverBuilders.cpp
#include "SolverBuilderTemplate.hpp"

// 種類が多い場合はマクロを使う
static SolverBuilderBase<PartialPivLuSolver> partialPivLuSolverBuilder("PartialPivLu");
static SolverBuilderBase<FullPivLuSolver> fullPivLuSolverBuilder("FullPivLu");

ソルバーファクトリクラス

ソルバーファクトリクラスに各ソルバービルダーを登録する機能を追加します。

SolverFactory.hpp
#include <map>
#include <string>
#include "Solver.hpp"
#include "SolverBuilder.hpp"

class SolverFactory {
public:
  // SolverFactoryのインスタンスを返す(Singleton)
  static SolverFactory& Instance() {
    static SolverFactory factory;
    return factory;
  }

  // 各ソルバービルダークラスを登録する
  void Register(const std::string& key, SolverBuilder* builder) {
    auto it = builders_.find(key);
    if (it != builders_.end()) {
      // エラー処理
    }
    builders_[key] = builder;
  }

  // 登録されたポインタを基にソルバーを生成する
  std::unique_ptr<Solver> Create(const std::string& key) const {
    auto it = builders_.find(key);
    if (it == builders_.end()) {
      // エラー処理
    }
    return it->Create();
  }

private:
  // ソルバービルダークラスへのポインタを格納する
  std::map<std::string, *SolverBuilder> builders_;
};

SolverFactoryのインスタンスが確実に作成され初期化されること、スレッドセーフであることを保証するために、SolverFactory::Instance()の中でboost::serialization::singletonクラスを使用した方がいいかもしれません。
私のレベルだと参考サイトで述べられているように、これが確実な実装方法なのかわからない…。

[C++] boost::serializationで利用されているsingletonに関して
[C++] boost::serializationにおけるsingletonの内部の仕組み