Entry

プログラミングメモ - 動的 Factory メソッドパターンとか

2011年12月08日

『Modern C++ Design』で紹介されていた,リフレクション的な Factory をちょっと実験してみました。

Modern C++ Design―ジェネリック・プログラミングおよびデザイン・パターンを利用するための究極のテンプレート活用術 (C++ In‐Depth Series)
Modern C++ Design
posted with amazlet at 11.12.08
アンドレイ アレキサンドレスク
ピアソンエデュケーション
売り上げランキング: 107026

Factory メソッドパターンは GoF のパターンで,やりたいことは new 演算子のオペランド(クラス名)を直接書きたくないから,別の方法で生成したいとゆもんでした。いわゆる ABC (Abstract Base Class)へのポインタを受け取ることで,派生クラスの生成と生成方法を分離することができます。

要するに,コードで書くなら次のようなことがしたいとゆこと。

int
main(int argc, char* argv[]) {
  // 例えば文字列を引数にして object 型を基底クラスにする
  // オブジェクトを生成する。
  std::unique_ptr<object> d(object::create("abc"));
  d->greeting();

  return 0;
}

ちょっと抽象的だけれども,実際は例えば,ユーザが入力したファイル名のうち,".png"とゆ拡張子文字列を受け取って,PNG ファイルを読み取るクラスのオブジェクトを返す,なんてことができます。

ここでは引数を文字列にしているけれども,キーになるもんだったらなんでもいい。また,Factory メソッドは Singleton で構成することもあるけれど,Singleton パターンはいろいろとアレなので,素直に基底クラスの静的関数にした方がいいと思う。

で,GoF を読めば分かるのだけれども,問題はこの Factory メソッドの実装で,どうしても汚い if-文が出てきてしまうのでした。これをなくしたいとゆのが,ここでの動機です。

if-文を取り除くのに『Modern C++ Design』では,キーとなるオブジェクトと派生クラスを生成する関数へのポインタを std::map に登録する方法を提案しています。ここでは,そのコンセプトを単純に実装しただけだけれども,ほんの少しだけ工夫してみました。

まず,ABC となる object 型。ヘッダはこんな感じ。

// object.h
#ifndef OBJECT_H
#define OBJECT_H

#include <string>
#include <map>

class object {
public:
  typedef object*                             (*creator_type)();
  typedef std::map<std::string, creator_type> creator_map_type;
public:
  object();
  object(const object&);
  virtual ~object();
public:
  object& operator=(const object&);
public:
  virtual void greeting() const = 0;
public:
  static object* create(const std::string& name);
  static void set_creator(const std::string& name, creator_type creator);
private:
  static creator_map_type creator_map_;
};

#endif  // OBJECT_H

キーと生成用の関数(へのポインタ)を登録する set_creator 関数と,実際にキーからオブジェクトを生成する create 関数を宣言しています。生成方法についてテンプレートベースでファンクタを取れるようにすると多少イケてるかもしれない。で,ABC の実装はこちら。

#include <object.h>
// object.cpp
/* static variable definition */
object::creator_map_type object::creator_map_;

object::object() {}

object::object(const object&) {}

object::~object() {}

object&
object::operator=(const object&) {
  return *this;
}

void
object::set_creator(const std::string& name, creator_type creator) {
  object::creator_map_.insert(std::pair<std::string, creator_type>(name, creator));
}

object*
object::create(const std::string& name) {
  creator_map_type::iterator itr = object::creator_map_.find(name);
  return (itr != object::creator_map_.end()) ? (itr->second)() : nullptr;
}

set_creator では std::map<std::string, creator_type> のオブジェクトに登録して,create 関数で実際に生成します。ま,これはそれほど難しくない。

一方,この方法を取った場合,生成用の関数を派生クラスに持たせておく必要はないのだけれども,ここでは派生クラスの静的関数として実装しました。もっともこの場合,派生クラス側でいちいち object* 型の戻りを返す関数(create)を書くのは面倒なので,ひとつライブラリ内部で使うテンプレートクラスをかませます。それがこちら。

#ifndef BASIC_OBJECT_H
#define BASIC_OBJECT_H

#include "object.h"

template<typename BaseT_, typename DerivedT_>
class basic_object : public BaseT_ {
public:
  typedef basic_object<BaseT_, DerivedT_> self_type;
  typedef BaseT_                          base_type;
  typedef DerivedT_                       derived_type;
  typedef derived_type*                   derived_pointer;
  typedef derived_type&                   derived_reference;
public:
  basic_object() {}
  basic_object(const self_type& other) : base_type(other) {}
  virtual ~basic_object() {}
public:
  self_type& operator=(self_type& rhs) {
    return BaseT_::operator=(rhs);
  }
public:
  static base_type* create() {
    return static_cast<base_type*>(new derived_type());
  }
};

#endif  // BASIC_OBJECT_H

ABC となる基底クラスと派生クラスの間に継承させる CRTP のクラスです。create 関数はこっちに集約しておけば,いちいち派生クラスで create 関数を作る必要がなくなります。CRTP なので,派生クラスはこんな風になる。

#ifndef DERIVED_H
#define DERIVED_H

#include "basic_object.h"

class object;

class derived : public basic_object<object, derived> {
public:
  derived();
  derived(const derived& other);
  virtual ~derived();
public:
  derived& operator=(const derived&);
public:
  void greeting() const;
};

#endif  // DERIVED_H

CRTP は派生クラスのみを基底のテンプレートクラスに渡すのが普通だけれども,そうすると,CRTP が継承するクラス(object)が固定されてしまいます。このように create 関数を継承ツリーにインジェクトするクラスは,他の継承ツリーでも使いまわしたいので,基底クラスもテンプレート引数として渡します。こうしておけば,派生クラスをテンプレートにして,基底クラスを切り替えることもできるようになります。

使うときは,こんな風に使う。テストなので,derived クラスが見えてしまっているけれども,main 関数2行目の登録はライブラリ内部に隠蔽するものなので,実際 derived クラスはライブラリ内でプライベートになります。

#include <iostream>
#include <memory>

#include "object.h"
#include "derived.h"

int
main(int argc, char* argv[]) {
  // この登録は外部で行う
  object::set_creator("abc", derived::create);
  // オブジェクトをもらう
  std::unique_ptr<object> d(object::create("abc"));
  d->greeting();

  return 0;
}

この設計で Factory メソッドから if-文がなくなりました。もっとも,ある利点を強調した設計は,それに基づく不利な点も踏まえておかなくちゃいけません。

この設計の場合,もっとも不利になるのは,クラス生成の関数とキーの組み合わせを動的に登録する必要がある点です。実行時にあらかじめ(プログラマからすれば)決まりきったバインディング(マッピング)を行うのは無駄です。静的にキーと生成関数のマップを定義できればいいんですけれど,なかなか難しいところ。生成時のコストを考えると,たくさんの派生クラスを登録すると,ややもたつくかもしれません。

一方,std::map の探索は O(log(n)) なので,十分スケーラビリティがあります。使いどころが難しいけれども,アプリケーション起動時にユーザオプションの設定を読み込む場合なんかはいいかもしれない。また,今回は ABC の外部から生成関数を登録できるようにしたので,拡張性がある程度あります。例えば,冒頭の画像読み込みの事例で,新たに TIFF 読み込みオプションを追加した場合なんかは,ライブラリの外側からユーザが TIFF 読み込みクラスを定義できます。同様に,DLL などから動的にバインドする仕組みも作れると思う。

実際にプログラム,特にライブラリを作る場合の注意点を挙げておくと,基底となる ABC のヘッダは公開しておいて(実装は非公開),受け渡しする派生クラスについてはヘッダも実装も非公開にしないと Factory メソッドパターンを利用する意味があまりありません。名前レベルで派生クラスがクライアントから見えてしまっていると,どちみち再コンパイルが必要になりがちで,内部実装を隠蔽しにくくなるからです。

それなりに応用範囲が考えられそうな感じ。ま,それだけ。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN