【まとめ】良いコード/悪いコードで学ぶ設計入門
職場でおすすめされたので、こちらの書籍を読んでみました。
備忘録も兼ねて各章ごとの重要だと感じた点をまとめていきます。
【1章 悪しき構造の弊害を知覚する】
1章ではバグの温床となりやすい事例が紹介されています。
順番に見ていきます。
命名
書籍の中では良くない命名方法として以下の2つが挙げられています。
技術駆動命名
連番命名
1つ目の『技術駆動命名』はあまり聞き慣れない言葉でしたが、
「value, insert, update, flag, memory, data」
のようなプログラミングによく使う用語を用いた命名のことです。
『連番命名』はそのなの通り
「value01, value02, value03 …」
のように番号をつけて命名することです。
これらを用いると値が何を示しているのか分かりづらくなるため、バグの元となってしまいます。
深すぎるネスト
これは言わずもがなですね。
ネストが深いコードは理解が難しいですし、読む気もなかなか起きません…。
データクラス
ここでは低凝集であることが問題視されています。
低凝集とはデータとロジックが分散し、複雑になってしまっている状態のことを表します。
データを持つだけの単純なクラスかと思いきや、それを書き換えるためのメソッドやバリデーションが点在してしまうことがあります。
それにより仕様変更の際、修正漏れが発生しバグが発生してしまう可能性が高まってしまうのです。
【2章 設計の初歩】
2章では理解しやすいプログラムを書くための基本的なテクニックが記述されています。
変数の命名と用途
変数名は長くなったとしてもひと目で目的を理解できる必要があります。
頭文字だけの変数名では実装時には時間が短縮できるかもしれませんが、読み解くのに余計時間がかかります。
変数を使い回さないことも重要です。
『その変数の用途は何なのか』を明確にする必要があります。
再代入することで用途が変わってしまうと、読み手が混乱してしまいバグを埋め込んでしまう恐れもあります。
メソッドに切り分ける
ベタ書きだとプログラムを読むときに、何をしているのかを考えながら読み進める必要があります。
個人的に非常に頭のリソースを消費してしまうと感じるんですよね…。
メソッドに切り分けるとメソッド名という形で処理のまとまりに名前をつけることができます。
これは「プログラムの大まかな流れを理解したい」というときにはとてもありがたいです。
メソッドの中身を見なくてもなんとなくは把握できるので、理解のスピードが格段に早まると感じます。
クラスにまとめる
これは1章にも少し出てきた低凝集を避けるための方法です。
変数とそれにまつわるメソッドを、1つのクラスにまとめることが推奨されています。
具体的には『金額』という変数と『消費税を計算する』というメソッドを1つのクラスの中に作ろうということです。
こうすることで消費税を計算するメソッドが点在してしまうことを避け、高凝集で変更に強いプログラムにすることができます。
【3章 クラス設計】
この章は「バグが発生しないためのクラスを設計しよう!」ということを説いています。
クラス単体で動作する
クラスはインスタンス変数とメソッドで構成されています。
インスタンス変数の変更を行うのは同じクラス内のメソッドでなくてはならず、メソッドは同じクラスのインスタンス変数を使っている必要があります。(一部例外あり)
自分の身は自分で守る
クラスの設計パターンとして『完全コンストラクタ』と呼ばれるものがあります。
これは、コンストラクタですべてのインスタンス変数を初期化し、正しい状態のオブジェクトのみを存在させるようにするものです。
インスタンス変数のバリデーションをコンストラクタで行うことで、正しいオブジェクトのみインスタンス化できるようになります。
堅牢なクラスにする
以下の2つの方法が紹介されています。
final修飾子を使う
プリミティブ型を使わない
final修飾子を利用することで変数の再代入ができなくなります。
2章の最初に説明した『変数の用途を明確にする』という観点にもつながってきます。
インスタンス変数のみでなく引数にもfinalをつけることで、中で何が行われているのか理解しやすいコードを書きやすくなります。
次にプリミティブ型についてです。
プリミティブ型を使うと独自のバリデーションを値に持たせることができません。
上で紹介した『自分の身は自分で守る』が実現しづらくなってしまいます。
例えばお金を表すオブジェクトをint型で使用するのではなく、Moneyオブジェクトなどを自作し、コンストラクタに『0未満にはならない』などの成約を課すことで堅牢性が高まります。
【4章 普遍の活用】
finalを使う
「変数への再代入は良くない」と強く言われています。
再代入することによって本来の趣旨と役割がずれてしまうためです。
新たな変数を用意して、役割を明確にしましょう。
そのためにfinalを使うことが推奨されています。
finalにすることで再代入が不可になるので、仕組みで解決することができます。
デフォルトは不変にするのがよいそうです。
finalを付けたフィールドを変更する
できる限りfinalをつけると、各クラスのフィールドにもfinalをつけるということになります。
そうすると値を後から修正できなくなってしまうように思えますが、本書ではフィールドを変更するのではなく、変更した値で新たにインスタンス化するということが説明されています。
これは少しやりすぎなのでは…?とも思いますが、どれだけ堅牢にすべきかと個人の好みですね。
【5章 低凝集】
ここでは低凝集を引き起こさないための方法がいくつか挙げられています。
具体的には以下のような観点で述べれられています。
staticメソッドを避ける
初期化ロジックをメソッド化
多すぎる引数は避ける
プリミティブ型に執着しない
メソッドチェーンは避ける
いくつか気になった部分についてまとめます。
staticメソッド
staticメソッドはインスタンス変数をメソッド内で利用できないため、低凝集の元とされています。
ここで個人的になるほどと感じたことは『インスタンスメソッドのフリをしたstaticメソッド』です。
試しにメソッドにstaticを付けてみて、コンパイルエラーにならなければそのメソッドは実質staticメソッドです。
これも低凝集の元になっているので、適切な箇所に書き換えることを検討したほうが良いかも知れません。
プリミティブ型に執着しない
自作のクラスを作成しようという話です。
これも人によって大きく好みが分かれそうですね。
自作クラスのいいところは、もちろんロジックを1箇所にまとめることができることです。
また、引数の数を減らすことにも繋がります。
意味のあるまとまりにすることで、このクラス内の変数を引数で1つずつ渡すよりもそのクラスごと渡したほうが、間違った値を代入するリスクや視認性がグッと改善されます。
【6章 条件分岐】
早期return
条件分岐を浅くするための基本ではないでしょうか。
私もネストが深くなりそうなときはメソッド化して早期returnでどうにかできないか考えることが多いです。
if(value >= 90) return "S";
if(value >= 80) return "A";
if(value >= 60) return "B";
return "C";
のようにif文を1行で書けばかなり完結に書くことができます。
ストラテジパターン
これはif文やswitch文ではなく、インターフェースの実装などで処理の切り替えを行うやり方です。
これには以下のようなメリットがあります。
switch文を多数書かなくて済む
修正漏れをなくせる(コンパイルエラーになる)
端的に実装できる
この際、リスコフの置換原則に従うよう注意が必要とされています。
これは子のオブジェクトは親オブジェクトに置換しても正常に動かなくてはならないという原則です。
せっかくストラテジパターンで切り替えられるようにしても、クラスを条件にswitch文などを使ってしまっては旨味がなくなってしまいます。
工夫を凝らして見通しの良いプログラムを書きたいですね。
【7章 コレクション】
車輪の再発明
Listなどの操作ではstreamを使うと非常に簡単に書ける場合ば多いです。
しかし、便利な書き方を知らないばかりに自分で実装してしまうこともあります。
これを『車輪の再発明』と呼んでいます。
自分で実装するとコードが冗長になる、バグの温床になるなどデメリットがおおくなります。
知ってるか知らないかの勝負になってくるので、たくさんのコードと触れ合って知識を増やしていきたいです…。
ファーストクラスコレクション
これはコレクションをプリミティブ型のように扱い、それをラップして堅牢性を高めるデザインパターンです。
Listへの要素追加、削除などはすべてこのクラスのメソッドで行います。
Listを外部へ渡すときはそのまま渡してしまうといくらでも変更できてしまうので、以下のようにunmodifiableList()を使って渡すと変更できなくなります。
List<Item> items() {
return items.unmodifiableList()
}
【8章 密結合】
単一責任の原則
『1つのクラスの責任は1つに限定するべき』という考え方ですね。
「同じ処理の部分は共通化しよう」という意識が働きすぎると、その後の仕様変更が大変になってしまいます。
使いまわしすぎると本来とは別の目的の箇所の実装にも影響範囲が広がってしまうので、やってることは似ていても目的や持つべき責任を考えながら実装したいですね。
高凝集を意識した結果、密結合になるのもありがちです。
やはりこれも責任や目的を意識して、どのメソッドをどのクラスに書くのかやクラスを分割すべきなのかが重要になります。
インターフェースをむやみに使わない
継承は密結合の呼び寄せる危険なものだと本書では書かれています。
継承を用いたクラスに変更が加わり、あまり関連性の無い処理になってしまうとあとから見たときに「ぜんぜん違うことしてるのになんで継承してるんだ?」となってしまいそうですよね。
親クラス側で子クラスごとの条件分岐などを書いてしまった暁には、どこに何が書いてあるのか後から追うのがとてつもなく大変です…。
【9章 健全な設計】
本章ではこれまで説明したもの以外の負債の元となる事柄について説明されています。
挙げられている中の一部には以下のようなものがあります。
デッドコード
YAGNI原則
nullの許容
例外の握り潰し
YAGNI原則とは、必要になったときに実装するという原則です。
「いつか使いそうだからとりあえず作っておこう」は良くないですね。
結果的に使われることの無いデッドコードになる恐れがあります。
nullの許容については呼んでいて「そうだよなぁ…」となってしまいました。
普段業務でコードを書いていて、今扱っている値はnullが入る可能性があるのかを考えることが多く、バグの元や脳のリソースを削ぎ落とすことにつながると実感しているからです。
【10章 名前設計】
クラス名が中身を決める
本書では命名について、『意味範囲が狭い、目的ベース』などのことが重要とされています。
最初に実装したときは大雑把な名前でも疎結合高凝集な実装ができるかも知れません。
しかし別の人が後から機能追加をしようとした際、意味範囲の広い名前を見て、「とりあえずここに書いておこう」となり次第にそのクラスが担う役割が広がってしまいます。
8章でも上げた『単一責任の原則』に沿わなくなってしまいますね。
そのためより具体的で何をするためのクラスなのかを名前に詰める必要があります。
メソッド名
『動詞 + 目的語』で表されるメソッド名には注意が必要と書かれています。
これは本来このクラスにあるべきではないメソッドである可能性が高いためです。
これまでも似たような説明をしてきましたが、別の関心事に対するメソッドは本来はその関心事を表すクラスにあるべきです。
低凝集の元となり、同じロジックが複数箇所に書かれることに繋がります。
理想的なのは動詞一語で表せることです。
そのクラスに対し動詞のみでメソッドが定義できれば、高凝集になっている証拠と言えるでしょう。
【11章 コメント】
退化コメント
改修時にコードのみ修正され、コメントがメンテナンスされないことがあります。
こうして実装とコメントに乖離があるものを退化コメントと呼びます。
ロジックをそのまま言語化したようなコメントは、修正時に毎度メンテナンスが必要になります。
これは退化コメントになりやすい代表例と言えるでしょう。
なので、このようなコメントは避けることが本書では推奨されています。
【12章 メソッド】
コマンド・クエリ分離の原則(CQS)
『取得と更新のメソッドは分けるべき』という原則です。
理由としては、
使う側が思わぬ挙動にならないようにする
拡張性が高まる
などが挙げられます。
型を使って意味をわかりやすく
引数、戻り値の型をプリミティブ型ではなく、できる限り自作のクラスを使うことで「何を入れるべきか、何を返すのか」が明確になります。
プリミティブ型だと間違った値を引数に入れてしまったりする恐れもあるので、バグの温床になります。
また、引数と戻り値共にnullを許容しない形が望ましいです。
nullチェックをしないとNullPointExceptionが発生するメソッドは利用者が気づかないことも多く、これもまたバグの温床になると記述されています。
【13章 モデリング】
『モノ』と『モデル』は1対1ではない
モデルを考える際、目的を深く考えないと 1 つのクラスが膨大になってしまいます。
例えば商品を表す時、商品関連の処理をすべて同じクラスに書いてしまうと冗長なクラスが完成することが容易に想像できますね。 そうではなく、商品の発送、注文、予約など目的別にモデルを分けるべきとされています。
モデリングする際に 1 つのモノに対し 1 つのモデルとなってしまいがちですが、そのモノが『どんな目的を持っているのか』を考えることで細分化することができます。
モデルは現実世界のモノ単位ではなく、目的や役割別に作成することを意識したいですね。
【14章 リファクタリング】
ユニットテストは必須
既存の仕様を維持するためにユニットテストは非常に重要な役割を果たします。
リファクタリングの流れは以下のようにするとよいとされています。
クラスの雛形作成
ユニットテスト作成(この時点でテストは失敗)
とりあえずテストが通る実装をする
あるべき姿へ少しずつ修正
リファクタリング時の注意点
ここでは 2 つ紹介します。
1 つ目が『機能追加とリファクタリングを同時に行わない』です。
同時にやってしまうと、今自分が何をしているのか追いづらくなってしまいます。
これで追加した機能に更にバグが含まれてしまったらもってのほかですし…。
2 つ目が『粒度を細かく』です。
複雑なロジックのリファクタリングは、ただでさえどこをどのように修正したのか理解するのに苦労します。
細かくする事で本人はもちろん、レビューする側の人にとっても見やすい形となります。
どんな目的で修正したのか一言で表せるくらいだと見る側もありがたいですね。
【15章 設計の意義と設計への向き合い方】
コードの良し悪しの判断指標
プログラムをパッと見ただけではどこが技術的負債に陥ってるのか判断するのはなかなか難しいですよね。
そこで本書ではいくつかの指標が紹介されています。
実行可能コードの行数
目安 → メソッド:10行以内、クラス100行以内
循環的複雑度
条件分岐、ループ処理による複雑度を表す
凝集度、結合度
低凝集、密結合がよいとされる
客観的な数字があると判断しやすくなりますね。
どこを修正するのか
人やお金など様々な問題で、設計を見直すような作業がすべての箇所で行えるわけではありませんよね。
せっかくきちんと設計するなら、より効果が大きい箇所を修正したいと思うでしょう。
本書ではコアドメインとなる部分を対象とすることを推奨しています。
コアドメインとはそのサービスの売りになるビジネス領域のことです。
また、コアドメインを判断するにはドメインエキスパートの存在が重要になります。
対象のビジネス領域に対して深い知識を持つ人と協力することで、より重点的に設計を行うべき箇所を判断することができるでしょう。
【16章 開発プロセス】
割れ窓理論、ボーイスカウトの規則
『割れ窓理論』とは、現状の欠陥が放置されていると、たとえそれが軽いものでもエスカレートしてしまう事を言います。
アンカリング効果にも紐づく話であり、これはプログラミングにも当てはまります。
「既存の実装が汚いから自分も汚くていいや」と心理的に動いてしまうのです。
そこで『ボーイスカウトの規則』を適応するとよいとされています。
これは「来た時よりも綺麗にして帰る」という考え方です。
プログラミングでこの考え方が浸透すれば、品質向上に繋がりそうですよね。
規約を決める
各言語ごとにコーディング規約があるため、それを利用するとスムーズですね。
静的解析ツールを用いるとより確実に統一することができます。
規約の中でも代表的なものは命名ですね。
プロジェクトの中で何を使うのか決めておくことで、規模が大きくなった時の可読性に大きく影響しそうです。
【17章 設計技術の理解の深め方】
インプット2、アウトプット8
この数字は色々なところでよく聞きます。
「学習しよう!」と思うとついつい書籍を買い漁ったりと、インプット中心になりがちです。
自分への戒めも込めて、「学んだらやってみる」と常に意識したいです。
書籍紹介
本章ではこの書籍以外に設計の知識を深めるための本がいくつか紹介されています。
次どうするかのアクションが示されているのは、自分のような経験の浅いエンジニアにとっては大変ありがたいです。
終わりに
『良いコード/悪いコードで学ぶ設計入門』のまとめをしてきました。
本記事の内容は書籍のほんの一部であり、興味をお持ちになった方はぜひ購入して一読してみてください。
私は設計に関する書籍を読んだのはこれが初めてだったので、「なるほど!」となる事が何度もありました。
他の書籍も読み漁って、業務に活かして、を繰り返していきたいものですね。
「筆者の主張が強い」という意見も見られるようですが、個人的には私のようなエンジニア1,2年目のぺーぺーには役に立つ話がたくさん有りました。