🍏

『良いコード/悪いコードで学ぶ設計入門』が神本だった

2022/12/24に公開

概要

コードの書き方/設計に関する本。

https://www.amazon.co.jp/dp/4297127830/

とにかく売れに売れまくってる本なので、読んでみようかなと思い購入。
2022年12月現在、7刷重版、発行部数2万部らしい。

全部読み終わったので、備忘録がてら良かった部分をGoで書き換えて紹介する。

TL;DR

  • 和製リーダブルコード。最初に悪いコードが書かれて、「こうすると可読性上がるよね」という良いコードが後から書かれて比較する形。

  • とにかく文章が分かりやすい。平易な文章で書かれているので、ある種「漫画」のような感覚で読める。

https://twitter.com/minodriven/status/1605188056507564032?s=46&t=I6vUTaz-586_685w1irT2g

  • ある程度コード書いてる人なら「あー、こういう書き方にしちゃってる、、明日の開発から直そう」ってなるので、かなり実践的な内容になってる。

  • 個人的には、エンジニアになって半年とか、ある程度コードを書いてきた人に刺さると思う。

変数を使い回さない、目的ごとの変数を用意する

例えば、下記のようなゲームにおける敵へのダメージ計算ロジックがあったとする。

damageAmount := playerPower + playerWeaponPower
damageAmount = damageAmount - (enemyDefence + enemyArmorDefence)
if damageAmount < 0 {
  damageAmount = 0
}
return damageAmount

仕様的には
・最終的なダメージ量 == プレイヤーの総合攻撃力 - 敵の総合防御力
・敵の総合防御力の方が高ければ、敵へのダメージは0になる

という事が読み取れるが、如何せんdamegeAmountに再代入しまくっているので、可読性が低い。

これを目的ごとの変数を用意すると、下記のような感じになる。

totalPlayerPower := playerPower + playerWeaponPower
totalEnemyDefence := enemyDefence + enemyWeaponDefence
if totalPlayerPower < totalEnemyDefence {
   return 0
}
return totalPlayerPower - totalEnemyDefence

圧倒的に分かりやすい。

メモリの要件など厳密に厳しくなければ、新しい変数を作る事を恐れてはいけないなと、改めて感じた。

早期returnでネスト解消する

例えば、下記のようなゲームにおける、魔法発動までの条件分岐があったとする。

// 生存しているか判定
if 0 < member.hitPoint {
   // 行動可能か判定
   if member.canAct() {
      // 魔法力が残っているか判定
      if magic.costMagicPoint <= member.magicPoint {
         member.execMagic(magic.costMagicPoint)
      }
   }
}

ネストがとんでもなく深いので、ある条件を満たしていた場合にどこからどこまで実行されるのかなど非常に分かりにくい。

これを早期returnを駆使すると、下記のような感じになる。

// 生存しているか判定
if member.hitPoint <= 0 {
   return
}
// 行動可能か判定
if !member.canAct() {
   return
}
// 魔法力が残っているか判定
if magic.costMagicPoint > member.magicPoint {
   return
}

member.execMagic(magic.costMagicPoint)

見通しが大分良くなり、どういう条件の時に魔法が発動するのか分かりやすくなる。

ここまではある程度、コードレビューを行なっている人なら「そうだよね」となると思うが、この本では 「条件ロジックと実行ロジックを分離出来るのもメリット」 と書かれていて、その視点はなかったなと思った。

例えば、

・メンバーはテクニカルポイントを持つ
・魔法発動には所定のTPが必要

という仕様が追加になった場合。

// 生存しているか判定
if member.hitPoint <= 0 {
   return
}
// 行動可能か判定
if !member.canAct() {
   return
}
// 魔法力が残っているか判定
if magic.costMagicPoint > member.magicPoint {
   return
}
+// TPが残ってるか判定
+if member.technicalPoint < magic.costTechnicalPoint {
+   return
+}

member.execMagic(magic.costMagicPoint)

上の部分が条件ロジック、下の部分が実行ロジックになっているので、条件の追加をどこで行えば良いかは一目瞭然である。

新人エンジニアへのコードレビューする際は、こういう理由もつけてレビュー出来ると納得感出そう。

退化コメント

例えば、ゲームでプレイヤーのステータスが苦しい状態のときにtrueを返す関数があったとする。

// 毒、麻痺状態の時にtrueを返す
func (e *User) isPainful() bool {
  return e.StateType == StateType.Poison ||
         e.StateType == StateType.Paralyzed ||
	 e.StateType == StateType.Fear
}

一見、コメントを記載していて親切に思えるが、よく見ると

  • StateType.Poison = 毒
  • StateType.Paralyzed = 麻痺
  • StateType.Fear = 恐怖

なので、恐怖に対するコメントが欠けているので、コメントと異なっている。

この原因を、この本では 「コードと比べて、コメントはメンテナンスされにくいから」 と述べていて、なるほどなとなった。

実装と比べてコメントの情報が古くなった時点で、コメントは嘘をつきはじめる。
このように、情報が古くなり実装を正しく説明しなくなったコメントを 退化コメントと呼ぶ、、と書いてあった。

この例だと、特に改善したコードのサンプルは載せられていなかったが、

-
func (e *User) isPainful() bool {
  return e.StateType == StateType.Poison ||
         e.StateType == StateType.Paralyzed ||
	 e.StateType == StateType.Fear
}

今回の場合だと、コードリーディングする時に混乱するので、ロジックの挙動を説明するコメントはそもそも書かないのが良いのかなと思った。

もしそれでもロジックに対するコメントで補足しておきたい、、とかになったら 設計を見直すか、リファクタするかなどのアプローチを取りたいなと思った。

まとめ

超ざっくりになってしまったが、「値オブジェクト」「単一責任の原則」などのデザインパターンも学べるのでぜひ読んでみて欲しい。 超オススメ

Discussion