Entry

「コードを共通化するために継承しよう」なんて寝言は寝て言えとゆ話

2012年03月13日

こちらを読ませていただいて。

「共通化 → 継承」という考えは間違っている、と断言する。こんなものはオブジェクト指向でも何でもない。仮想関数を持たないスーパークラスは要注意だ。

「共通化 → 継承」という誤った考え

これはまさしくその通りで,「共通化 → 継承」のような説明がいまだに堂々とまかり通ってるのには,どうしたもんだろうと思う。それなりに人気のある書籍ですら,そんなこと書いてることがある。

思うのだけれども,「共通化 → 継承」といった発想が産まれる原因は,「差分プログラミング」の実装手段として継承を位置付けてるからなのだと思う。もちろん,差分プログラミングという概念自体,発想としてアリなのは言うまでもありません。同じ処理をあちこちに書かないことは,プログラマの美徳にすらなっている。しかし,それはあくまでも発想であって,具体的な実装手段を伴うものではありません。この点,そこに(実装)継承という実装手段をあてがったバカタレは,ほんとに罪深いと思う。委譲で済むなら委譲しなさいとなぜ書かない。

大体もって,「処理を抽象化すること」と「共通した処理を見つけること」は同じことではない。というのも,引用コメント欄でも言及されている通り,あるクラスを任意の継承ツリーに組み込む場合,それが「なぜ共通なのか」を考えないといけないからです。処理が共通でも継承ツリーに組み込めない場合がある。そして,この「なぜ共通なのか」を考えるにあたっては,差分プログラミングなんぞといった概念とはほとんど関係がなかったりします(というか,差分プログラミングを考える必要はない)。これを考えるには,継承の性質だけ考えれば十分です。

ある二つ以上のオブジェクトを継承関係でもって表現できるのか判断する基準は,よく言われている通り is-a の関係にある場合です。例えば,Cat is a Mammal だから Mammal クラスを継承して Cat クラスを実装できるといった具合。抽象的にオブジェクトの関係を考える限り,継承関係の説明はこれで必要十分です。

一方,現実の具体的な実装工程において,Cat クラスの基底として Mammal クラスを作るべきか,Mammal クラスから Cat クラスを派生させるべきかは,別の考慮が必要です。

Cat is a Mammal と抽象的に言えるのはそうとして,具体的な実装では,「クラス Cat が Mammal としての振る舞いを他から要求されているか」が決定的に重要な判断要素になります。だれも Mammal として Cat を扱わないのに,Cat が(現実世界において)Mammal であるという事実だけに基づいて継承関係を作っても意味がない。つまり,プログラマが Cat をより抽象的な概念である Mammal として扱う必要性が必要なわけです。抽象的な概念レベルにおける抽象化は,静的かつ客観的な関係として規定/記述できるわけだけれども,プログラマの抽象化にははっきりとした具体的な意図があります。抽象化することで具体的なメリットを得られるから,プログラマは継承関係を作るわけです。

Cat と Mammal の話はあまりにもしょーもない例なので,もう少し現実的な例を挙げましょう。

例えば,画像を加工するソフトを作るときに必要なフィルタクラス(ぼかしたりエッジを強調したりするクラス)を考えましょう。いわゆる線形フィルタ(linear filter)の場合,窓の定義が変わるだけで適用方法はまったく変わりません。共通した処理になります。この場合の実装方法として,次のような方法が考えられます。

class image_t;  // 画像クラス
class mask_t;   // 線形窓クラス

/** フィルタの基底クラス */
class linear_filter {
public:
  /**
   * I/F となる仮想関数
   */
  virtual void apply(image_t& img) = 0;
protected:
  /** 内部で共通して呼ばれる関数
   *  画像を走査して窓を適用する。
   */
  void scan_forward(image_t& img, const mask_t& mask);
};

/** ラプラシアン */
class laplacian_filter : public linear_filter {
public:  // override
  void apply(image_t& img);
private:
  /** ラプラシアンのマスク */
  static const mask_t mask_;
};

/** ガウシアン */
class gaussian_filter : public linear_filter {
public:  // override
  void apply(image_t& img);
private:
  /** ガウシアンのマスク */
  static const mask_t mask_;
};

一方で,同じ処理を次のように委譲して書くこともできる。

class image_t;  // 画像クラス
class mask_t;   // 線形窓クラス

/** 画像を順次走査して窓を適用するクラス */
class linear_scanner {
public:
  void operator()(image_t& img, const mask_t& mask);
};

/** ラプラシアン */
class laplacian_filter {
public:
  // 関数内で linear_scanner オブジェクトを作り順次適用する
  void apply(image_t& img);
private:
  /** ラプラシアンのマスク */
  static const mask_t mask_;
};

/** ガウシアン */
class gaussian_filter {
public:
  // 関数内で linear_scanner オブジェクトを作り順次適用する
  void apply(image_t& img);
private:
  /** ガウシアンのマスク */
  static const mask_t mask_;
};

どちらがいいのか。これだけでは何とも言えません。しかし,先にも述べた通り,継承ツリーを作る場合は,基底クラスである linear_filter が linear_filter として扱える状況がなければ,作る意味がありません。例えば,次のような場合は作る意味がある。

  • 将来的にサポートするフィルタを増やす可能性がある。
  • エンドユーザの入力から動的にフィルタを生成したい。
  • ライブラリとして公開している場合,ユーザが独自のマスクを定義できるようにしたい。
  • フィルタをプラグインとして動的にアプリケーションに組み込めるようにしたい。

そのような状況がなければ,一般に基底クラスを作る意味はない。意味があるのかないのか,それはプログラマが具体的な目的を持って決めることです。差分プログラミングも関係なければ,共通してるから継承とかいったヘンチクリンな話も出てこない。

また,C++ の話になってしまうけれども,意味がある云々のほかに考えることとして,安易に継承関係を作ると,不必要に実装を公開する必要が出てきてしまう問題があります(これはテンプレートの場合も同じ)。上の委譲の例を見れば分かるけれども,各フィルタクラスの定義には,linear_scanner クラスが現れません。つまり,画像内を走査する処理を実装ファイル内でローカルに扱うことができる(linear_scan を公開ヘッダにしなくてもいい)ということです。将来的にこの走査クラスを差し替える場合も,影響を最小限にすることができます。継承の場合は派生クラスをコンパイルするのに基底クラスの定義が必要なので,こうはいかない。

つらつら書いたけれども,結局のところ言いたいのは,「コードを共通化するために継承しよう」なんて寝言は寝て言えとゆこと。ま,ただそれだけ。

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