ギルドワークスの増田です。
(前回書いた リファクタリングのエッセンス の続編です)
if文のちょっとしたの書き方の違いは、ソフトウェアの変更のやりやすさに大きく影響します。前回のサンプルコードから、if文の条件式部分だけ抜き出してみます。
※注意:この記事は2014年9月22日に GuildWorks Blog で公開したエントリをリライトしたものです。
A.設計改善前(論理演算式をべた書き)
if(date.isBefore(SUMMER_START)||date.isAfter(SUMMER_END)) ...
B.設計改善後(メソッドに抽出)
if(isNotSummer(date)) ...// 同じクラス内のメソッド boolean isNotSummer(Date date){ return date.isBefore(SUMMER_START)||date.isAfter(SUMMER_END);}
C.オブジェクト指向設計らしい改善
if(date.isNotSummer()) ...//ServiceDateクラスにロジックを移動class ServiceDate{ private final LocalDate date; boolean isNotSummer() { return date.isBefore(SUMMER_START)||date.isAfter(SUMMER_END); }}
AからB、BからCへの設計改善について、詳しく検討してみます。
if文の条件式は演算式のべた書きではなくメソッドに置き換える
まず、OR演算式を、isNotSummer() メソッドに置き替えました。 たった一行の演算式 を、わざわざ メソッドに抽出 する理由は次の二つです。
- 「何をしたいのか( 目的 )」と「どう実現するか( 手段 )」を分離する
- 同じOR演算式を別の箇所から利用できるようにする
特に大切なのが、「 目的 」と「 手段 」の分離です。
メソッド名 で、 目的 を表現します。 メソッドの定義 内容で、 手段 を表現します。
if文の条件式にOR演算式をべた書きするのは、手段のみの記述です。isNotSummer()メソッドの導入により「夏季ではなかった場合」という、コードの意図が メソッドの名前 として登場しました。
「プログラムが動作する」という観点なら、どちらの書き方でもいっしょです。
しかし、人間がこのコードを読むときにisNotSummer() メソッドは重要な意味を持ちます。「夏季ではない」という業務上の関心事がコード上で明確になりました。 OR演算式そのものは、業務上の関心事ではなく、プログラミング上の関心事です。
コードを書くときに、プログラミング上の関心事だけべた書きするのか、業務の文脈での関心事をメソッド名で表すのか違いは、プログラム全体を見た時に、ソフトウェアのわかりやすさや変更のやりやすさに大きな影響を及ぼします。
以下の二つのルールでプログラミングした場合の違いを想像してみてください。
- ルールa. if文の条件式は、かならず、 論理演算式の中身を記述 すること
- ルールb. if文の条件式は、かならず、 メソッド呼び出し で記述すること
プログラムの規模が大きくなれば、a. は変更が不可能になるくらいわかりにくいコードになっていきます。一つ一つのif文を正しく読み解くだけでも、時間とエネルギーが必要になります。
b.であれば、論理演算(判断の詳細)はとりえあず考えずに、夏季がどうかの違いで処理が変わることが楽に理解できます。
たった一行の論理演算式でも、isNotSummer() のように、メソッドにすることで、コードの意図が読み取りやすくなります。そして、同じ論理演算式を、isNotSummer()として、再利用することが可能になります。
if文の条件式は、論理演算式のべた書きよりも、メソッドに抽出したほうが良い設計なのです。
コードの重複を防ぐオブジェクト指向らしい設計
さきほどの例で、isNotSummer()メソッドは、コードの再利用が「 可能 」になると書きました。しかし、実際には、あちこちに同じisNotSummer()メソッドが登場しがちです。
チームで「条件式はメソッド呼び出しにする」というプログラミングスタイルを共有できたとします。if文の条件式が書かれた同じクラス内に、isNotSummer()メソッドを追加することは簡単です。しかし、他のクラスで、このisNotSummer()メソッドが再利用されるかどうかは、保障の限りではありません。
isNotSummer()メソッドのようなちょっとした共通ロジックは、事前に設計もできないし、また、チームで、このレベルの便利メソッドを常に共有しておくのは、現実的には無理です。
isNotSummer( date ) という、日付データを引数にもったメソッドは、典型的な手続き型プログラミングのスタイルです。そして、このスタイルの問題は次の2点です。
- そのメソッドをどのクラスに書くべきか、わかりやすい判断基準がない
- そのメソッドの存在をチーム内で必ず共有して再利用する簡単なやり方がない
この問題を解決する工夫が「オブジェクト指向」の設計です。
考え方は、単純です。
関連するロジックとデータは同じクラスに置く
class ServiceDate{ private final LocalDate date; ServiceDate( LocalDate date ) { this.date = date; }
boolean isNotSummer() { return date.isBefore(SUMMER START)||date.isAfter(SUMMER END); }}
ServiceDate クラスは、サービスを提供する日、夏季か冬季かを判断するための元データを保持します。その日付が夏季か冬季かを判断するロジックは、このServiceDateクラスのメソッドにするのが、オブジェクト指向らしい設計なのです。
手続き型プログラミングのスタイルのメソッドでは、isNotSummer( LocalDate date) のように、計算に必要なデータはメソッドの引数として渡しました。
ServiceDate クラスでは、計算に必要なデータは、オブジェクトの生成時に、コンストラクタで渡します。そして、メソッド isNotSummer() は、引数はなくなります。オブジェクト自身が必要な日付データを持っていますから、メソッド呼出の時に引数で日付データを渡す必要はありません。
ServiceDateクラスが、サービス提供日を持つことをチーム内で共有することは容易です。サービス提供日を扱う時、ServiceDateクラスを使うことは言葉の意味としてごく自然です。
そして「日付を持つ」クラスが「夏季か冬季の判断メソッド」を持っていることもわかりやすい。データを持つクラスに関連するロジックを置く。 これがオブジェクト指向設計の基本的な考え方なのです。
ServiceDateクラスのようにちょっとしたデータとロジックを持つクラスを作る。そうすることで、サービス提供日が夏季かどうかの判断の論理演算式は、このクラスだけに登場する。プログラムの他の場所では、 ServiceDateクラスのインスタンスのisNotSummer()メソッドを呼び出すことが、この論理演算を実行する唯一の手段になる。
こういうプログラミングのスタイルを徹底することで、あちこちに、if文の論理演算式が重複して記述されることを防ぐことができるわけです。
データとロジックは同じ場所に置く。そうすれば、コードの重複が起きなくなる。これが、オブジェクト指向で設計する狙いでありメリットなのです。
isNotSummer(LocalDate date) から ServiceDate#isNotSummer()へ
サービス提供日をメソッドの引数として渡すのは、典型的な手続き型プログラミングのスタイルです。
サービス提供日をインスタンス変数として持つ ServiceDateクラス自身に、isNotSummer()メソッドを持たせることが、オブジェクト指向らしいプログラミングスタイルです。
メソッドを比べると、いちばんの違いは引数の有無です。
これは、手続き型スタイルの設計を、 オブジェクト指向らしい設計に変換する、基本パターンです。
- 引数を渡すメソッドがある isNotSummer(LocalDate date)
- その引数をインスタンス変数として持つクラスを作成する ServiceDate
- ServiceDateクラスのインスタンス変数は、オブジェクトの生成時にコンストラクタで設定する
- ServiceDateクラスに isNotSummer() メソッドを移動する(引数は不要になる)
メソッドに引数が登場したら、こういうオブジェクト指向らしい設計への転換の可能性をいつも考えるようにしましょう。
気が付くと、ごく自然に、関連するデータとロジックを同じクラスに置くことが当たり前になってきます。チーム全体でそれが常識になってくれば効果は絶大です。
演算式レベルのコードの重複が激減します。コードは読みやすくなり、また、演算式で表現していたビジネスルールの変更は、プログラムのあちこちを調べたり、何か所も変更する必要はなくなります。修正は一箇所に限定され、修正の影響範囲も、そのロジックのあるクラス内部に局所化されます。
修正漏れや修正の副作用に悩むことがなくなります。
それが、オブジェクト指向で設計する目的であり、メリットなのです。
現場で、コードレベルで、実際に
ギルドワークスでは、こういう設計の考え方ややり方を学ぶためのワークショップやセミナーを定期的に開催しています。また、みなさんの現場に出かけて実際のコードを題材にやってみるオンサイトのワークショップにも力を入れています。ご興味を持たれた方は ギルドワークスのホームページ からお気軽に お問い合わせ ください。
<補足>
実は、このサンプルのロジックは、南半球では使えません。また、isNotSummer() というメソッド名は “Not” がわかりにくい感じがします。そもそも夏季・冬季の判断を、 if-then-elseで場合分けするのが、ほんとうに良い設計なのかも、議論の余地があります。ここらへんの話しは、次回で書こうと思います。
Photo credit: https://www.flickr.com/photos/benterrett/9047854229
この記事もどうですか?
-
SIerを飛び出して、どんな風景が見えたか
これは ギルドワークス Advent Calendar 2019 7日目の記事となります。 私は前職ではSIerに10年間勤めており、様々な現場で様々なシステムの開発をしてきました。 この記事では、私がSIerを飛び出して見た風景をお伝えし…
- アジャイル開発
-
オレオレ Technology Radar を作って最適な技術選定をしよう
はじめに この記事は ギルドワークス Advent Calendar 2019 11日目の記事になります。 今回は、Technology Radarというものを使って、普段開発をやる上で使用する技術の選定を効率よくやって、最善・最適なプロダ…
- 仕事のやり方
-
1on1でフラットな感じで話したい時に付箋を使ってみる
状況 1on1や雑談、相談の場 解決したいこと フラットな感じで話したい。 雑談の中でポッと出てきたアイデアを残しておきたい。 こうやっている 仕事柄、1on1での相談や日々の雑感を話す場がそこそこある。ギルドワークスでは、こういうのをスモ…
- 1on1