「良いコード悪いコードで学ぶ設計入門」メモ

これ何

良いコード悪いコードで学ぶ設計入門を読んで大事だなと思ったことを忘れないようにメモしておく。

良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方:書籍案内|技術評論社

個人的にはリーダブルコードよりも読みやすく、全てのコードを書く人におすすめできる本だと思いました。

各章ごと

3:クラス設計

  • クラスは単体で作動するように設計する
    • 自己防衛責務がある
    • ベストプラクティス
      • コンストラクタで正常に全ての初期値を設定すること。
        • 不正な値を許さないバリデーションも備えておくべき
      • 計算ロジックをクラスのメソッドとして提供しておく
        • 凝縮性が上がってメンテナンスしやすい
      • インスタンス変数を上書きできないようにする(できれば)
        • finalのような式があれば利用する
      • インスタンスを更新したい場合には、上書きするのではなく、新しいインスタンスを作成する
      • 静的型付け言語であれば、型によるバリデーションも提供できる
        • intやstringといったプリミティブ型ではなく、独自の型を定義しておくほうがバグを減らせる
    • よい設計パターン
      • 完全constructor
      • 値object
        • 値をクラス(type)として表現する方法

大事なこと:データとデータ操作ロジックを一箇所にまとめておくこと(凝集性が高い状態)。必要な操作だけを後悔すること。

# example: 物体検出問題において正解データとなるbounding boxデータを表現するクラス
class BoundingBox:
    def __init__(self, xmin: int, xmax: int, ymin: int, ymax: int, label: str):

        if not self._are_valid_positions(xmin, xmax, ymin, ymax):
            raise Exception("Invalid positions.")
        if len(label) == 0:
            raise Exception("Empty string is passed as a label name.")

        self.xmin = xmin
        self.xmax = xmax
        self.ymin = ymin
        self.ymax = ymax
        self.label = label
        return

    def _are_valid_positions(self, xmin: int, xmax: int, ymin: int, ymax: int) -> bool:
        # 座標のバリデーション
        if xmin < 0 or ymin < 0:
            return False
        if xmin >= xmax or ymin >= ymax:
            return False
        return True

    def get_bbox_area(self) -> int:
        # bboxの面積を返すメソッド
        # クラスに関連する処理はクラスのメソッドとして提供することで凝縮性を高める
         return (self.xmax - self.xmin) * (self.ymax - self.ymin)

    def slide_bbox(self, stride_x: int, stride_y: int):
        # bboxを移動するメソッド
        # データと同じ場所に操作するメソッドを定義することで凝集性を高める
        # 副作用を防ぐために、クラス変数を上書きするのではなく新しいインスタンスを作成する
        return BoundingBox(
            self.xmin + stride_x,
            self.xmax + stride_x,
            self.ymin + stride_y,
            self.ymax + stride_y,
            self.label
        )

4:不変の活用

  • 再代入をできるだけ許さない
  • 可変であることで生じること
    • 可変インスタンスがあちこちで変更されて収集がつかなくなる
    • 副作用のデメリット
      • 主作用と副作用
    • 関数の影響範囲を限定することが重要。引数で状態を受け取り、状態変更なしに値を返す関数が理想。
      • データを引数で受け取る
      • 状態を変更しない
      • 値は関数の戻り値で返却する

6: 条件分岐

  • 条件分岐が入れ子になっているといいことはない
    • 早期returnは良いsolution
  • switchが色々なところに実装されると、変更に弱いコードになる。
    • switchするのは一箇所にまとめる
    • 条件が増えていくのであればinterfaceとして共通する構造を定義するのがよい
      • 種類ごとに切り替えたい機能をinterfaceのメソッドとして提供するのがよい
      • interfaceの型で分岐処理をしたいときはinterface側に実装する
  • フラグ引数(0ならこれ、1なら違う処理、みたいな関数の引数)
    • 関数を分けましょう

7: コレクション

  • 言語がコレクションに対する処理を提供している場合は自前で実装しない。
  • ループ中に条件分岐が多くある場合には早期continueやbreakを活用する
  • コレクションに関する実装が散らばってくるときにはFirst class collectionを検討する(カプセル化)
    • コレクション型のインスタンス変数と、それらを不正状態から防御し正常に制御するためのメソッドを提供する。
@dataclass
class Member:
    name: str
    hp: int


# PartyはFirst Class Collectionとして機能する
class Party:
    def __init__(self, members: List[Member]):
        self.members = members

    def add_member(self, new_member: Member) -> Party:
        # self.membersを上書きするのではなく、新しいPartyインスタンスを返却する
        new_members = copy.deepcopy(self.members)
        new_members.append(new_member)
        return Party(members=new_members)

8:密結合

  • 単一責任の原則(クラスが担う責任はただ一つにするべき)を強く意識しておくことが疎結合性を担保するコツ
  • 重複コードを恐れない。ほとんど同じコードであっても責務や概念が異なる場合にはコードを分割することは悪いことではない。

12:メソッド

  • 他のクラスのインスタンス変数を変更しない。 変更するのは自身のインスタンス変数のみに絞る。
  • コマンド・クエリ分離(Command-Query Separation:CQS)の原則を守る
    • 状態の取得及び状態の変更のどちらかを責務とするメソッドにする。どちらも同時に行わない。
  • 引数が多くなりそうなら別クラスにまとめることを考える
    • 概念ごとにクラスに分割すれば引数が多くなりすぎることがなくなるはず
  • 戻り値
    • プリミティブ型で返すよりも独自の型を定義して返す方が安全
    • エラーは戻り値で返すのではなく例外をthrowする
      • 負数などでエラーを表現しない

14:リファクタリング

  • リファクタリングの流れ
    • ネストの解消
      • 条件の反転や早期returnでネストを解消できる
    • ロジックの入れ替え
    • 条件を読みやすくする
    • ベタガキの条件文を、目的を表すメソッドでまとめる
  • ユニットテストでバグを防ぐ
  • リファクタリングの注意点
    • 機能追加とリファクタリングを同時にやらない。レビュワーも実装する人も訳がわからなくなる
    • スモールステップで実施する