概要
今回こちらの本を読みました。
内容的には、コードの保守性や、可読性を高めることによって、開発スピードを上げようということを発信しています。
そのための手法はどのようにしたらよいかというのをKotlinを利用して実際のコードを書いて説明しています。
生産性について
コードを綺麗に書くこととハッキーな形で書く事について
綺麗に書くことのメリット
エンジニアは作業の多くを「コードを読む」ことに費やす。
そのため、読む負担というのを下げる必要がある。
単純に難読なコードは少しの改修でも時間がかかったりなどで負債としての影響は大きい。
ハッキーな書き方をする
上記理由につき、ハッキーになればなるほど読みにくくはなるが、反面実装スピードは早くなる。
これを天秤にかけた時、今はスピードを優先するかというのを考える必要がある。
注意しないといけない点として、ハッキーな方は実装は汚いが早いという点で、
エンジニア以外からは見えないところで手を抜いているのに、見えてる部分だけで評価されやすい
という点。
そういう場面もあるだろうが、基本的には綺麗なコードを書くことで全体の生産性を上げることも評価されるべきであるのに対し、実際はリファクタを重ねることで実装が遅くなったりすることで評価が適切にされないなどの難点がある。
綺麗なコードとは
では、綺麗なコードとはどんなものなのか。
- 意図が通じやすい明確なコード
- 独立性の高いコード
- 構造化されたコード
に対して注意を払う必要がある。
そのために、以下のことを行うと良い。
ボーイスカウトルール
「コードを見つけた時よりも良い状態にしておく」ということ。
- コードのリファクタリング
- コメントの追加や更新
- 不要なコードの削除
- テストの改善
YAGNI
「将来必要になるかもしれない」という理由で、現時点では必要のない機能やコードを追加しないという考え方
- 複雑さの低減
- 開発時間の節約
- 変更への柔軟性
KISS
Keep It Simple, Stupid原則
複雑さを避け、理解しやすく効率的な設計を実現すること
単一責任の原則
各クラスやモジュールが一つの責任を持つべきだという考え方。
早計な最適化を控える
97%の最適化は無駄だと戒めている。
コードを複雑にして最適化を行うくらいなら行わない方が良いという考え。
命名
命名にも、責任の範囲を限定したものにすることが必要。
- 曖昧性の少ない単語を選ぶ
- 紛らわしい略語を避ける
- 単位や実態を記すごくを追加する
- 肯定的な単語を用いる
曖昧な単語
- flag
- check
- old
コメント
2種類存在する。
- ドキュメンテーション
- 非形式的なコメント
ドキュメンテーション
宣言や定義に対してある決まった書式で書くコメント。
これを書く事により、IDEによってはクイックヒントが表示される。
アンチパターンとして
- 自動生成されたドキュメンテーションを放置する
- 宣言と同じ内容を繰り返す
- コードを自然言語に翻訳する
- 概要を書かない
- 実装の詳細に言及する
- コードを使う側に言及する
非形式的なコメント
コードを読む際に補助をするためのコメント
コードのまとまりにコメントを追加する。
function ...() { // ここにコメントを書く if (messageModel == null) { ... } }
状態
直交と非直交
2つの変数について、それぞれの値の取りうる範囲(変域)がもう一方の値に影響されない場合、それらの変数は互いに直交の関係にある。
コインの例を出すと
所持しているコインが1枚の時に、画面に1 coinと表示される。
2枚上の時に、画面に2 coinsと表示されるという要件の場合
コインの枚数と、表示する文字が別々で関数に渡されたりすると
10 coin といった表示になる可能性もある。
このように変数の関係によって片方に影響が与えられる状態のことを直交という。
片方の変数が決まることで、もう片方の変数も確定する場合、「従属」するという表現になる。
直和型
直交でも、従属でもない関係に対して使われる。
直和型とは、いくつかの型をまとめ、そのどれかひとつの値を持つような型のこと
列挙型
3つ以上の状態が存在し、出し分ける必要がある時は、列挙型を使う。
関数
関数の名前や仮引数、戻り値の型などの情報によって、関数の中身を読まなくても理解できるようになる。
- コメントなどで処理の要約を書く。
- コマンドクエリ分離の原則 (command-query separation, CQS)
- 定義指向プログラミング
- 早期リターン
- 操作対象による分割
コマンドクエリ分離の原則
オブジェクトの振る舞いをコマンド(状態を変更する操作)とクエリ(状態を参照する操作)に分離すること
メソッドや関数を設計する際に、状態を変更する操作(コマンド)と状態を参照する操作(クエリ)を明確に分離する。 クエリ操作では、オブジェクトの状態を変更しないことを保証し、副作用が発生しないようにする。 コマンド操作では、状態の変更のみを行い、値を返さないようにする。ただし、例外的なケースやエラー処理のために値を返すことが適切な場合もある。
コマンドクエリを適用するかどうかは、
返り値が関数の主となる結果か、それとも副次的な結果(メタデータ)かを確認するとよい
副次的な結果とは、失敗するかもしれない関数におけるエラーの種別や、データを保存したデータサイズや、状態を変更する場合は変更前の状態など
定義指向プログラミング
ネスト
- ネストはどこが重要なコードが分かりづらくなるので避ける
- プライベート関数などを使ってネストを避ける
例:
if (isValidMessage(messageId) && isViewShownFor(messageId) && isUnderstanding(messageId) && ) { showStatusText("sending") }
メソッドチェイン
こちらも同様に、チェインが長くなると把握しにくくなる。
適切な情報が得られたら、そこで一度打ち切るのが良い
例:
const friendProfileBitmaps =
userModelList
.filter(userModel => userModel.isFriend)
.map(userModel => userModel.profileBitmap);
friendProfileBitmaps.forEach(
bitmap => imageGridView.addImage(bitmap)
)
マジックナンバー
自明なものについては、ナンバーをそのまま使うのは良い。
同じ値が別の目的で使われる可能性があるものについては
定数でマジックナンバーを抜き出し、名前をつける。
早期リターン
関数の主な目的を達成できるケースをハッピーパス。
関数の主な目的を達成できないケースをアンハッピーパス。
先にアンハッピーパスを書くことで、残りをハッピーパスとみなすことができるので読む負荷が下がる。
そもそも、アンハッピーパスを作らないことを心がけるのが良い。
関数の呼び出し元でその関数を呼び出すかどうかを前もって判別できるのであれば、呼び出された関数で早期リターンを行う必要もない。
依存
結合
1.無結合(No coupling): 二つの部分が全く関連していない状態。
function functionA() { // 処理A } function functionB() { // 処理B } // functionAとfunctionBは互いに関連していない
2.データ結合(Data coupling): 二つの部分が単純なデータ(プリミティブ型)でやり取りをする状態。
function add(a, b) { return a + b; } const sum = add(3, 4); // add関数は単純なデータを受け取り、処理を行う
3.スタンプ結合(Stamp coupling): 二つの部分がデータ構造(オブジェクト、構造体など) でやり取りをする状態。
function getFullName(user) { return `${user.firstName} ${user.lastName}`; } const user = { firstName: "Taro", lastName: "Yamada", }; const fullName = getFullName(user); // getFullName関数はデータ構造(オブジェクト)を受け取り、処理を行う
4.制御結合(Control coupling): ある部分が他の部分の制御フローに影響を与える状態。
function getFullName(user) { return `${user.firstName} ${user.lastName}`; } const user = { firstName: "Taro", lastName: "Yamada", }; const fullName = getFullName(user); // getFullName関数はデータ構造(オブジェクト)を受け取り、処理を行う
5.外部結合(External coupling): 二つの部分が外部資源(ファイル、データベースなど)を共有する状態。
6.共通結合(Common coupling): 二つの部分が同じグローバル変数やシングルトンオブジェクトに依存する状態。
function getFullName(user) { return `${user.firstName} ${user.lastName}`; } const user = { firstName: "Taro", lastName: "Yamada", }; const fullName = getFullName(user); // getFullName関数はデータ構造(オブジェクト)を受け取り、処理を行う
7.内容結合(Content coupling): ある部分が他の部分の内部実装に依存する状態。
class MyClass { constructor() { this._secretValue = 42; } getValue() { return this._secretValue; } } function accessSecretValueDirectly(obj) { return obj._secretValue; } const myInstance = new MyClass(); const secretValue = accessSecretValueDirectly(myInstance); // accessSecretValueDirectly関数はMyClassの内部実装に依存している
単純なクラスから複雑なクラスへの依存
モデルなどの単純なクラスに対して、requesterのような取得に関わる複雑なクラスが依存すると便利ではあるが
requesterがデータベースやネットワークといったリソースを保持していたりすると、そのリソースを解放するためにモデルを使ってるコードを全て確認する必要がある。
モデルを通してrequesterが使われている可能性があるため。
そういう時は、関数に外部から渡すような形にすることで、モデルに組み込まないようにすることが重要。
それでも、複雑なクラスへの依存が発生してしまうパターンがある。
そういう時は、メディエータパターンを採用する。
しかし、過度な抽象化はしない方が良い。
コードレビュー
読みやすいPR、レビューしやすいPRというのがある。
PRの目的を明示的にすることは当然として
PRの分割には気をつけたいところ
大きくなりそうなPRに対しては
まずはスケルトンクラスを作るなど
こまめのコミットを行う方が良い。
また、コミットについても
機能AとBを作るに当たって
- 機能AとBを実装
- 機能AとBのテスト実装
とするより
- 機能Aと機能Aのテスト実装
- 機能Bと機能Bのテスト実装
とした方がレビュアーには優しい。
既存のコードとバッティングしてしまった時は
現在作業中のブランチから、新たにブランチを切って、元のブランチがマージされてからリベースをするのが良い。
元のブランチがマージされるまで期間があきそうなら、先にその対応だけを行ったPRを作成するのが良い。
頭に置いておく必要があるのは
レビューを受けたコメントは全てが正しい訳ではないので、全てを鵜呑みにしない方が良いということ。
まとめ
こちらの本は、愚直にコードを改善していくというところに着目していると感じました。
もちろん、プロジェクトによって、妥協する必要がある場面もあるかと思います。
しかし、それもトレードオフで受け入れるデメリットを理解し、どこかでその返済を行う必要があるでしょう。
その辺りの調整は、エンジニアだけではできない部分もあるので、いろんな人を巻き込んでの話し合いにもなるかもしれません。
そうなった時に、可読性やコードの綺麗さによって、生産性にどれほどの影響をもたらすのかというのを計測したり、プロトタイプなどを作るなどして目安を把握しておく必要があると思います。
今回読んだ中で出てきた表現には、当然別の本で読んだ表現も含まれていて
同時並行で読んでいたこともあり、理解が深まったり、より信憑性が高まったところもありました。
少し横道逸れますが、同じ系統の本を同時進行で読むことも効果が高いことを実感できたと思います。