現実の構造を分析し、それをプログラムの構造にそのまま写すのが何故いけないか

わたしの以前のエントリー中の

例えば、カモノハシ本 の5章では、エイトクイーンパズルを解いていますが、これは

  1. Queen オブジェクト自体に「取られない位置に進む」「この位置を自分が攻撃できるか?を答える」という責務を持たせる
  2. Queen を各列に一づつ置く
  3. 端から順にQueen に「取られない位置に進め」をさせる。

という解き方をしています。各Queen は自らの位置の解を自ら解きます。

(中略)

Board というオブジェクトは必ずしも必要ないですし、連結リストの一番端には現実には存在しない「番兵」を置く場合もあります。なによりも、Queen の駒が現実で勝手に自分の攻撃されない位置を求めて動くなんてありません。(そんなチェス盤を開発してくれ、という要件ではないのです)

つまり、これは現実の写像ではありません。でも良いデザインです。

憂鬱本レビューの補足 - みねこあ

について、わからない人さん からの質問コメント

本記事にて,カモノハシ本の中のエイトクイーン問題でQueenオブジェクトのみで問題を解けており, Boardオブジェクトは必ずしも必要ないと書かれておりました.

(中略)

しかし,advancedメソッドの中と,main関数の中で,チェス板の縦横の長さが8としてハードコーディングされています.この,8という数字は,あくまでも現在のチェス板の大きさが決まっているのでハードコーディングできる話だと思うのですが,8という数字は,盤面の大きさを定義しているものであり盤面の大きさをQueenオブジェクトとmain関数が持つというのは,何か不自然な感じを受けました.

(中略)

「盤面の情報を提供する」(ここでは,8という値を提供する)という責任は,Boardオブジェクトが持つべき責任ではないか,と自分は考えております.

(中略)

自分は,現実写像型でのモデル作成をやってきましたが,現在は現実写像型のモデルについて肯定も否定もしない立場です.私は,モデルの良さはどうやって測ればよいのか,という疑問を持っておりまして,その疑問を解決したいと考えてご質問させて頂きました.

2008-05-04 - みねこあ

がありました。コメントで書こうとしたのですが、いつも通り長くてグダグダになってしまったので(文章書くの下手ですみません..orz)新たにエントリーをば。


質問を勝手に単純化すると

  • 盤面の大きさを得る為の Board オブジェクトがあっても良いのでは?
  • ...と言うことをオカズに、現実写像モデリングの是非について問いたい

というコトだと思います。


Board オブジェクトは必要か?

「8」というマジックナンバーQueen と main関数 に置くのが気持ち悪いというのは解ります。どこかに集めようとしたとき、現実世界では盤面が持っているから Board が持つ というのも一見自然な流れです。

ただし、Board は本当に「マスの数を知っている」という責務のためだけのオブジェクトになるので

class Board
{
public:
   int colSize(){ return 8; }
   int rowSize(){ return 8; }
};

こんなのです(縦横比固定なら、さらに1定数に)。さすがにこれは

  • 単なる2定数のホルダの為に 新たなオブジェクトを作るべきか
  • 単なる定数ホルダーに「Board」という抽象を与えるのは本当に適切なのか

と、思ってしまいます。正直「現実世界でもBoard はあるんだし」以外に、Board オブジェクトを設ける理由が見あたりません。


* * *


定数ホルダーとしての「Board」を設ける以外の、マジックナンバーの気持ち悪さの解消法を考えてみます。まず、8というマジックナンバーについて。盤面における列数と行数ですが、この8Queenパズルプログラムにおいては、列数は Queen の生成数、行数は Queen のadvance上限 でしかありません。

例えば N-Queen 対応等で、advance 上限を Queen クラス外に求める場合、1)解を探している間に変わったりしない、2)Queenの数と整合性を取るべき、を考えれば、Queen の 初期化時パラメータとして渡すのが適切でしょう。従って これら二つのパラメータが必要なのは 構築時(Queen インスタンス生成時)だけです。

となるとこれは「構築時のパラメータを誰が知っているべきか」と言う問題で、それは 構築を行うオブジェクトが知っているべき。カモノハシ本のコード例のように 構築を main 関数 が担っているのに違和感を感じるならば、その処理をカプセル化すれば良いでしょう。

このアプリケーション全体のインターフェイスとなるパズルオブジェクトを設けて、そのクラスに構築を行わせるか*1、別にファクトリオブジェクト(または関数)を設けて生成済みパズルオブジェクトを返させるか。前者を取れば、

NQueenPuzzle * puzzle = new NQueenPuzzle( 8 );
puzzle->findSolusion();
puzzle->print();

なコードになるでしょうか。

ここで「Board」という名前(概念)の使いどころがあるのか再考してみます。

ファクトリを「Board」と名付けるのは 不自然です。なぜなら持つ責務(パズルを生成する) と 名称の示す概念(ゲームボード) が あまりマッチ していないからです。同様のミスマッチは パズル と「Board」間にもあると思います。ただ、あえて強引にそう名付けてしまうのも有りかもしれません。いや、無しかな?(昔わたしの書いたコードをみたら、見事に NQueenBoard なるクラスがありやがりました^^;)

ファクトリまたはクラスのコンストラクタに渡す為の引数を コンテナに詰めてそれを Board と 抽象化するのは、前述の通り、ダメダメでしょう。

と、こんな感じでもよいかしら?


現実の構造をそのままプログラムの構造に写しこむのがなぜダメか

「Board があって Queen という駒があって、それの打ち手が居て」という現実の構造をそのままプログラムの構造に写せば、

  • 解を考える(駒を動かし、結果を検証する)のは打ち手(Player)オブジェクトの責務
  • 駒と駒の位置情報、盤面の広さの保持は Board オブジェクトの責務
  • Queen オブジェクトの責務は、駒の移動ルールと攻撃のルールを持つこと

となりますが、これは適切に責務を分散してるとは言い難いです。Player が神様過ぎます。

実はこのミスマッチは、作りたいのは「8Queen を解くプログラム」なのに、「チェスのプログラムの オマケとして8QueenプレイヤーAI を付けたプログラム」になってしまっているからです。結果、プログラム本体が殆ど全部「プレイヤーAI」に押し込められ、その他オブジェクトは単なるワーキングメモリに毛が生えたモノでしかなくなっています。

この例はある意味「8Queenを解く現実の姿を忠実になぞらえたもの」とも言えますが、「プログラミング対象の捉え間違い」とも言えます。本当は私たちはその AI(パズルを解く人)の頭の中相当のものをオブジェクト指向――手続きではなく、責務分散協調系の構造をもったプログラムに書き下ろそうとしていたハズです。

カモノハシ本の 8Queen 例題が素晴らしいのは、「自ら答えを見つけるオブジェクト」が「協調して」全体の解を求める点です。わたしの書き方が悪かったのですが、

Board というオブジェクトは必ずしも必要ないですし、連結リストの一番端には現実には存在しない「番兵」を置く場合もあります。なによりも、Queen の駒が現実で勝手に自分の攻撃されない位置を求めて動くなんてありません。

憂鬱本レビューの補足 - みねこあ

で重要なのは Boardの有無では決してなく、太字で協調した部分、現実にはあり得ない、子供の夢の中の世界のようにQueen のコマが自立的に勝手に解を求めて動くところ、そこにあります。それは現実にはあり得ない姿ですが、非常にOOPらしいプログラムです。

いろんな意味で「現実の写像」が設計の足を引っ張る良い例だと思います。


* * *


確かに、「プログラムの構造」が「現実の構造」に似ていた方が「人がピン!と来やすい」というメリットはあるでしょう。しかし「ピン!」とくるのは「既に知っているルールに似てるモノを『直感的』と感じる」からで「その構造が(普遍的に)良い構造だから」ではありません。

現実の構造は、あくまで現実に即した構造であり、プログラムに適した構造とは限りません。またクラスを使って現実の概念分類をなぞることにあまり気を取られすぎると、本当のプログラミング対象を見失ったり、現実の複雑さのうちプログラムの中では必要のない複雑さをもプログラムに取り込んでしまう(取り込んでも気がつかなくなってしまう)こともあるでしょう。

現実の分析は分析、プログラムの設計は設計。現実の構造を分析することは要件の理解になるかもでしょうが、それは単にそれだけ。理解した要件を元に、設計は設計で「プログラムの構造としてどのようなものが適切か」をきちんと考えるのが必要なのは、やっぱり他のプログラミングパラダイムと同じなのです。


「適切な設計」とその学び方

では、「適切な設計」とはどんなものでしょうか。「オブジェクト指向プログラミング」の類友、「構造化プログラミング」と比べながら考えてみます。

構造化プログラミングでは 主に「関数(サブルーチン)」を構造材(モジュール)にプログラムを構造化しましたが、オブジェクト指向プログラミングでは「オブジェクト」を構造材にプログラムを構造化します。一番大きな、そして根源的な違いはここになります。

そして、構造材が変われば作られるプログラムの姿も変わります。構造化プログラミングでは構造化されたプログラムは段階的詳細化されていますが、オブジェクト指向プログラミングでは責務分散協調系の形になります。

OOで構造化に用いる箱「オブジェクト」は「関数」に比べ強力です。たとえば、

  • 関数の場合呼び出し先の共通化しか出来ないが、オブジェクトの場合 ポリモーフィズムによって呼び出し元の共通化が出来る
  • 関数の場合、構造(関数)の間には「呼び出し元・呼び出し先」という関係しかないが、オブジェクトやクラスの場合には、もっといろいろな関係が存在する

とかとか。強力な分だけ自由で、選択肢が多くなります。一方それは、取り得る構造のどちらがよい構造なのかより迷いやすいというデメリットにもなります。

幸いなことにそれら構造の善し悪しを判断する指針はあります。「開放閉鎖原則」などなど。そういう指針を頼りに「オブジェクト」を手によりよい設計を 試行錯誤していくと、選択肢の広さに反して、意外に同じような構造に収斂しちゃうのです。自由すぎ、広大で大変だー!・・と思ったけれど、なんかパターン化されてるよな、OOP。なんだ、大したことないじゃん。

こういう「よく見る構造」に名前をつけたのがデザインパターンです。


* * *


さて、OOP を学ぶ良い方法について。プログラミングですからやっぱり プログラムを書くこと、良いプログラムを読むことにつきます。

件の Board の件も、マス目数の置き場として Board のあるプログラムを書いてみれば、「Boardオブジェクトはどの様に定数をQueen に渡すの?」「Queen は Board を知っている必要がある?」「使うときにリアルタイムに取得する?」「するとBoard オブジェクトの寿命はいつまでになる?」と、色々悩ましく、その試行錯誤の分だけ得るものはあるハズ。

とはいえ、せっかく OOP の良い設計をギューと濃縮したデザインパターンなんてものがあるのですから、それを利用しない手はないでしょう。

というわけで、「ちゃんとした設計」が出来る力を独習するなら、わたしの一押しはこの本。

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

最初に単純な、思いつくままの設計し(これが OOP という道具を手にしたら誰でもやってしまうような設計なのです)、その間違った(良くない点)を明らかにし、次第に改善していくと、デザインパターンの構造に出会うというアプローチで書かれています。

この本を手に、コードを書きながら(←ここ重要)ベテランOOプログラマがたくさんのプログラミングの果てにたどり着い道筋を追体験すれば、ギュー・・・と効率的に、OOP とは何かを具体例と経験をもって身につけることが出来ますにゃ。

特にこの本は、非常に敷居は低いですから、OOに挫折することも、間違ったOOを身につけてしまうことも無いでしょう。あまりに簡単に習得できるので、「OOって難しい」とか「悟りが必要」とか「脳構造の変革が必要」とか、そんなの全然大袈裟じゃん、と、思ってしまうこと請け合いです。

*1:クラスをオブジェクトの一種として捉えたとき、「インスタンス生成ファクトリ」と「インスタンスの処理の委譲先」という二つの責務を備えたオブジェクトになります。