良いコード悪いコードで学ぶ設計入門
第1章「悪しき構造の弊害を知覚する」
・変更に強い構造を知り、ギャップとして悪しき構造を認識できるようになる。
・悪しき構造:コード読解コストが高い、バグの埋込やすさ、構造劣化の負のスパイラル。
・技術駆動命名、連番命令の弊害、人間が理解できる意味をコードから外部化したときの同期漏れが混乱を招く、
・欲しい機能が実装済みであることが把握しずらい弊害。可読性が悪かったり、ネーミングが悪かったり、低凝集だったりが要因。
・未初期化状態が発生するクラスは生焼けオブジェクトの弊害。
第2章「設計の初歩」
・ネーミングで対処、再代入はやめて混乱しないように。
・意味のある単位で区分けする、関係するものは近い距離(同一クラス)に納める。
第3章「クラス設計」
・関心の分離が重要。クラスベース:データとロジックをクラスにひとまとめにする。
⇒クラスが単体で正常動作するよう設計する。
⇒インスタンス変数、
インスタンス変数を不正状態から防御し、正常に操作するメソッド
この二つが必要。
・自己防衛責務:自分の身は自分で守る。
⇒わざわざ他のクラスに初期化してもらったり、データのバリデートをしてもらわない。
・データクラス
・生焼けオブジェクト:デフォルトコンストラクタでインスタンスを生成し、インスタンス変数に個別に値を代入して初期化するもの。未初期化のリスクがあり、NullPointerExceptonが発生する。
⇒クラスのインスタンスを生成する時点で、正常値が確実に設定されている状態にする。
・ガード節:処理の対象外となる条件をメソッドの先頭に定義する方法。後続のロジックがシンプルになる。
・インスタンス変数にfinalをつけて、不変にする。
・変更したい場合は、新たなオブジェクトを作る
・引数をプリミティブ型からクラス型にして、渡し間違いを防ぐ。なるべく具象化する。
・クラス設計とは:インスタンス変数を不正な状態にしないための仕組みづくりである。
・設計パターン
- 完全コンストラクタ
⇒インスタンス変数をすべて初期化できるだけの引数を持ったコンストラクタ
⇒ガード節で不正値をはじく。
⇒正常値を持ったインスタンスのみを存在可能にする。
⇒インスタンス変数にfinalをつけることで、普遍にもなる。
- 値オブジェクト
⇒金額、割引率、税率など、数値型で表せるものをクラスとして表現する。
⇒それぞれに密接にかかわるロジックをそのクラスに入れることで高凝集にする。
⇒ミスを防ぐ効果もある。
第4章「不変の活用」
・破壊的再代入:一時変数は使い回さないこと。⇒ローカル変数にfinalをつける。
・インスタンス変数は不変にし、更新処理は新しいインスタンスを生成することで行うようにする。
・副作用:関数が、外部の状態を変更すること
⇒引数で状態を受け取り、状態変更せず、値を返すだけの関数が理想。
⇒副作用のない関数を厳密に作りこむスタイルよりも、クラス内に閉じ込めるスタイルが一般的。
・例外的に、パフォーマンスを上げたい時、局所的なスコープ内では可変とする。
・不確実性をはらむコード外とのやりとり(ファイルの操作、DB接続など)で発生する悪影響を局所化する方法としてリポジトリパターンがある。これは永続化処理をカプセル化するもの。
第5章「低凝集」
・凝集度はデータとロジックの距離の近さ。⇒高凝集な構造は変更に強い。
・staticメソッドの誤用
⇒データ保持はMoneyDataクラスで行い、メソッドはOrderManagerクラスで行っている。
⇒データとロジックが別のクラスに保持されている。
・staticメソッドはインスタンス変数を使えない
⇒データとロジックが乖離せざるを得ない。
⇒インスタンス変数と、ロジックを同じクラス内に閉じ込めた構造が高凝集。
・インスタンスメソッドのふりをしたstaticメソッド
⇒staticはついていないが、インスタンス変数を使っていないメソッド
⇒staticをつけてもエラーが出ない。インスタンス変数を使っていたら、staticをつけた時点でエラーが出る。
・初期化ロジックの分散
⇒コンストラクタを公開すると、さまざまな用途に使われがちになり、メンテナンスが大変になる。
⇒private コンストラクタ + ファクトリメソッド で目的別に初期化
⇒ファクトリメソッド化(public static of)
⇒インスタンス生成をクラス内部のみに限定する。
増えすぎて可読性が落ちてきたら生成ロジックをファクトリクラスとして分離する。
・ユーティリティクラスはstaticで書きがち
⇒単に似たようなものをまとめているだけだと低凝集に陥る。
⇒思考停止にならずどのクラスに書くべきかを意識すれば再利用性と高凝集度の両方が得られる。
・staticでUtilにしても良いのは横断的関心事。
- ログ出力
- エラー検出
- デバッグ
- 例外処理
- キャッシュ
- 同期処理
- 分散処理
・引数が多くなっている場合は関心事を絞れていない兆候。
⇒引数が多い=メソッドの中で色々なことをやりすぎている証拠。
・プリミティブ型執着は重複コードができやすくなる。
⇒一つ一つクラスにしておくと、凝集しやすくなる。
⇒この場合は、に、割引料金、定価、割引率を一つ一つクラスへ成長させます。定価クラスRegularPriceの中に、バリデーションをカプセル化します。割引率も同様にクラス化します。
⇒このように、プリミティブ型でなくクラスの型を渡すと関係のあるクラスが凝集する。
・引数が多すぎる事態を回避するには、引数がインスタンス変数になるようなクラスを作るのも手
・メソッドチェイン
⇒たとえば、 members、equipments、canChange、armorにアクセスするコードがさまざまな箇所にいくつも実装されていたとします。これらの要素に仕様変更が生じた場合、呼び出している箇所すべての影響を調べて回らなければならなくなります。
⇒また、バグが発生した場合も同様に、どこでバグが混入したのか呼び出し箇所をすべて調べて回らなければならなくなります。
・利用するオブジェクトの中身を知る必要にかられたら、それは設計がおかしい。デメテルの法則。
・尋ねずに命じるだけで利用できることが大切。
⇒インスタンスの中身を参照して、それによって自身の振る舞いを変えるのはおかしい。
⇒参照したインスタンスの中で、処理を行わせるべき。
第6章「条件分岐」
・早期リターンの活用
・単一責任選択の原則により、switch分の重複コードを回避する。つまり何かの選択肢で分岐する処理はひとつのswitch文にまとめる。これでヌケモレが防げる。さらに良いのはinterface化で分岐内容をクラスに内包させる。
・何をinterface化するかは、なんの仲間であるかの問いかけで行う。
・interfaceを用いて処理を一斉に切り替える設計をストラテジパターンという。
・interfaceが実装、クラス継承が汎化、クラス内でメンバ変数としてもつのは関連、メソッドスコープで依存先が留まっているのが依存。
・業務の関心事ルールをinterface化するのがポリシーパターン。
・リスコフの置換原則にもとづいてinterfaceを使った実装に誤りがないかをチェックする。つまり他の継承型に置換して問題なく動作するかで確認する。
第7章「コレクション」
・車輪の再発名:anyMatchがあるのにつかわないなど。
・コレクション処理も低凝集に陥りやすい。
⇒コレクションの処理はあちこちに実装されてしまいがち。
・ファーストクラスコレクションを活用し低凝集化を防ぐ
⇒コレクション処理をカプセル化する設計パターン。
⇒インスタンス変数(コレクション型)、インスタンス変数の不正な状態を防げるメソッドの2つを設定。
・参照目的で外部にわたす場合はコレクションを不変化させて返す。
⇒もとのメンバーリストは変化させず、追加したリストを返す。
・メンバーのステータス一覧を見るときなど、外部にリストを渡す時は、インスタンス変数は渡さない
⇒インスタンス変数をそのまま渡すと、外部でメンバーを追加したり削除したり出来てしまう。これでは低凝集に逆戻り。javaならunmodifiableList()メソッドをリストに付与。
第8章「密結合」
・密結合:あるクラスが、他の多くのクラスに依存している構造。
・責務:ある関心事について、正常に動作するよう制御する責任。
・通常割引と夏季割引の例
⇒割引メソッドを共通化しているため、通常割引の仕様変更時に、夏季割引でバグが発生。
①RegularPriceクラスを作り、まずそこで価格のバリデートを行う
②RegularDiscountedPriceクラスとSummerDiscountedPriceを作る
⇒クラスが通常価格、通常割引価格、夏季割引価格の責務ごとにわかれる。
⇒それぞれのクラスを変更しても、影響がない。疎結合を達成。
※RegularDiscountedPriceクラスとSummerDiscountedPriceには、現状同じ割引ロジックがあるが、重複コードではない。
⇒将来の仕様変更で、夏季割引だけ、パーセンテージが変わるかもしれない。
⇒「DRY原則」:Don't repeat Yourself。これは重複コードを許すなということではなく、システム内の知識や概念に重複を許すなということ。
⇒通常割引と、夏季割引は異なる概念。
・(触れたことがないので、飛ばした。)安易な継承はスーパークラス依存で密結合を生み出す。委譲によるコンポジション構造がベター。
・package privateをデフォルトとする。(C#ならinternal)
⇒パッケージ同士は強く関連付いたクラス同士を凝集するように設計する。
⇒外部に本当に公開したいクラスのみ限定的にpublic宣言する。
※恐らく、入門書などでpublicが普通に使われているのが原因。
⇒クラスは標準的にpackage privateとする。クラスのアクセス修飾子を省略すると、パッケージ内でのみアクセス可能になる。
・private修飾子が増えてきたら、単一責任の原則から外れてきていることを疑う。
・高凝集の誤解により、密結合になってしまうケースもある。
⇒異なる概念は分離し、疎結合にする必要がある。
・スマートUI
⇒複雑な計算ロジックや、分岐ロジックがフロント側に実装されてしまう。
⇒仕様変更時にバグに繋がりやすい。
⇒メソッド内に一連の処理手順がダラダラと長く書き連ねられている構造。いきすぎると神クラスになる。
第9章「設計の健全性を損なうさまざまな悪魔たち」
・デッドコードのクリーン化を心がける。
・YAGNIの原則。最適な形はそのときにならないとわからないから必要になったら作れば良い。
⇒仕様が確定しておらず、明確に言語化もされていない要求に対して実装しても、ほとんどの場合予測は外れ、デッドコードになる。
・マジックナンバー ⇒ static finalな定数として定義する。
・文字列型執着
・グローバル変数:多くのロジックでグローバル変数を参照していると、どのタイミングで値が書き換わったのか把握が非常に困難になる。また、排他制御が必要な場合も生じるため、デッドロックなどの可能性も増える。
・nullをプログラム上に存在させてはいけない
⇒いたるところでnullをチェックする必要がある。
※そもそもnullは、未初期化状態のメモリ領域へのアクセスは、生行状トラブルの原因になるため、それを避けるために生み出された。トラブルを防ぐ仕組み出会って、null自体はそもそも無効な扱い。
⇒nullを返さない、nullを渡さないことが大切。
⇒装備なしのときも常にインスタンスが存在するようにしておけば、null例外で落ちる心配がなくなる
・例外握りつぶし
⇒エラーが起こっても、外から検知する術が無くなってしまう
⇒コンストラクタ内で、不正なデータが渡された時点で例外をスローすれば、不正なデータを持ったインスタンス自体の存在を排除できる。
・技術駆動パッケージング
⇒設計パターンなど、構造的に似ている者同士でフォルダ分け、パッケージ分けすること。
⇒フレームワークの標準構造が技術駆動パッケージングであるためか、それをお手本に内部のフォルダ構造も技術駆動パッケージングになりがち。
⇒本来強く関係しあうファイル同士がバラバラになり、ファイル単位で低集合になる
第10章「悪魔を呼び寄せる名前」 ← この章がかなりキモ!
・目的駆動設計であるべき。関心事でより具体的な呼び名にする。(疎結合高凝集)
例)商品→配送品、予約品、注文品などにわけて、それぞれクラスを作る。
※商品は、取り扱うユースケースがたくさんあるので、様々なクラスと関係してしまいがち。また、商品クラス自体も、結びついたクラスに関係するロジックを持ち始め、どんどん巨大化してしまう。
・このようにすれば、仕様変更が生じた時に、それぞれのクラスだけに注意すればよいので、影響範囲が低くなり、ソフトウェアの価値が上がりやすくなる。
・「商品」という名前だと、あらゆる事柄に関係しそうな雰囲気があり、あらゆるロジックをひきつけてしまう強力な心理的引力が働く。⇒目的不明オブジェクト
・名前設計の便益には、可読性向上だけでなく、疎結合高凝集の実現がある。
・重要ポイント
- 具体的で、意味範囲が狭く、特化した名前を選ぶ
・名前とは無関係なロジックを排除しやすくなる。
・クラスが小さくなる。関係するクラスの個数が少なくなる。
・結合度(15.5.4参照)が低減する。
・関係クラス個数が少ないので、仕様変更時に影響範囲が小さく済む。
- 存在ベースでなく目的ベースの名前にする
・存在ベース:「ユーザ」、「商品」など。具体的な目的がないもの。
⇒意味が多重になりがち。ユーザにも、法人ユーザや、個人ユーザがいる。
- 関心事はなんなのか分析する、
- 声に出して話してみる、
- 利用規約を読んでみる
・必要な単語が載っている場合がある。
- 置き換え可能かの検討、
- 疎結合高凝集かがチェック
・目的に特化した名前を選ぶと、目的以外のロジックを寄せ付けにくくします。
・ほかのクラスと何個も関連付けられているのは良くない兆候。
・形容詞で区別が必要な時は、クラス化のチャンス。
⇒口頭でひたする形容詞をつけて同僚に説明するなど
もともと の 最大 ヒット ポイント: originalMaxHitPoint
補正 さ れ た 最大 ヒット ポイント: correctedMaxHitPoint
⇒さまざまなユースケースで利用されることが容易に想像できる。最大ヒットポイントを単純なint型変数で実装していると、意味の違いが名前で正しく表現されているかいちいち注意しなければならないし、実装が五月雨になり低凝集になりやすい。
⇒こうすれば、補正された場合の仕様が変更になった場合は、correctedMaxHitPointだけ見ればよくなる。
・データクラスに陥る名前 Info、Data
⇒「データだけ持たせるクラスなんだ、ロジックを実装しちゃダメなんだ」と読み手に印象付けてしまい、低凝集に陥る。
⇒たとえばProductInfoは、Productに改名し、インスタンス変数に強く関係するロジックをProductクラスにカプセル化する。
・DTO(Data Transfer Object)
⇒単にデータを取得して表示側に転送するだけ。インスタンスはfinalで宣言し、コンストラクタで値が確定するようにする。更新系で使用は絶対にしない。
・コマンドクエリ責務分離(CQRS)のアーキテクチャパターン。
・●●Managerも注意
⇒MemberManagerとする、メンバーに関連するものをすべて入れてしまいがちになる。
・状況によって意味が変わる言葉には注意。例)アカウント
・関心事のコンテキストの違いを考慮するべき。この場合はパッケージで分ける。
配送コンテキスト:自動車が貨物として配送されるコンテキスト。配送元、配送先、配送経路などが自動車に付いて回る。
販売コンテキスト:ディーラーにより顧客に自動車が販売されるコンテキスト。販売価格、販売オプションなどが自動車に付いて回る。
⇒コンテキストが違うものを無理やり一つのCarクラスに入れるのは、クラスが複数のコンテキストを抱えることになる。
・「動詞+目的語」形式の名前になる時は関心事が異なっている兆候あり。この場合は目的語の概念をクラスで表現し、動詞の概念でメソッドを設ける。
⇒addItemTOPartyメソッドは、敵の関心事とは明確に異なっている。
⇒アイテム獲得はダンジョンの宝箱や、重要イベントで入手する場合がある。
⇒そのたびに同じようなメソッドを作ることになる。
⇒PartyItemsクラスを作成して。そこにadd(Item newItem)メソッドを追加する。
・boolean型メソッドの追加の際は「クラス名is状態」の形に読み変えて自然な英語で読めるかで判断。
第11章「コメント」
・コメントが退化し、書かれていないほうが良い場面がある。挙動をなぞるだけのコメントは退化しやすい。具体的すぎて変更に振り回されやすいから。
・どんなに尽くしてもコメントはコードの劣化コピーにしかならない。構成と命名でわかりやすいコードにするのが良い。
第12章「メソッド」
・自身のクラスのインスタンス変数を使うように設計するのが原則。
・getter/setterは使わない。ほかのクラスのインスタンス変数を変更するメソッド構造は低凝集に陥る。その場合は、そのメソッドを別クラスに定義する。
・よそのクラスの状態を判断したりするなど、他のクラスを気にするメソッドは低凝集。
・データと操作ロジックは呼び出される側に閉じ込め、呼び出し側には手続きのみ公開する。
・コマンド(変更)とクエリ(取得)は分けてメソッド化する。
・引数は不変化する。引数を変更する場合は、新たなローカル変数に代入する。
・引数にnull渡さない設計にする。NullPointerExceptonやnullチェックなどの複雑さをもたらす。そのためには,nullに意味を持たせず、●●●.EMPTYなどで表現する。
出力引数は使わない、引数の数はなるべく少なくする。
・戻り値の型もなるべくプリミティブ型ではなく意図を織り込んだクラス型とする。
・エラーを戻り値で表すことはしない、例外で表現すること。
第13章「モデリング」
・Userクラスは、なんでも詰め込みやすく、悪いモデルになりやすい。
⇒現実の存在とモデルが1対1の関係になるとは限らない。
・動作原理やしくみが伝わりやすいように、特徴や関係性を図式化したものをモデル、このモデルを作る行為をモデリングという。
⇒最低限考慮が必要な要素を備えたものがモデル。属性を厳選してモデリングすることが大切。
・社会活動をコンピュータ内で実現するために、活動がもつ現実の構造をシステム内にも導入し、現実の変化に追従するものを作ろう。
・システム内に投影されるべきは現実世界の概念のみ。処理しているのは概念的なやりとり。
・モデルはモノではなく目的達成手段と捉える。モノだと抽象的すぎて詰め込みになりやすいので。
・特定の目的に特化させて設計することで、変更に強い講師品質な構造になる。
・モデルがいびつで不自然な時は、
①モデルが達成しようとしていることを洗い出す、
②目的それぞれ特化したモデリングをしなおす、
③目的に基づき命名する、
④目的外の要素が入り込んで以内か見直す。
・最初からうまいモデリングができることはない、日々改善が大切。
第14章「リファクタリング」
・否定の否定の命名は、ひっくり返して肯定ですんなり読めるようにする。
・意味がすんなり伝わるように、関数でラッピングする。
・どちらの帽子を被っているのかを意識して、機能追加とリファクタリングを同時に行わないようにする。
・動的型付け言語は静的型付け言語よりリファクタリングの難易度が高い。
第15章「設計の意義と設計への向き合い方」。
・コードの変更容易性が高いほど、ソフトウェアの価値を素早く高めることができる。
・エンジニアの本質的な資産は技術力。レガシーコードは誤った導きになる。スキル向上の機会を奪ってしまう。
・理想形が定義できない、説明できない状況でのむやみな練習はしないほうが良い。
理想形を知ることで技術的負債が知覚できるようになる。
・計測はコードメトリクスにもとづいて行う。
・コードが一つのメソッドに網羅されていたほうが読みやすいというのは、信頼性が低いのが根本原因。標準ライブラリのような信頼性が確保できればこの問題はクリアになる。
・条件分岐やループ処理の増加、ネストが深くなるなどの構造的読みづらさは、循環的複雑度として計測できる。
・循環的複雑度:条件分岐やループ処理などが複雑度増大につながり、バグ混入率を高める。
・凝集度:関係するロジックは近くに集める。メトリクスとしてLCOMがある。
・結合度:依存するクラスが多いほど、変更の影響が大きくなり、変更可能性が低くなる。
・短期に記憶できる数は4個であり、マジカルナンバー4という。記憶単位をチャンクという。
・コード分析ツール、Code Climate Quality、Understand、Visual Studioのコードメトリクス計測機能がある。
第16章「設計を妨げる開発プロセスとの戦い」
・コンウェイの法則:プロダクトの構造と組織構造は似てくる。
・心理的安全性、発言に対して恥じることや拒絶されることなどの不利益を被ることがないことを共有できている心理状態。
・実はテストコードも書くTDDの方が最終的なコードを完成させるまでの時間が短い。
・改善のサイクルを回し続けること、意思決定もその時のベストであり、実現されたら見直し対象となることをチーム内で共有しておく。
・多数決や全会一致で意思決定はしない。レベルの低い方に合わせた基準になってしまうから。
・ジョシュア・ツリーの法則、名前がないものや名前を知らないものは存在を知覚できない。
エンジニアとして生きる。
エンジニアとしての最初のキャリア
現在30歳。29歳の時に全くの未経験からエンジニアに転職。某大手SES企業に就職し、案件ガチャで最初の現場から開発を経験。
これまで使用した言語と開発内容は、主にC#(WinFormを使ったデスクトップアプリ開発)、Java(JSFを使ったWebアプリケーション開発)、もちろんHTML・CSSも。JavaScriptは自分の所掌範囲ではないため業務では扱っていないが、基礎的な本は読んだ程度。あとは、VBAシステムの改修を少々。
使用DBはOracle、Postgre。
モダンな言語や、Dockerやクラウド環境などの使用経験はなし。ネットワークやサーバの知識も実践で使えるものは皆無。
1年の経験のあとフリーランスに
最初に入った会社はでは、開発の経験を積ませてもらったが、経済面がかなり厳しかったのと、会社からのコンプラ違反があったので転職を考え始める。
とある伝手で自力で案件を獲得できそうだったため、特に紹介会社に所属せず、フリーランスに転身を決意。
YouTubeやTwitterでみかける華麗な転身では決してなく、やむを得ずな感じ。よくある「自由な働き方を!」というテンションでは全然ない。
ヤバかったなと思うこと
この業界に入って分かったのだが、みなさん普通ポートフォリオを作って転職活動するのね。全く知らずになんとかなるだろうということで入ったのだが、かなり非常識なやり方だったと。
まぁそりゃヤバめな会社にしか入れないわけだ。
このブログを見ている何とかなると思っている方は、とりあえず何かしらのポートフォリオを作って転職活動しましょう。
このブログでやりたいこと
1年間エンジニアとして何とか働いてきて、技術的なことはGoogleドキュメントなどに適宜まとめたり、ちょっと本とかも読んでみたので、できる限り世間様の役に立てそうな情報を提供できればと思い、登録した次第。
あとは日々の雑感をポエム的に書かせていただければと思う。
よろしくお願いします。