CodeIQ Blog

自分の実力を知りたいITエンジニア向けの、実務スキル評価サービス「CodeIQ(コードアイキュー)」の公式ブログです。

いまさら聞けない「オブジェクト指向設計の3つのコツ」~オブジェクト指向設計問題解説 #objectoriented

CodeIQ中の人、millionsmileです。

いろいろ経歴を積むと、「いまさら聞けない」ことが増えてきます。「オブジェクト指向」というのもそんないまさら聞けないものの一つでしょうか。

そんなわけで、いまさら聞けないことをイマサラ問題として出題してみました。

問題は、日本のITエンジニアの父と言いたくなるくらい温かみのあるフィードバックをしてくれることで好評な有限会社システム設計の増田亨さんからの出題です。オブジェクト指向設計について2問出題していただきました。総計65名もの方に挑戦いただきました!

問題の解説記事は、オブジェクト指向設計の3つのコツを中心に説明してくれていますので、読みやすいですし、頭にすっと入ってきます。

ではでは、増田亨さんによる解説記事をお楽しみください。

https://codeiq.jp/ace/toru_masuda/
f:id:codeiq:20130826155727p:plain

◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇

■オブジェクト指向設計のコツ

私がオブジェクトを設計する時、次の3つを、いつも心がけています。

◎ データと、そのデータを使うロジックは、一つのクラスにまとめる
◎ 一つ一つのオブジェクトの役割は単純にする
◎ 複雑な処理は、オブジェクトを組み合わせて実現する

以下のコードを見つけると、設計を見直したくなります。

× あるクラスのデータを get して他のクラスで処理している
× public メソッドが多いクラス(いろいろな役割を持ちすぎている)
× クラスが大きい/メソッドが長い(複雑な仕事を自分だけでやろうとしている)

今回の CodeIQ での出題は、

◇データとロジックをまとめる
◇オブジェクトの役割を単純にする
◇複数のオブジェクトで役割分担する

というオブジェクト指向設計の基本スキルを覚えるための基礎練習を意図して作ってみました。

■出題内容

出題は2問です。

問題1:コンストラクタからメソッドを推定する
問題2:注文の合計金額計算を複数のオブジェクトに役割分担させる

☆問題1:クラスのメソッドを推定してください

クラスのコンストラクタ(1)~(3)を三つ提示します。

それぞれのクラスに想定されるメソッドを3から5の範囲で考えてください。

解答方法:想定したパラメータ無メソッドを以下の形式で記述する。

型 メソッド名()

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
例:
コンストラクタ String( String orignal ) に対する解答例

Integer length()
Boolean isEmpty()
String trim()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

次のコンストラクタを持つクラスのメソッドを推定してください。

(1)コンストラクタ: StringAndPosition( String source, Position position )
(2)コンストラクタ: StringAndSize( String source, Size size )
(3)コンストラクタ: StringPair( String one, String another )

☆ 問題2:オブジェクトの役割分担を考えてみよう

以下の注文クラスがあります。合計金額は、Amount クラスのインスタンスです。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Order
{
	Amount total();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
・注文は明細行(複数)があります。
・各明細行は、商品、単価、注文数量のデータを持っています。
・税額計算は考えないでください。(税込金額とします)

合計金額を計算する仕事を、 Order, Amount 以外のクラスに役割分担をさせてください。

役割を分担するクラス(いくつでも良い)を、上記の Order クラスのように、
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class クラス名
{
	型 メソッド名();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
の形式で、記述してください。

◎特定のプログラミング言語の文法にこだわる必要はありません。
◎設計の意図をクラス名とメソッド名で表現する練習問題です。

■コンストラクタからメソッドを推定する

この出題のポイントは、メソッドで「パラメータを渡さない」ことです。

オブジェクト指向らしい設計を心がけると、メソッドでパラメータは渡すことが減ってきます。
この問題は、パラメータ無メソッドの感覚をつかむ練習問題です。

メソッドでパラメータを渡すのと渡さないのでは、何が違うのでしょうか?
文字列から部分文字列を取り出す例で考えてみましょう。

"CodeIQ" から、"IQ" を取り出してみます。

☆パラメータを渡す

String source = new String("CodeIQ");
String result = source.substring(4);

☆パラメータを渡さない

StringAndPosition source = new StringAndPosition( "CodeIQ", 4 );
String result = source.substring();

文字の位置を、メソッドのパラメータで渡すか、コンストラクタで渡すかの違いですね。

前者の String クラスの source オブジェクトは、生まれた時に知っている情報は "CodeIQ" だけです。
substring( int 文字位置 ) が呼ばれると、一時的に文字位置を知ることになります。
メソッド実行が終わると、文字位置は忘れます。

後者の StringAndPosition クラスの source オブジェクトは生まれた時に "CodeIQ" と文字位置 4 の両方を知っています。

substring() が呼ばれると、知っている二つの情報(文字列と文字位置)を使って結果を返します。
結果を返した後も、文字位置を忘れません。

オブジェクト指向設計の勘所のひとつが、この、「オブジェクトが知っている情報だけ」を使って結果を返すことです。

■必要なデータはすべてオブジェクトに持たせておく

オブジェクト指向プログラミングでは、オブジェクトに「仕事をまかせる」ことをいつも心がけましょう。
仕事をまかせるためには、必要は情報は、可能な限りオブジェクト自身が知っているべきです。

オブジェクト指向らしい設計では、

原則1:必要な情報はオブジェクト生成時にコンストラクタで全て設定する
原則2:メソッドでパラメータを渡さない

の2点を重視します。

void setPosition( position ) のように、setter メソッドで値を設定するのは、「原則1:生成時に設定する」「原則2:メソッドでパラメータを渡さない」の両方の原則に反しています。

この2つの設計原則は「Complete Constructor」(完全コンストラクタ)という、オブジェクト指向の基本中の基本ともいえる設計パターンです。

■オブジェクト指向設計の基本は「完全コンストラクタ」パターン

「完全コンストラクタ」は、オブジェクトが生成された時点で必要なデータを全てオブジェクト自身が知っています。

setter もありませんから、最初に設定したデータが途中で変わってしまうこともありません。
不変(immutable)のオブジェクトですね。

こういう不変オブジェクトは常に安定した結果を返します。

それと比較して、

× メソッドでパラメータを渡す
× setter でオブジェクトの内部情報を書き換える

をやってしまうと、メソッドの結果は不安定になります。

結果がおかしくならないようにするために、あちこちのメソッドにパラメータの妥当性チェックのコードが増殖していきます。
個々のメソッドが、本来の仕事以外に、パラメータの妥当性まで心配しないといけなくなります。

こういうコードは、見通しが悪く、副作用が怖く、修正が難しくなります。

「完全コンストラクタ」パターンであれば、オブジェクトの生成時点で、準備が完全に整います。

これがオブジェクトに、安定した良い仕事をさせる設計のコツなんです。
各メソッドのコードは、本来の仕事を記述するだけなので、自然とすっきりとしたコードになります。

メソッド内でパラメータのチェックや、パラメータの加工をごちゃごちゃやっているコードを見つけたら、「完全コンストラクタ」パターンに変更することを考えましょう。

やり方は簡単です。

この問題で提示したように、必要なデータをすべて設定するコンストラクタを持ったクラスを準備するだけです。

StringAndPosition( String source, Position position )
StringAndSize( String source, Size size )
StringPair( String one, String another )

最初は、こういうクラスを追加して、値の組み合わせが異なるたびに別のクラスを設計したり、別の値を持たせるために、別のオブジェクトを生成することは、面倒に思えるかもしれません。

メソッドにパラメータを渡して必要な時だけ挙動を変えるほうが、簡単で柔軟に見えると思います。

しかし、これが落とし穴なんです。

コードのあちこちで、パラメータ渡しをやるたびに、全体の見通しが悪くなっていきます。結果も不安定になり、やっかいなバグと戦うハメになります。

デバッガで変数の状態を追いかけることが習い性になっていると、これが当たり前になってしまうかもしれません。

しかし、「完全コンストラクタ」パターンを積極的に使えば、こういうことは自然に起きなくなります。

◎ メソッドでパラメータを渡すのではなく、オブジェクト生成時に必要なデータをすべて準備してしまう。
◎ メソッドではパラメータを渡さない

これを徹底することが、オブジェクト指向らしい設計なんです。

メソッドでパラメータを渡したり setter で値を変えるのは、手続き型プログラミングをひきづった古いオブジェクト指向設計。完全コンストラクタで不変オブジェクトを重視するのは、関数型プログラミングにもつながるモダンなオブジェクト指向設計、といえるかもしれません。

■役割分担を明確に

「完全コンストラクタ」パターンの一番のメリットは、オブジェクト間の役割分担が明確になることです。
オブジェクトとオブジェクトの関係が疎結合になる、という言い方もできます。

◎仕事に必要な情報は同じオブジェクトに集約する

「完全コンストラクタ」では、必要な情報をオブジェクト生成時に、すべて渡してしまいます。
オブジェクト生成後は、情報の管理責任を生成したオブジェクトに全てまかせるわけです。

部分文字列がほしい時に、ソース文字列を知っているオブジェクトと、 文字位置を知っているオブジェクトが別というのは良い役割分担ではありません。

部分文字列を返すオブジェクトは、ソース文字列と文字位置を両方とも知っているべきです。

そうすることで、部分文字列作成という仕事に専念し、必要な情報はすべて把握している、役割が明確なオブジェクトになります。

◎完全に準備する

「完全コンストラクタ」パターンではオブジェクト生成時に必要なデータをすべて「完全」に準備します。
妥当性のチェックが必要であれば、生成時にすべてチェックします。不足した情報があれば、適切は既定値を設定しておきます。

オブジェクトが存在する(生成できた)=準備が完全

であることを常に保障するわけです。

そうすれば、仕事を依頼する側は、いつでも安心して仕事が頼めるし、安定した結果を期待できます。

setter でオブジェクトの内部状態を変えたり、メソッド呼び出すごとにパラメータで異なる値を渡すと、そのたびごとに、この値を渡して大丈夫かとか、ちゃんとした結果が返ってくるかを、心配しながら、コードを書く必要がでてきます。

メソッドを呼び出す側で、その心配をするのは、役割分担として明らかにおかしい。

かといって、呼び出される側のオブジェクトが、メソッドでパラメータが渡ってくるたび、setter を使われるたびに、毎回、わたってくる値の妥当性を心配するようでは、負担が大きすぎます。

「完全コンストラクタ」パターンは、オブジェクト生成時に、この心配ごとをまとめて解決しておくパターンです。

仕事を頼む側と頼まれる側の約束事を、オブジェクト生成時に、確定しておくやり方です。

そうすることで、役割分担が明確になり、責任の分界点がはっきりします。

■メソッドはすべてのフィールド変数を使うこと

オブジェクトのそれぞれのメソッドは、コンストラクタで準備したすべてのデータを使うべきです。

StringAndPosition( String ソース文字列 , Position 文字位置 )

というコンストラクタを持つクラスのメソッドは「ソース文字列」と「文字位置」を両方使うべきです。

String getSource()
Position getPosition()

という getter は、それぞれのデータを個別に取り出すだけです。
こういうメソッドは、何も仕事をしていません。オブジェクトのメソッドとして好ましくありません。

両方のデータを使って、何かの処理結果を返すメソッドのほうが価値が高いことは明確ですね。

関連するデータとロジックをひとつのクラスに集めていけば、自然に、メソッドはすべてフィールド変数を使い、役に立つ仕事をするようになります。

■クラスが肥大化したらクラスを抽出する

コンストラクタのパラメータ(フィールド変数)が増えると、一部のフィールド変数だけを使うメソッドが増えてきます。

フィールド変数が5つもあれば、それをすべて使うメソッドは toString() くらい?

一部のフィールドだけ使うメソッドが増えきたらクラスの分割を検討しましょう。

5つフィールド変数のうち、2つだけを使うメソッドがあれば、2つのフィールド変数だけを持つ別のクラスに分離してみます。

リファクタリングパターンの一つ、「クラスの抽出」ですね。

もちろん「完全コンストラクタ」パターンで設計します。コンストラクタで2つフィールド変数を完全に準備します。そのデータを使うメソッドを、元のクラスからこちらに移動します。

「クラスの抽出」をすると、強く関連したデータとロジックが同じクラスに集まり、その他のデータとロジックは別のクラスに分離できます。

こうやって強く関係したデータとロジックだけを集めたオブジェクトを増やしていくと、プログラム全体の構造が整理され、オブジェクトの役割が単純に明快になり、全体の見通しがすっきりしてきます。

「完全コンストラクタ」パターンを使い、「メソッドはすべてのフィールド変数を使う」という設計原則を心がけると、コードが整理され、役に立つ良いオブジェクトが増えてくるわけです。

■「ファーストクラス・コレクション」パターン

「関連するデータとロジックを一つのクラスに集める」原則のより具体的なやり方として「ファーストクラス・コレクション」という設計パターンがあります。

「設問2:オブジェクトの役割分担」は「明細行の集計」を「ファーストクラス・コレクション」のオブジェクトに担当させることを期待した出題でした。

ほとんど方が、明細行ごとの「単価×数量」の計算は、Detail クラスに役割分担させる、という設計を解答されました。
これは期待通りでした。

明細行の「集計」は、Order クラスが自分で担当する、という解答がほとんどだったのは、正直、期待はずれでした。

明細行のコレクション(List)を、Order クラスが持つのではなく、 Details クラスに分離する解答を期待していました。

関連するデータとメソッドは一つのクラスにまとめる、というオブジェクト指向設計の原則をたいせつにすると、コレクションの集計処理はそれ専用の別クラス、という設計になります。

Details
{
  List<Detail> details;//このクラスで唯一のフィールド変数
  
  Amount total()
  {
     Amount result = new Amount()
     for( Detail each : details )
     {
        result.add( each.amount() );
     }
     return result;
   }
}

というクラスです。

コレクションに関するロジックは複雑になりがちです。
また、似たような処理がコードのあちこちに散在しがちです。

コレクションとその操作ロジックを、一つのクラスに集めて閉じ込める(カプセル化する)ことは、オブジェクト指向設計の基本の一つです。

「ファーストクラス・コレクション」という設計パターンです。

コレクションは特別扱いをして、いつも、専用クラスに閉じ込めておくという考え方ですね。

コレクションまわりの処理がごちゃごちゃしてきて見通しが悪かったら、とりあえず「ファーストクラス・コレクション」にクラスを抽出してみましょう。

コレクション操作がらみのバグを減らしたかったら、ぜひ「ファーストクラス・コレクション」を試してみてください。

データと関連するロジックをひとかたまりにするオブジェクト指向らしい設計の練習としても「ファーストクラス・コレクション」はおすすめです。

■オブジェクト指向設計の基本

最初に書いたように、私がいつも心がけている設計の基本は次の三つです。

◎ データと、そのデータを使うロジックは、一つのクラスにまとめる
◎ 一つ一つのオブジェクトの役割は単純にする
◎ 複雑な処理は、オブジェクトを組み合わせて実現する

「完全コンストラクタ」パターンも、「ファーストクラス・コレクション」パターンも、この基本を実践する良いやり方です。

またリファクタリングパターンの「クラスの抽出」「メソッドオブジェクトによるメソッドの置き換え」なども、この三つの基本を実践するための良いテクニックです。

今回の問題1:クラスのメソッドの推定は「完全コンストラクタ」パターンを、問題2:オブジェクトの役割分担は「ファーストクラス・コレクション」パターンを念頭においた出題でした。

オブジェクト指向設計の三つの基本や、この解説記事で紹介したパターンを、私は、いろいろな本を読み、コードで指向錯誤しながら、使い方を学び、また、効果を実感してきました。

特に、ケントベック「実装パターン」とマーチンファウラー「リファクタリング」からは、多くのものを学びました。

今回のプレゼントにこの2冊の本を選んだのも、そのためです。

この解説記事が、みなさんの設計スキル、特に、オブジェクト指向設計のスキルをさらに向上することに、少しでも参考になればうれしい限りです。

◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇

CodeIQ中の人後記:

プレゼント用のケントベック「実装パターン」は、手に入れるのが大変でした。出版元のピアソン・エデュケーションに確認したら2冊しかないということで、その貴重な2冊を購入しましたー。当選した方はかなりラッキーかもですよ!!

エンジニアのための新しい転職活動!CodeIQのウチに来ない?の特集ページを見る