はじめに
良いコード/悪いコードで学ぶ設計入門を読んだので自分にとって重要なことをメモとして残します。
本書の良かったところ
- リーダブルコード的な内容やswitch文、interfaceといった具体的なものから、設計や開発プロセスのような抽象的な話を展開しているのでとっつきやすい。
- サンプルコードが多い。
- 常にゲームを例に設計を説明しているのでわかりやすい。
- デザインパターン、レガシーコードとの向き合い方、組織的的な問題への対応などエンジニアとして役立つ情報が幅広く載っている。
Javaの基礎知識
本書はJavaで書かれているのでJavaの記法を知っていないと読みにくいところがあります。
なので個人的に抑えておきたい記法を紹介します。
引用元が公式リファレンスでないのは許してください。(リファレンスの使い方がわかりませんでした...)
final修飾子
コードの所々に書かれているfinal
は再代入やオーバーライド、継承を禁止します。
このオブジェクトを操作できるのは最初で最後(final)って感じです。
参考記事
コンストラクタ
class Example {
Example() {
}
}
クラスの中にクラスと同じメソッド名を定義するとコンストラクタとして動作するようです。
他の言語だとinit
やconstructor
のような名前が多いですが、Javaは変わっているなと感じました。
参考記事
要約
1 悪しき構造の弊害を知覚する
分かりにくい命名やifの多重ネスト、インスタンス変数しか定義していないデータクラスといった、アンチパターンの紹介。
2 設計の初歩
1章に対して改善した書き方。
3 クラス設計 ―すべてにつながる設計の基盤―
クラス単体で正常動作することをめざし、インスタンス変数やメソッドを活用しよう。
コンストラクタでインスタンス変数を受け取るようにし、値の検証をしよう。
- ガード節
- メソッドの処理の対象外になる条件をクラスの最初らへんに書くことを指す。
- 例えばコンストラクタでインスタンス変数の検証を入れるのが挙げられる。
4 不変の活用 ―安定動作を構築する―
可変(ミュータブル)と不変(ミュータブル)を詳しく解説。
再代入、可変インスタンスの使い回しの紹介。
- 副作用
- 関数(メソッド)が値を受け取り、値を返す(主作用)以外で起きる関数外部の状態変更
- 具体的には
- インスタンス変数の変更
- グローバル変数の変更
- 引数の変更
- ファイルの読み書きなどによるI/O操作
- 関数内で宣言したローカル変数の変更は外部で変更したものでないので対象外
基本的に不変にしたほうがいい。
可変にしていいのは
- パフォーマンスに問題がある
- スコープが局所的
- ループカウントは再代入しまくっていい
リポジトリパターンはデータベースの永続化処理のカプセル化する設計。
5 低凝集 ―バラバラになったモノたち―
モジュール内における、データとロジックの関係性の強さを示す指標
高凝集だと変更に強く、低凝集だと変更に弱い。
なるべくインスタンスメソッドを使い、フォーマット変換といった凝集度に影響がない処理ならstaticメソッド使う。
様々な場所で引数の違うクラスの初期化が散らばっている場合
- 直接インスタンスを生成できないようにする。
- ファクトリメソッド or ファクトリクラスを用意して関節的にインスタンスを返す仕組みを作る。
たぶんファクトリパターンだと思う。
class Example {
private Example(final int param) {
...
}
static Example returnNormalInstance() {
return new Example(1000)
}
static Example returnSpecialInstance() {
return new Example(9999)
}
}
Common, Utilは使いすぎないようにする。ログ出力やエラー検出といった横断的関心事はstaticの共通処理として実装してOK。
出力引数という引数をいじるだけのメソッドは良くない。なんとなくわかるが、出力引数が理解しきれていない。
出力引数って検索しても同じ意味でヒットしないのが不思議。
https://qiita.com/mocha-N/items/20d7f5451fc755776cf0
メソッドチェイン(.
)でインスタンス変数を編集しないようにクラスに編集ロジックを作ろう。
尋ねるな、命じろ(Tell, Don't Ask)って格言はわかりやすかった。
6 条件分岐 ―迷宮化した分岐処理を解きほぐす技法―
アーリーリターンを使ってif文の多重ネスト、else文を回避しよう。
同じ条件式のswitch文は書かないで一箇所にまとめる。
interfaceを使いこなせると便利
Goのinterfaceはまだ怪しいのでしっかりやろうと思いました...
フラグ引数によってメソッドの振る舞いを変えるのではなく、ストラテジーパターンを実装しよう。
mockはストラテジーパターンなのかなと思いました。
https://negation.hatenadiary.org/entry/20120124/1327413251
7 コレクション ―ネストを解消する構造化技法―
コレクションに定義されているメソッドを使うとコードが短縮される。(Array.prototype.forEach()的なもの)
また、コレクションに対して独自でメソッドを定義することで関数やメソッドに渡さず処理がかけてわかりやすい。
8 密結合 ―絡まって解きほぐせない構造―
結合度とはモジュール間の依存の度合いを表す指標で、例えばクラスが他のクラスを呼び出している数を指す。
多くのクラスが依存してるクラスを作り出すと密結合となり、変更が難しくなるので避ける必要がある。
単一責任の法則: クラスの責任を一つにするべき
- DRY原則: 繰り返しを避けよ
- 繰り返しを避けるのはいいが、意味合いの違うメソッドを一纏めにするのはよくない
- たとえば、通常の割引メソッドと夏限定割引メソッドで割り引き率が同じだからまとめるみたいな
- 繰り返しを避けるのはいいが、意味合いの違うメソッドを一纏めにするのはよくない
- 継承は密結合を生みやすいので、かなり注意して使う。
- スーパークラスの仕様が変わるとサブクラスが壊れやすい
- スーパークラスに共通処理を起きがち
- 継承より委譲
- コンポジション構造を使おうというもので、継承したいと思った処理をプライベートメソッドで呼び出すだけにする
- publicメソッドが多いとprivateメソッドに変更する必要があるかも
- privateメソッドが多いとクラス分割を考える必要があるかも
- 高凝集にしようとすると密結合になってしまうことがある。疎結合高凝集をめざそう。
- 単一責任の法則に従っているか考え、クラス分割を考えよう
- スマートUI: 表示系クラスに表示ロジック以外のものが入っている
- クラスじゃないけどテンプレートとかにロジックゴリゴリに書くのもだめかも
- トランザクションスクリプトパターン: データクラスとデータクラスを処理するサービスクラスに分けること(よくわかっていない)
- 低凝集密結合
- データクラスに値を入れるときにバリデーションやエラーを飛ばす処理をしてないから、サービスクラスのメソッドに色々ロジックが入るってこと?
- 調べてみたらトランザクションスクリプトパターン自体が悪いわけではないらしい
9 設計の健全性をそこなうさまざまな悪魔たち
- デッドコードと呼ばれる実行されないコードは削除しよう
- 今必要な機能だけ実装して、将来使うかもしれない機能は作らない。デッドコードになる可能性があるから。
- ロジック中に変更されない数字を使う場合は定数として定義しよう。いきなり数字を書かれても意図がわからないし変更の手間がかかる。(マジックナンバー)
- 様々な数字を一つの文字列に詰め込むのをやめよう。splitメソッドで読むこむ前提にしないで数字ごとに変数定義をする。
- 変数、データクラスをグローバルにするのは避ける。排他制御の必要性が出てくる。
- nullは初期化状態のメモリ領域アクセスで制御トラブルを起こさないもので値として積極的に使うものではない。
- null安全の機能を使うのもいい。Kotlinはデフォルトで変数にnullを入れるとコンパイルエラーになる。変数のあとに
?
をつけるとnullを入れられる仕組み。
- null安全の機能を使うのもいい。Kotlinはデフォルトで変数にnullを入れるとコンパイルエラーになる。変数のあとに
- 例外処理はログ記録と上位レイヤークラスにエラー通知をする。
- メタプログラミング、リフレクタと呼ばれる機能は極力さけるか、狭いスコープで使う。変更不可の変数を変更したり予想しない挙動をするので注意が必要。
- 技術駆動パッケージングは避けてビジネス概念ごとにフォルダを分けるとファイルの相関関係がわかりやすい
- 技術駆動パッケージング: models, views, controllersのような設計パターン要因でフォルダを分ける
- クリーンアーキテクチャ系の本で紹介されてているのは技術駆動パッケージング?
- 最悪エディタで相関関係が追えるし設計がシンプルになるから技術駆動パッケージングをやめることには共感できなかった。
10 名前設計 ―あるべき構造を見破る名前―
- 抽象度が高い命名をするとクラスは巨大化するので分解してそれぞれを関心事に関係した命名にする。そうするとクラスは小さくなり影響範囲を小さくできる。
- 商品->予約品、注文品、在庫品、発送品
- 目的(大体がビジネス)に特化した名前をつけるには目的を深く理解する必要がある
- ラバーダッキングはデバッグとして使えるがモデルを決めるときにも使える。(悩み事なんでも使える)
- ユビキタス言語、利用規約を使って関心の分離をするのがよさそう。
- コードの説明がややこしい時(特に形容詞を頻繁に使う)は命名を変えたりクラス化したほうがいい。
- メソッド名から処理が予想しやすいようにする
- メモリやメソッドみたいな技術的なものが入った命名は避ける
- ロジックをなぞっただけの命名はさける
- メソッド名から想像のつかない挙動して驚くことがないようにする。(驚き最小の原則)
- class名 メソッド(動詞), class名 is boolean系メソッド(形容詞)で違和感がないようにメソッドを追加する
- 命名は基本的に省略しない
- 略語でも意味が通じるなら省略OK、スコープが小さいなら省略OK
11 コメント ―保守と変更の正確性を高める書き方―
- コメントは整備されにくいので内容が古くなる。命名をきちんとしたほうがいい。
- コメントは挙動をなぞるのではなく、意図や仕様変更時の注意点を書いたほうがいい。
- Java Docみたいなドキュメントを自動生成するためのコメントは良さそう
コメントというよりドキュメントって整備がめんどくさいのでなるべく書かないほうがいいかもしれない。
命名をきちんと守るとコードを書く->システムが動く+簡単なドキュメント作れる
2つの問題を一気に解決できる。
一方でJava Docのような仕組みがあるとコメントを書く->めんどくさいドキュメントが一気に生成できる、メンテナンス不要
みたいなレバレッジが効くからいいと思う。
12 メソッド(関数) ―良きクラスには良きメソッドあり―
内容は今までのまとめ
- 低凝集になるので他クラスのインスタンス変数を変更しない。
- インスタンス変数はなるべく不変にする。
- getter, setterは低凝集構造なのでおすすめしない。インスタンス変数を弄くりまわせるから。
- カプセル化とはデータとデータを操作するロジックをクラスにまとめ、必要なメソッドのみを外部公開すること
- getter, setter自体がカプセル化ではない
- コマンドクエリ分離(CQS)とはコマンドとクエリのロジックを分離する考え方。実装がシンプルになる。
- コマンド: 状態を変更する(インスタンス変数を変更する)
- クエリ: 状態を返す(戻り値を返す?)
- モディファイア: コマンドとクエリ両方やる
- 戻り値の型はプリミティブ型ではなく独自型を使おう。
- nullを渡さないことも大事。どうしてもというならempty系の値を返そう
- エラーは戻り値で返さず例外をスローする
- 例えば価格(int)を返すメソッドで-1を返したらエラーであるとすると-1がエラーであると知らないと行けないし、価格戻り値がエラーの意味も持つダブルミーニング状態になって分かりづらい
- goではerror返せばOK
13 モデリング ―クラス設計の土台―
- モデリングとは動作原理や仕組みを理解、説明するために図式化する(本書ではUMLを使う)行為
- モデルとは特定の目的達成のための最低限考慮が必要な要素を備えたもの
- 複数の目的を持ったモデルは一貫性がない
- 情報システムは物理的制約を受けないのでモデルには概念だけを実装することができる。
14 リファクタリング ―既存コードを成長に導く技―
- リファクタリングとは外からの挙動を変えずに構造を整理すること。
- クラスをリファクタリングするなら先に雛形クラスを作って既存のクラスロジックを移植してくのが良さそう。
- テストがないコードには失敗してもいいのでテストを追加して仕様をテストコードで表現する(仕様化テスト)ここすきポイント
- テストコードがある開発ではロジックを理解する->機能変更、追加->テストコード変更、追加で行けるがテストコードがない場合ロジックを理解することが困難なので仕様化テストが有効っぽい
- あるメソッドに引数と期待する結果をテストコードで書けば仕様が言語化される。
- テストコードを書かずにリファクタリングする。mainブランチにマージせずとりあえずリファクタリングして理解を深める。とりあえず手を動かせ解決法(試行リファクタリング)
- IDEの一括名前変更、メソッド抽出機能はリファクタリングには有用。
- 機能追加とリファクタリングは一緒にやらない。無駄な機能は削除する。コミット(プルリク)の粒度も細かくする。
15 設計の意義と設計への向き合い方
- 設計といっても色々な種類があって本書の設計のアンチパターンを解消するのは保守性になる。
- 設計を怠るとロジック変更やデバッグに時間がかかる。(木こりのジレンマ)
- ソースコードの読解力と技術的負債の知覚力は別。なので言語を学ぶ、なにか作るとは別に設計を学んで技術的負債の知覚力を鍛える必要がある。
- 知覚力がないエンジニアがレガシーコードに触れると間違った型を覚えることになり成長が難しい。
- 自分は今まで設計を学習したことがなく、レガシーコードを触る機会が多かったので成長が遅かったかもしれない。
- 知覚力がないエンジニアがレガシーコードに触れると間違った型を覚えることになり成長が難しい。
- 技術負債を改善した際の効果測定は難しいので下記の指標を改善していくと良いコードになる
- 実行可能コード数が一定以上ある場合はメソッド、クラスの分割をするタイミング。
- 循環複雑度は条件分岐やループ処理の多さで評価される。
- 凝集度はクラス内のデータとロジックの関係の強さを示す
- 結合度はあるクラスがいくつ他クラスに依存しているか図る指標。
- チャンクとは4±1くらいのものを一つとしてカウントする概念。人間には4±1のものが理解しやすい性質がある(マジカルナンバー4)
- だから命名は4±1の長さかロジックを区切ったりしたほうがいいのかな?具体的にはよくわかっていない
- ツール紹介
- code climate quality: コードスコアリングツール
- understand: 同じくスコアリングツールっぽい?
16 設計を妨げる開発プロセスとの戦い
設計を改善するための対組織の考え方。
- クラスを作る必要があるなら作った方がいい。クラス作成によってパフォーマンスが落ちる問題は早すぎる最適化になるので一旦考えず、時間があるときに検証する。
- 多数決で設計を決めるとコード品質は落ちるのでシニアエンジニアだけで設計する。設計だけはトップダウン。
- 正しい設計を知らないとレガシーコードに意識を持ってかれる
- アンカリング効果: 最初の情報を基準にしてしまい、その後の判断を歪める
- アンチパターンは名前を知って始めて知覚できる
- ジシュアツリーの法則: 名前を知って始めて存在を知覚、名前を知らなければ知覚できない
- メンバークラスで設計を改善したい場合根回しがいる。
- マーケットシェア理論によると10.9%のシェアがあれば無視できない影響力として認知されるので仲間を増やそう。マネージャーに相談したいときは1on1ではなく仲間と一緒にMTGする。
- フォローアップ勉強会、バッドノウハウ共有をしてチームの設計力を上げる施策も必要
17 設計技術の理解の深め方
- リファクタリングが設計力向上におすすめ
- いきなり品質の高いコードは難しいので一旦動くものを作ってあとから設計し直す。