前回までの振り返り
前回までで、MVCとドメイン駆動設計について、自分なりに学んで理解してきました。その過程で感じたことは、「設計の基礎についての理解が足りていないな」ということでした。
例えば、この設計でこういう形にしてるのはなぜだろう?と疑問に思ったとして、その理由の根底にあるのは、全ての設計に共通するような、そういう基本的な原則を守るためだったりします。
そこで、今回から、特定の設計の勉強ではなく、もっと広い視点での設計基礎の勉強をしようと思います!
はじめに
今回は本で勉強します。良いコード/悪いコードで学ぶ設計入門という本です。悪いコードの事例と、なぜ悪いのか、そして良いコードにするにはどうすれば良いのか、ということがわかりやすく書かれています。
実はこの本、設計の勉強を始めた時に、初めに軽く目を通していました。読んだ時には、ふむふむなるほどと分かったような気になりましたが、実際に自分で設計を考える時には、1割も頭に残ってなかったんじゃないかな〜と思います。
コードの写経を始めてから、やっと実務でも学んだことを生かせるようになってきたので、今ならもっと得られるものがありそうなので、もう一度一から読み直してみました!
(そもそも初めに読んだときは流し読みだったというのもあります)
さて、前置きが長くなってしまいましたが、とりあえず本の内容で印象に残った部分を二回くらいに分けて残していこうと思います。
クラス設計
クラス設計について、設計時に気を付けることがいろいろと説明されていました。
クラス設計で気を付けるポイント
- コンストラクタで確実に正常値を設定すること
- インスタンス変数は不変にすること
- 値の渡し間違いを防ぐために、プリミティブ型はできるだけ使用しないこと
- 不必要なメソッドを追加しないこと
- 計算を対象のデータを持つクラス自身にやらせること(高凝集)
細かい部分はまだまだありますが、大体上記のようなことが書かれていました。
しかし、これら全てをいきなり覚えて設計の時に活かす。というのは難しいと思います。なので、一番大事だと思ったポイントをまず覚えておくことにします。
「クラス設計とは、インスタンス変数を不正状態に陥らせないための仕組みづくりです。」
つまり、上記のポイントは全て、インスタンス変数に変な値を渡す可能性を防ぐためのポイントです。
低凝集
凝集度とは、モジュール内における、データとロジックの関係性の強さを表す指標です。
高凝集であれば、変更に強く、低凝集なコードはバグが埋め込まれやすく壊れやすいです。本では、様々な低凝集の例が紹介されていました。個人的にやってしまいそうだなと思ったこと、もしくはやってしまったことがあるものを残しておきます。
-
Common,Utilなどの共通処理クラス
共通処理クラスは、低凝集になりやすいクラスとして紹介されていました。
例えば、二つのクラスで共通の処理実装したいとき、CommonやUtil実装されてしまうと、低凝集になってしまいます。本来は、どんなユースケースでも必要となる処理(ログ出力、例外処理、エラー処理)のみを書くべきで、共通処理クラスは、できるだけ使わないという気持ちを持っていた方がいいということでした。
(これ、一度やって指摘されたことがあります)
-
出力引数によって起こる低凝集
出力引数とは、引数として渡された値に変更を加え、それをそのまま出力としてreturnしているもののことを言います。以下がその一例です。class Action { void move(Location location, int shiftX, int shiftY) { location.x += shiftX; location.y += shiftY; } }
Locationは別クラスで定義されており、低凝集になっています。
また、引数は入力値として受け渡すのが一般的です。引数を出力値として扱ってしまうと、引数が入力なのか出力なのか、move()
の内部のロジックを読んで判断しなければならなくなり、可読性の低下を招きます。ソフトウェア設計には、「尋ねるな、命じろ」という有名な格言があります。他のオブジェクトの内部状態を尋ねたり、その状態に応じて呼び出し側が判断したりするのではなく、呼び出し側はただメソッドで命ずるだけで、命令された側で適切な判断や制御するように設計することが大切です。
デメテルの法則
利用するオブジェクトの内部を知るべきではない、という法則。
「知らない人に話しかけるな」と要約されたりする。
出力引数などは、デメテルの法則に違反している。
条件分岐
条件分岐のスマートな書き方として、ストラテジパターンというものが紹介されていました。条件分岐を、interfaceによって実現する方法です。
例えば、以下のような条件分岐のコードがあったとします。
void showArea(Object shape) {
if(shape instanceof Rectangle) {
System.out.println(((Rectangle) shape).area())
}
if(shape instanceof Circle) {
System.out.println(((Circle) shape).area())
}
...
}
上記のコードでは、無理矢理型を判定して、その結果に応じて条件分岐をしています。
これをストラテジパターンを適用すると、以下のような構成になります。
(interfaceの使い方に関しては、勉強記録第2回で詳しくみたので詳細は省きます。)
このようにして、RectangleもCircleも同じShape型として利用できるようにすると、条件分岐をかかずに、条件分岐と同じ動きの実装ができます。
結果、showArea()
は以下のように変わります。
void showArea(Shape shape){
System.out.println(shape.area())
}
ストラテジパターンに関しては、もっと色々な使い方やポイントがあるのですが、一旦これだけ覚えておきます。
「条件分岐を書きたくなったら、まずinterface」
密結合
密結合とは、あるクラスが、他の多くのクラスに依存している構造のことを指します。密結合なコードは理解が難しく、変更も困難になってしまいます。密結合はどんな時に起こるのかや、どうすれば解消できるのかについて、本では丁寧に説明されていました。
その中で、いくつか自分でもやってしまいそうなことについてまとめます。
-
DRY原則の勘違いから起こる密結合
DRY原則とは、直訳すると「繰り返しを避けよ」という原則です。一見シンプルな原則ですが、一部では「コードの重複を許すな」という解釈で広まっているようです。(私も初めはそのような捉え方をしてしまいました)では、コードの繰り返しでないなら、なんの繰り返しなのか、という話になります。原典では以下のように記述されているようです。
全ての知識は、システム内において、単一、かつ明確な、そして信頼できる表現になっていなければならない
つまり、繰り返してはいけないものは、「知識」です。では「知識」とは何か、これには様々なものが考えられそうですが、本の中では、その中の一つとして、割引やキャンペーンなど、ソフトウェアで扱う、ビジネス概念という捉え方をしていました。
例を挙げて説明すると、「通常割引」と「季節限定割引」の実装を行うとして、これらはコード上は非常に似通った内容になると思います。しかし、この二つは別の概念が異なるので、DRYにすべきではないのです。
-
高凝集の誤解から起こる密結合
低凝集の章で、高凝集であれば、変更に強いということを書きましたが、高凝集を誤解していると、密結合になってしまうことがあります。「高凝集を意識して、強く関係してそうなロジックを一箇所にまとめようとしたが、結果として密結合になってしまっている」ケースです。例えば、「販売価格」クラスに、販売価格に関係しているからと、「販売手数料」や「配送料」の計算ロジックまで実装してしまっているコードがあったとします。
しかし、「販売価格」、「販売手数料」、「配送料」は全てビジネス概念として捉えると別概念であり、分離して実装するのが正しいです。このようなケースは、誰もが極めて陥りやすい罠として紹介されていました。設計時には、別の概念が混在してしまっていないかをしっかり考慮することが大切です。
終わりに
一旦今回はここで打ち切ります。大体本の半分、1~8章までの内容で印象に残ったものを書きました。
設計の勉強をしたからか、一回目と比べて内容がすらすら頭に入ってくる気がします!
次回は残りの半分の内容と、全体を通して感じたことなどを書こうかと思ってます!