技術書 読書感想

【技術書感想】読みやすいコードのガイドライン

スポンサーリンク

概要

「読みやすいコードのガイドライン -持続可能なソフトウェア開発のために」を読んだので感想を書こうと思います。

開発が大規模化・長期化するほど、コードを「読む」コストは増大していきます。そのため「読みやすさ」の向上は、生産性を改善し、プロダクトの成長限界を引き上げる重要な手段と言えるでしょう。 本書は、読みやすさの本質を学び、実践するための考え方をマスターできる一冊です。体系的な理解を実現するため、あらゆる角度から、豊富な例を交えて解説しています。表面的なテクニックではなく、いま目の前にあるコードに最適な改良方法を選び取る力が身に付きます。

 

スポンサーリンク

書籍概要

どんな本?

開発が大規模化・長期化するほど,コードを「読む」コストは増大していきます。そのため「読みやすさ」の向上は,生産性を改善し,プロダクトの成長限界を引き上げる重要な手段と言えるでしょう。本書は,読みやすさの本質を学び,実践するための考え方をマスターできる一冊です。体系的な理解を実現するため,あらゆる角度から,豊富な例を交えて解説しています。表面的なテクニックではなく,いま目の前にあるコードに最適な改良方法を選び取る力が身に付きます。

  • 石川宗寿 著
  • 2022年10月 発行
  • 272ページ
  • 定価2,750円(税込)

 

スポンサーリンク

内容のまとめと感想

最初は読みやすいコードを書く上で必要となる、基本的な概念(YAGNI、KISS、単一責任の原則など)から始まり、命名やコメントといった、個々のコード自体を読みやすくするにはどうするか?といった流れで説明がされています。

その後、変数(状態)の保持や更新〜関数〜クラス間の依存関係、といったより大きい視点での読みやすいコードに関しての説明がされています。

ポリモーフィズム、デザインパターンを利用したコードの構成など、そういったオブジェ指向的な設計手法などにはあまり触れておらず、あくまでコード自体の読みやすさにフォーカスしています。

面白いのは、コードレビューに関しても1つの章を使って、レビューしやすい方法に関して論じているところです。

コードの中身にばかり目が行きがちですが、この部分もしっかりとやるのは大事ですね。

 

悪い例から良い例という流れで説明

各項目の説明の流れとして、悪いコード例を先に示した上で、どうあるべきかといった良いコードを示すという形になっています。

ですので、何が悪くて改善されたのかが理解しやすい構成になっています。

 

コードはKotlinで書かれている

サンプルとして書かれているコードはKotlinになっています。

基本的にKotlin未経験者でも他の言語の経験者であれば読めるような配慮はされています。(章末にはKotlinの基本文法の説明付録などが付いています。)

それでも、個人的には、見慣れないKotlin固有の構文などが出てきて、一部読みにくく、理解が難しかったり、時間がかかったりしました。

(大体の構文は最初は慣れなくて読みにくかったですが、ある程度読み進めていけば慣れました。)

こういった特定の言語に依存しないテーマの書籍であれば、最大多数の読者を想定してJavaとかの方が良かったのではないかと思います。

 

コンパクトで読みやすい

全体でも300ページ以下で、章単位でコンパクトに内容がまとまっていて読みやすいです。

ちょっとした空き時間に1章だけ気軽に読むといった使い方でも短期間で読めてしまい、何度も読み返すにも良いボリューム感です。

 

まとめ

読みやすいコードというテーマで、コードやコメント、クラスの依存関係などに関して説明してくれる本です。

実例コードベースで悪い例と良い例を示してくれて理解しやすく、内容もコンパクトで何度も読み直せるようになっています。

関数などの具体的に小さい粒度で良いコードの書き方を学ぶ事ができるので、ボトムアップ的にすぐ使う事ができるのがメリットに感じました。

こういったテーマの定番書といえる、「リーダブルコード」にクラス関係依存関係なども盛り込んだ、より踏み込んだ内容になっているのではないかと思います。

 

よりマクロな観点で良い構造や設計などにフォーカスした「現場で役立つシステム設計の原則」などを、併せて読むことを個人的にオススメします。

他にも、「良いコード/悪いコードで学ぶ設計入門」も本書と異なる部分をカバーしていて良いのではないかと思います。

 

【技術書感想】現場で役立つシステム設計の原則

 

「ソースがごちゃごちゃしていて、どこに何が書いてあるのか理解するまでがたいへん」「1つの修正のために、あっちもこっちも書きなおす必要がある」「ちょっとした変更のはずが、本来はありえない場所にまで影響して、大幅なやり直しになってしまった」といったトラブルが起こるのは、ソフトウェアの設計に問題があるから。日本最大級となる60万件以上の求人情報サイト「イーキャリアJobSearch」の主任設計者であり、システム設計のベテランである著者が、コードの具体例を示しながら、良い設計のやり方と考え方を解説します。
本書は、より成長させやすいコードの書き方と設計を学ぶ入門書です。システム開発では、ソフトウェアの変更が難しくなる事態が頻発します。コードの可読性が低く調査に時間がかかる、コードの影響範囲が不明で変更すると動かなくなる、新機能を追加したいがどこに実装すればいいかわからない……。変更しづらいコードは、成長できないコードです。ビジネスの進化への追随や、機能の改善が難しくなります。成長できないコードの問題を、設計で解決します。

 

個人的メモ

第1章 可読性の高いコードを書くために

まずは導入部という事で、コードの可読性の定義や必要となる背景、また関連する原則に関して語られている。

他の書籍等でもよく言及されているような、定番の概念などをサクッと理解したり復習する事ができる。

コードと生産性

度々言われている事だが、プログラミング時にはコードを「書く」時間よりも、既存のコードを「読む」時間の方が長い事が多い。

可読性の高いコードはこの「読む」時間を短くできるので、将来の生産性に大きく影響を与える。(処理の理解や影響範囲の把握)

短期的な視点で見ると、コードは汚いけど開発速度は早い方が、コードは綺麗だけど開発速度は遅い場合より評価されてしまいがちなので、長期的な視点での生産性を評価できるようなチームでの価値観や方向性を共有する事が大切。

可読性の指標

  1. 単純なコード
  2. 意図が明確なコード
  3. 独立性の高いコード
  4. 構造化されたコード

代表的なプログラミング原則

  • ボーイスカウトルール:修正前より良いコードにする。
  • YAGNI:将来のための過度は拡張は不要。
  • KISS:コードをシンプルに保つ。デザパタやライブラリなどを無理に使用しない。
  • 単一責任の原則:1つのクラスの責任、関心は1つに。
  • 早計な最適化は諸悪の根源:可読性を下げる最適化は必要な時以外しない。

 

第2章 命名

可読性の高いコードの命名規則に関してまとめた章。

正確かつ説明的

命名は、正確かつ説明的であるべきである。

  • 正確:命名された意味と実際の役割が同じ
  • 説明的;名前を見ただけで何を意味するのか理解できる(例:w => widthであるべき。image => imageUrl など)

文法

英文法に近い形で語順や語形を決めると良い。

  • クラスや変数:名詞または名詞句(例:buttonHeight)
    • 最も重要な単語を最後に持ってくる
  • 関数:命令文(例;logUserAction)
    • 先頭に動詞を持ってくる

名前の示す内容

  • 良い例:何であるか?何をするか?を示す。 (例:MessageListProvider, userId)
  • 悪い例:いつ、どこで、どのように使われるかを示す。(例:onMessageRecieved, isCalledFromScreen)

単語の選択

使用する単語にも気を遣う。

  • 曖昧性の少ない単語を選ぶ
    • flag => xxxFlagの代わりにisInitializing、canInitializedなど
    • check => 何をチェックするのかわかるようにする。(checkMessageではなく、isMessageFormatValidなど)
    • old => 何と比較して古いのかわかりにくい。比較対象や条件などに着目する。(previous、 original, uneditedなど
  • 単位や実態を示す値を追加する
    • timeoutではなく、timeoutMinなど
  • 肯定的な単語を用いる
    • 否定語の否定はわかりにくい(!isDisabled)

 

第3章 コメント

コードを捕捉するための有効なコメントの書き方に関してまとめた章。

  • 種類
    • ドキュメンテーション:ヘッダとかに決まったフォーマットで書くJavaDocみたいなの
    • 非形式的:コード内に書く
  • 目的
    • コードの理解を加速させる:コードよりも抽象度の高い説明が必要。コードのまとまり単位での補足など
    • ミスを防ぐ:注意や制約を書く。非直感的でないものなどの理由や補足として
    • リファクタリングを促進する:長いコメントはリファクタリングすべきコードとわかる

第4章 状態

プログラムの状態を表す変数などの実装に関して、読み手側が理解しやすく、不正な状態を持ち込まないようにするためにどうしたら良いのか?といった点を何個かのテーマで説明する章。

これまではコードのスタイル的なものがメインであったのに対して、プログラム自体の書き方に関して説明している。

 

可変と不変

基本的に変数などは不変(イミュータブル)な方が良いとされるが、全てのケースにおいてそうというわけではない。

不変にするために、複雑性が増して読みにくいコードになるならば、可変で直感的なコードの方が良い場合もある。

(例として再起を使用した幅優先探索において、再帰を使用した不変な実装と、再帰を使用しない可変な実装)

 

複数変数における関係性

  • 直行:2つの変数間に関連性や影響がない状態
  • 非直行:2つ変数間に関連性や影響がある状態

クラス間の変数は直行であるのが望ましい。非直行は不正な状態を作り出す可能性がある。

非直行を排除する方法の例1
  • コインの枚数(数値)
  • 枚数を表示する文字 (X枚の持っています)

のようなメンバーを保持するクラスがあるとする。それぞれの変数を別途持つ事で毎数と表示の文字が不一致となる可能性がある。

  • 手法1:関数への置き換え =>枚数だけのメンバーにしてしまい、枚数を表示する文字は関数化してしまい毎数から算出する。
  • 手法2:手法1の算出にコストがかかる場合、コンストラクタをprivateにして枚数だけのファクトリ関数を提供する。コンストラクタ内の処理で、不変な変数として1度だけ文字を生成。
非直行を排除する方法の例2
  • API等の応答レスポンスクラス
    • 応答結果のデータ(失敗時にはnull)
    • 応答失敗時のエラー(成功時にはnull)

このような構造の場合、両方が非null,両方がnullといったケースが不正な組み合わせとして存在してしまう。

このような時には、直和型(IntOrBooleanのようなどちらかの値を持つ)の変数を使用する。

KotlinなどはSealdClassを使用して外で継承できない構造のクラスを作成して、内部に上記の成功時と、失敗時のクラスを作成しておく。

SealdClassが使えないならば、privateコンストラクタを使用して、ファクトリ関数で上記どちらかのパターンしか作れないようにしてしまう。

こうする事で、生成のタイミングで不正なデータが作れない構造になる。

非直行を排除する方法の例3

2つのBooleanの変数を持つ

  • 応答結果を表示
  • 失敗結果を表示

どちらもtrueになるというケースはありえない。

このような場合、enumを使用して列挙型として定義するようにする(非表示、応答結果表示、エラー表示 の3つの定義を持つenum)

 

状態遷移における設計

不変性:そもそも状態が変わらない事が一番良い。ライフサイクルが異なるものがある場合には下記のような手法で、効果的に不変性を実現できる。

異なるライフサイクルを持つデータの更新
  • ユーザ情報(氏名、電話番号など
  • オンライン状態

を保持する画面クラスがある場合、オンライン情報は頻繁に更新されるがユーザ情報は基本的に頻繁に変わらない。

このような場合、それぞれを更新するメソッドを提供するだけではなく、クラス自体も分割してしまう。

ユーザ情報モデル、オンライ状態モデルという2つのクラスを変数として保持する事で、それぞれの更新タイミングが違うということが明示化される。

 

冪等性

同じ操作を何回行っても同じ結果に事が一番望ましい。

  • Closeといった何かしらの処理を閉じるクラスの例:
    • Bad: 既にCloseした状態でCloseを呼ぶとエラーが発生する => 呼び出し元が事前条件のチェックが必要になる
    • Good:何回呼んでもエラーとならない。既にClose状態なら内部で何もしない。 =>呼び出し元はチェックが必要になる
巡回・非巡回
  • 巡回:元の状態に戻る事ができる(例:開始=>実行中=>終了 => 開始)
  • 非巡回:元の状態に戻れない (例;開始 => 実行中 => 終了 で開始には戻れない)

可変な状態を保つクラスを設計する場合、自己ループを除いて非巡回な状態となる方が望ましい。必要ならばインスタンス自体を再生成する。

巡回可能にする事で、パフォーマンスは上がるが多くの場合にはそれは早計な最適化になる。

 

第5章 関数

どのような関数が読みやすいコードとなるかを説明した章。

予測しやすい関数

関数の詳細を読まなくても理解できる予測しやすい関数とは?

  • 関数名と動作が一致している
  • 名前が十分に具体的
  • ドキュメンテーションの要約が容易に書ける

これを満たしていない関数は、1つの関数が責務を持ち過ぎているので責務の分割が必要。

コマンドとクエリの分割

分割の基準として、コマンドクエリの原則を基準とする。

  • コマンド:自身や外部の状態を変更するもの。戻り値は持たない。ただし、実行結果などの副次的な戻り値はアリ。
  • クエリ:戻り値によって情報を取得する関数。状態は変化させない。

関数の流れ

流れが明確な関数は内容を斜め読みするだけで、概要を把握できる。

流れが明確な関数の特徴は下記。

  • 詳細な挙動(ネスト、定義の右辺、エラー処理)などを読み飛ばしても理解できる
  • どの部分が重要かがわかりやすい
  • 条件分岐を網羅しなくても理解できる
定義指向プログラミング

流れが明確になるための手法。ネストやメソッドチェインなどを使用する代わりに、名前のついた変数や関数や定義を使用する。

  • ネストしない:関数の戻り値をそのまま他のメソッドの引数にしない。一度名前のついたローカル変数に入れる。
  • メソッドチェイン:無理矢理1つのチェインにまとめずに、意味の通る粒度でローカル変数に入れる。

その他に早期リターン、操作対象の分割などがある。

(操作対象の分割に関しては、個人的にあまり好みではないと感じた。ファクトリとかでまとめて切り分けるなりした方が良いと感じた。)

 

第6章 依存関係

クラス間の処理の依存関係をどのようにしたら、良い設計となるかに関して説明している章。

依存関係の観点

  • 強さ(結合)
  • 方向
  • 重複
  • 明示性

結合

内容結合

処理の内部詳細がクラス間で結合していまっているケース。最も強い結合。

アンチパターン1:不正な使い方が可能なコード

クラスAがクラスBの処理を実行する際に、クラスBの複数の処理を決まった順番で呼び出さないといけない。

緩和策として、クラスBの処理を1にまとめた上で、戻り値だけを使用する関係に直す。

 

アンチパターン2:内部状態を共有するコード

一覧情報を受け取って、表示するPresenterクラスがある。(コンストラクタで受け取る)

一覧情報は呼び出し元が渡すため、その一覧情報が他の処理でも参照、更新されている。(どこが管理してるのか不明)

このような状態の場合、一覧情報が不正な状態に更新されてしまった時に、どの処理のせいなのか原因を突き止めるのが大変。

Presenterクラスに一覧のaddなどの更新処理を用意して、直接編集させないようにする事で問題を緩和できる。

 

共通結合、外部結合

グローバル変数やシングルトン、ファイルやDBなどでデータを受け渡しする際に発生。

どこからでも参照できるものでやりとりすることになるので、影響の範囲確認が大変だったりやテスト容易性が下がる。

クラス間の話だけではなく、クラス内のメソッド間でのデータの引き渡しにメンバ変数などを使用するのも同じ。

基本的にメソッドの引数と戻り値でのやりとりに変えたり、直接シングルトンを使うのではなく、DI等でシングルトンのクラスを注入するなりする事で緩和できる。

 

制御結合

呼び出し元からフラグなどを渡して処理の分岐を行う際に該当する結合。

関連性が薄いならば、関数を分割した上で、呼び出し元でそれぞれ別の関数を呼ぶように変えるなどによって関数の複雑性を減らすことができる。

 

スタンプ結合、データ結合

データの受け渡しは関数の引数や戻り値で、制御結合がない弱い結合。

スタンプ結合は引数や戻り値にクラスや構造体などを使用するもので、データ構造はプリミティブな型だけを使用するもの。

スタンプ結合はデータの制約や単純化が必要な場合に使用が適切。

 

メッセージ結合

引数や戻り値の無い、関数の呼び出しだけの一番弱い結合。

メソッド単体が上記を満たしていても、その前後で何かしらの値を渡したり、別の処理を呼ぶ必要があったらこれまでの結合になる。

 

依存の方向

依存の方向性として、巡回がない事が望ましい。(巡回性のある例:A => B => C => A)

巡回があると、処理を追いずらかったり、処理の影響範囲が広がるデメリットがある。

巡回を完全に消すのが難しいとしても、巡回の範囲を小さい依存関係にする事が大切。

 

依存関係の方向の基準

影響を小さくするために依存の方向性を決める基準

呼び出し元 => 呼び出し先

クラス自体を渡してしまうのではなく、文字列など必要な情報だけを参照で渡す形にする事で依存を削除できる場合がある。

もしくは別の処理として切り出してしまうことで、依存を相互巡回ではなく、一方向にすることができる。

複雑・可変 => 単純・不変

例:データモデル的なものなど単純、不変なクラスが、DBアクセスするためのクラスをメンバーとして保持。

 

依存の重複

数珠繋ぎの依存

本来関係ないクラスなのに、中のクラスを使用したい場合に参照するケース。

PresenterA => Provider という関係がある上で、PresenterBがProviderを使用したいためにPresenterAを参照する。

デメテルの法則

数珠繋ぎの依存を防ぎ、直接的な依存を使うことを定義したもの。

メソッド内で発生するメンバアクセスに関して下記に限られるべきである。

  • this自身
  • thisのプロパティ
  • メソッドの引数
  • メソッド内で作られたオブジェクト
  • グローバル変数やシングルトン

NGな例:引数のメソッドの戻り値のメンバ、プロパティのプロパティ(例:this.PropA.ABC)

無理にこの法則を適応するのは良くない。あくまでそのクラスが知るべき、知らないべき情報は何なのかを意識して設計する事が大切。

 

依存の集合の重複

依存先の集合に重複があると将来の変更等で脆くなるため、依存先をまとめた中間レイヤを用いる事で解消できる。

--------

例:PresenterAとPresenterBがそれぞれ、LocalXXModelProviderとRemoteXXModelProviderに依存している。

XXModelProviderという中間レイヤとなるクラスを作成して、LocalXXModelProviderとRemoteXXModelProviderに依存させる。

PresenterAとPresenterBはXXModelProviderに依存するように変更する。

-------

このような中間レイヤの作成は最初からやる必要はない。重複が生じたタイミングで検討する。

また、この時LocalXXModelProviderとRemoteXXModelProviderは他のクラスから使えないようにアクセススコープを合わせて変更する必要がある。

 

依存の明示性

Interfaceを使用した過度な抽象化は、見た目は依存が少なくなったように見えるが、実際はコードが読みにくくなっているだけなので注意。

 

第7章 コードレビュー

これまでの読みやすいコードを書くという観点ではなく、コードレビューにおいて可読性の高いコードをレビューしてもらうためのノウハウを書いた章。

プルリクでどのような粒度でレビューしてもらうかや、ブランチの戦略などに関して書かれている。

 

-技術書, 読書感想

© 2024 nobu blog Powered by AFFINGER5