ギルドワークスの増田です。
(前回書いたリファクタリングのエッセンスの続編です)
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(SUMMERSTART)||date.isAfter(SUMMEREND);
}
}
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
2
取り消す
この記事に共感したら、何度でも押してこの記事のポイントをみんなでアップしよう。