Entry

プログラミングメモ - フラグを使わない実装パターンとか

2011年11月29日

久しぶりの実装ネタ。パターンとかいう程の話ではないんだけれども。

どうしてかについてはいろいろと理由があるのだけれども,一般に,C++ のような高機能/高水準な言語で boolean 型のフラグを多用しているプログラムはへたっぴだと言われています。boolean 型というのは true/false の値(真偽値)を持つ型のこと。ここで,ロジック上真偽値として使われていれば,実装時の型は整数型でもなんでも構いません。具体的には次のような関数を作るのは,へたっぴということ。

void some_func(bool is_good) {
  if (is_good) {
    /* good な状態の処理 */
  } else {
    /* no good な状態の処理 */
  }
}

こゆ処理の最もよくないところは,呼び出し側で何を設定しているか分からないところです。例えば,上の処理は,次のような呼び出しになるけれども,ここで true が何を表しているのか分からない。結局,保守する人は some_func の実装やマニュアルを読まなきゃいけません。

some_func(true);

理想は,呼び出し側でどのような意味の値を渡しているのかが分かるといい。例えば,次のようになっていれば,おおむね何をやっているかが分かります。

bool good = true;
some_func(good);

しかし,これもあまり上手くない。どうしてかというと,変数名として good を使ったのはいいけれども,これも所詮は boolean 型の値に過ぎないからです。変数 good には,good としての役割と機能があるはずなんだけれども,こうした意味はまったく引数として盛り込まれません。極端な話,以下のように書いても,some_func は good として振舞います。

bool bad = true;
some_func(bad);

もうひとつ,関数 some_func の実装で,if-文の分岐があるのもよくありません。ここでは boolean 値をひとつしか渡してないけれども,2つ渡したら4パターン,3つ渡したら8パターンといった具合に,考慮すべき状態が増えてしまいます。状態が増えるに従って,フラグの数を増やしていくと,場合の数が指数関数的に増加することになります。これはよくない。

こゆ問題を解決する方法はいくつかあるけれども,一番お手軽な方法は,if-文の中身を他のクラスなり関数(ポインタ)なりに委譲して,状態に対する振る舞いごとパラメタ化してしまうことです。例えば,上の例は,次のように書ける。

struct health_t {
  virtual void do() = 0;
}
struct good : public health_t {
  void do() { /* good の処理 */ };
}
struct bad : public health_t {
  void do() { /* bad の処理 */ };
}

void some_func(const health_t& health) {
  health.do();
}

こうすれば,呼び出しは次のようになる。if-文もなくなります。

some_func(good());

ポイントは,状態を「値」から「型」に昇格させた点にあります。型だから,状態に対する振る舞いを定義することができる。また,ここでは動的に状態を変更している(仮想関数を使って実行時に判断している)けれども,あらかじめ静的に状態が決まっているなら,テンプレートを使ってもいい。

少しだけ応用として,以下のようなサンプルを書いてみました。よくあるパラメタの定義と設定,それにチェックの処理です。よく見るプログラムでは,パラメタ定義を動的に作っているものもあるけれど,パラメタ定義はあらかじめ決まっていることが多いから,静的に決定することができます(だからテンプレートを使う)。ここでは,パラメタの属性について,必須パラメタか任意のパラメタかの状態を設定できるようにしてみました。

#include <iostream>
#include <stdexcept>
#include <string>

////////////////////////////////////////////////////////////////////////////////

template<bool> struct is_required_type {};

template<>
struct is_required_type<true> {
  static bool validate(bool is_set) {
    return is_set;
  }
};
template<>
struct is_required_type<false> {
  static bool validate(bool /* is_set */) {
    return true;
  }
};

typedef is_required_type<true>  required_args;
typedef is_required_type<false> optional_args;

////////////////////////////////////////////////////////////////////////////////

template<typename isRequiredT>
class parameter_type {
public:
  parameter_type() {}
  virtual ~parameter_type() {}
public:
  void set_value(const std::string& name) {
    name_ = name;
  }
  void validate() const {
    if (!isRequiredT::validate(!name_.empty())) {
      throw std::runtime_error("parameter is not set");
    }
  }
private:
  std::string name_;
};

////////////////////////////////////////////////////////////////////////////////

int
main(int argc, char* argv[]) {
  /* パラメタのスキーマ定義 */
  parameter_type<optional_args> p1;  // p1 は任意に値を設定できる値
  parameter_type<required_args> p2;  // p2 は値を設定するのが必須のパラメタ

  /* 値を設定する */
  p2.set_value("foo");  // required

  /* ちゃんと設定されているかチェックする */
  try {
    p1.validate();
    std::cout << "1 ok" << std::endl;
    p2.validate();
    std::cout << "2 ok" << std::endl;
  } catch (const std::runtime_error& e) {
    std::cout << "ng" << std::endl;
    std::cerr << e.what() << std::endl;
  }

  return 0;
}

なお,ここでは,parameter_type::validate() の中で if-文が表れているけれども,ここで throw するようにしたために,判断と振る舞いが分かれてしまっているからです。判断と振る舞いを統合する static なtraits を導入すれば,if-文はなくなります。

面倒そうに見えるけれど,拡張するときの手間は,単純なフラグを使うよりこちらの方が少ないと思います。

ま,ただそれだけ。

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