処理中心思考とオブジェクト中心思考(2)
こんにちは、ビガーです。処理中心思考とオブジェクト中心思考(1)の続編です。
■はじめに
わたしの考える「処理中心思考」とは、【処理はこうなるはず】という発想が第一にくる考え方であると述べました。
それに対して、わたしの考える「オブジェクト中心思考」とは、【実現するべき事象を現実世界のモノゴトに置き換えるとこうなるはず】という発想が第一にくる考え方です。
■オブジェクト中心思考で考えてみる
FizzBuzzを例に考えてみましょう。
まず、FizzBuzzで実現するべき事象は、何かを考えると以下のようになります。
- 自然数を書き出したい
- あるルールが適用されたときに特定の文字を書き出したい
ここまでは、処理中心思考そのものです。
続いて、現実世界のモノゴトに置き換えるわけですが、オブジェクト中心思考とは、処理中心思考を現実世界にリンクさせるイメージです。
- 数をどこかに落書きしたい(わたしの子供は異常に数が好きなので実際やりそうで怖い……)
- 気に入らない奴の家には派手な落書きをしたい
ここでのポイントは、「どこかに」です。5W1HのWhereです。つまり、【現実世界のモノゴトに置き換えるとこうなるはず】というのは、「5W1Hで捉えることに等しい」とわたしは考えています。
そう考えると、
- わたしの子供(Who)が、数(What)を誰かの家(Where)に落書き(How)したい
- わたしの子供(Who)は、気に入らない奴(Whom)の家(Where)には、派手(What)な落書き(How)をしたい。
みたいな感じになるわけで。
整理すると、わたしの考えるオブジェクト中心思考は、処理中心思考に5W1Hの要素を取り入れた考え方であるといえます。問題解決の汎用的なフレームワークである5W1Hを軸に考えるので、より全体像を考慮した汎用的な発想になっちゃうわけなんです。
で、最も大事なのは、この考え方とプログラミングをどうやって紐付けるかです。その紐付けのギャップを設計で埋めることになります。
■オブジェクト中心思考で設計してみる
この設計でものをいうのは、デザイン原則やデザインパターンの本質をどこまで理解しているかで決まります。この作業(勉強)は、とても地道ですが、現実のプロジェクトで試しながら(もちろんプロジェクトが失敗しないようにリスクヘッジしつつ)スキルを上げていくしかないと考えています。
なぜなら、プロジェクトで実際やってみないと非機能要求を担保できているかを保証できないからです。あくまで機能的な汎用性を考えた場合のモノであるので、現実のプロジェクトにソノママ適用したらとんでもないことになります。そこの兼ね合いがミソなんですが、それがわたしの飯のタネですね、今のところの。
話がわき道にそれましたので、話を設計に戻します。
この例では、以下の通りのパターンを使用することにしました。
- わたしの子供(Who)=訪問者(Visitorパターン)
- 家または気に入らない奴の家=戦略(Strategyパターン)
- 数または派手な落書き=状態(Stateパターン)
■オブジェクト中心思考で実装してみる
コードは、後述の通りです。コードの意図を簡単に説明します。
まず、ThreeAndFiveFizzBuzzStrategyで3と5と15で派手な落書きをし、その他で数字を落書きする戦略を定義しています。そのほかの戦略を定義したい場合は、FizzBuzzStrategyを継承してjudgeを実装します。
次に落書きをするヒトがその戦略に準じて、家に落書きを開始します。それが、以下です。
new ThreeAndFiveFizzBuzzVisitor().visit(new ThreeAndFiveFizzBuzzStrategy());
家への訪問の仕方を変えたい場合は、FizzBuzzVisitorを継承して、visitを実装します。
public class FizzBuzz {
public static void main(String[] args) {
new FizzBuzz().run();
}
public void run() {
new ThreeAndFiveFizzBuzzVisitor().visit(new ThreeAndFiveFizzBuzzStrategy());
}
/**
* ループカウンタを訪問者に見立て、巡回基準に則って巡回することだけを目的としている。
*/
abstract class FizzBuzzVisitor {
protected int visitCount = 0;
protected static final int VISIT_COUNT_MAX = 100;
abstract void visit(FizzBuzzStrategy strategy);
public int getVisitCount() { return visitCount; }
}
class ThreeAndFiveFizzBuzzVisitor extends FizzBuzzVisitor {
@Override
public void visit(FizzBuzzStrategy strategy) {
if( strategy == null ) throw new IllegalArgumentException("strategy is null");
if( visitCount++ >= VISIT_COUNT_MAX ) return;
strategy.print(this);
}
}
/**
* 訪問者が訪れてきたときにするべきこと(戦略)を定義する。
*/
abstract class FizzBuzzStrategy {
private java.io.PrintStream output = System.out;
protected abstract FizzBuzzState judge(FizzBuzzVisitor visitor);
public void print(FizzBuzzVisitor visitor) {
if( visitor == null ) throw new IllegalArgumentException("visitor is null");
FizzBuzzState state = judge(visitor);
if( state == null ) { output.println(visitor.getVisitCount()); }
else { output.println(state.getStateName()); }
visitor.visit(this);
}
}
class ThreeAndFiveFizzBuzzStrategy extends FizzBuzzStrategy {
@Override
protected FizzBuzzState judge(FizzBuzzVisitor visitor) {
if( visitor == null ) throw new IllegalArgumentException("visitor is null");
int count = visitor.getVisitCount();
if( count % 3 == 0 && count % 5 == 0 ) { return new ThreeAndFiveFizzBuzzState(); }
else if( count % 3 == 0 ) { return new ThreeFizzBuzzState(); }
else if( count % 5 == 0 ) { return new FiveFizzBuzzState(); }
return null;
}
}
interface FizzBuzzState {
String getStateName();
}
class ThreeFizzBuzzState implements FizzBuzzState{
public String getStateName() { return "Fizz"; }
}
class FiveFizzBuzzState implements FizzBuzzState{
public String getStateName() { return "Buzz"; }
}
class ThreeAndFiveFizzBuzzState implements FizzBuzzState{
public String getStateName() { return "FizzBuzz"; }
}
}
■おわりに
コード自体は、ひでみさんのコラムのコメントの焼き増しで恐縮ですが、実装前の考え方が重要な部分なので、コラムにしてみました。犬やら猫やらを題材にしたオブジェクト指向云々の説明よりは、有意義だったかな? と考えています。不明点などあれば、コメントお願いします。
コメント
CMP
ビガーさん、こんにちは。
はい!質問です。
> 家または気に入らない奴の家=戦略(Strategyパターン)
> 数または派手な落書き=状態(Stateパターン)
Where,What,HowとStategy,Stateの結びつきがよくわかりません。
Visitorはある戦略に基づいて落書きする家を選択し、その家に対して落書きをするのですか?
それとも、Visitorが特定の家に落書きする方法をある戦略に基づいて選択し実行するのですか?
P.S.
ついでにWhenの使いどころのご指南もお願いします(汗)。
ビガー
CMPさん、コメントありがとうございます。
あら、仰る通り肝心の関連付けがわからないですね。失礼しました。
補足させていただきます。
Strategyは、どの家を対象にするかの判断(Where/Whom(特殊な条件))を表現しています。
Stateは、書き出すもの(What)を表現します。
そういう意味で、数字も本来Stateで表現するべきです。(穴発見。。)
また、Visitorは、WhoとWhen(タイミング)です。
今回は、1件ずつ訪問していますが、訪問先を飛ばしたり、訪問のタイミングを変える場合などが該当します。
Howについては、落書きをする行為(出力)になりますが、今回はStrategyがその役を担っています。
ちなみにWhyについては、今回の話とは関連がありません。
なぜならば、そもそもFizzBuzzをなんで解く必要があるのか?という問題は、今回のお題を実行する前に検討されている前提だからです。
CMP
ビガーさん、こんばんは。
回答ありがとうございます。
なるほど、ようやく理解できました。
とはいえ、いきなりオブジェクト中心思考で設計・開発は、無理っぽいw
何度も租借して、オブジェクト中心思考っぽい考え方をマスターしていきたいと思います。
(まずは「っぽい」ところから^^;)
ビガーさんはじめまして、いつも楽しく読ませて頂いてます。
オブジェクト中心思考という大変興味深いお題で、コードが載っていたので楽しめました。
ただ、コードに何箇所か違和感を感じるところがあり、自分なりに検証してみました。
まず、ビガーさんが書いたコードのクラス図を書いてみました。
http://csharp30matome.up.seesaa.net/image/java_fizzbuzz_class.png
細部まで正しいかは怪しいですが、大体こんな感じだと思います。
で、どこに違和感を感じたかというと、一番はVisitorパターンの使い方です。
Visitorパターンは、本来データ構造と処理を分離しようという目的で使うものだと思うのですが、
ビガーさんが書いたコードだと、そのデータ構造が曖昧な感じがしました。
StrategyとVisitorを行ったり来たりするという処理、そこがまず違和感を感じた点です。
あと、もう一つはStateにFizz, Buzz, FizzBuzzはあるのに、Normal(普通の数字)のときのStateが無いことです。
せっかくならStateに普通の数字を出力する状態のクラスも作った方がいいのかなと。
という訳で、私がビガーさんのように無理矢理Visitor,Strategy,Stateパターン使って書くならどうするかなー
と考えて書いたのが下のクラス図です
http://csharp30matome.up.seesaa.net/image/csharp_fizzbuzz_class.png
Numberクラスって熟考してないので何か微妙ですが、あえてVisitor使うならこんな感じかなと。
(半分酔っ払いなのでこの程度でご勘弁を)
ちなみに、クラス図内のサンプルコードはC#です。多分違和感なく読めるとは思いますが。
やっぱり、こうやってコードで語り合うっていうのは楽しいですよね。
貴重な機会をありがとうございました。また来ます。
ビガー
吾一さん、コメントありがとうございます。
検証いただきありがとうございます。ご批判大歓迎です!
大事なことなのでいっておきますが、何かのお題にデザインパターンを適用するという発想ではなく、お題をオブジェクトで捉えてからデザインパターンを見てみるという発想が重要だと考えてます。
それを踏まえて、
まず、VisitorとStrategyの関係についてですが、Visitorという側面で捉えた場合、一般に相手はAcceptorです。そのAcceptorが今回はStrategyだということ。
一方、Strategyという側面で捉えた場合、一般に相手はContextです。それが今回はVisitorです。
また、データ構造というのは、Stateになります。そのため、一般的なVisitorパターンを少々拡張しているのです。
また、Stateで数を表現するというのは、CMPさんへのコメントでもいいましたが、この実装の穴ですので、吾一さんのイメージが正しいと私も思います。
余談ですが、Visitorパターンはダブルディスパッチの一つの側面に過ぎません。そういう設計原則が重要なのですが、また別の機会にお伝えしたいと思います。