Atsushi2022の日記

データエンジニアリングに関連する記事を投稿してます

読書メモ~良いコード/悪いコードで学ぶ設計入門

概要

「良いコード/悪いコードで学ぶ設計入門」を読んで大事そうなところをメモしておく。

書籍中で書かれているコードを一部Pythonに変換してみる。

クラス設計(第3章)

  • インスタンス変数とメソッドの両方をクラス内で作成する
  • インスタンスを生成する。基本的にクラスメソッドを使わない
  • インスタンス生成時に不正値をチェックする。不正値の場合、例外をスローする
    • メソッドの先頭に定義する処理対象外条件をガード節と呼ぶ
class StorePoint(object):

    def __init__(self, point: int): 
        if point < 0:
          raise ValueError
        self.__point = point
class StorePoint(object):

    def __init__(self, point: int): 
        self.__point = point

    def add(self, additional_point: int):
        point = self.__point + additional_point
        return StorePoint(point)

    def show(self):
        print("あなたのポイントは ", self.__point, " ptです!")
  • パフォーマンスを重視するケースでは例外的にインスタンス変数への再代入(可変)を認める
    • その場合でもインスタンス変数へ外部から自由にアクセスするのはなるべく避け、正しく操作できるメソッドでのみインスタンス変数にアクセスできるようにする
class StorePoint(object):

    def __init__(self, point: int):
        self.__point = point

    def add(self, additional_point: int):
        self.__point += additional_point

    def show(self):
        print("あなたのポイントは ", self.__point, " ptです!")
  • intstrなどのプリミティブ型を使用せずに、独自のクラス(値オブジェクト)を作成し引数に渡すことで、引数の渡し間違いを防ぐ
class StorePoint(object):

    def __init__(self, point: int):
        self.__point = point

    def add(self, store_point_class: cls):
        point = self.__point + store_point_class.__point
        return StorePoint(point)

    def show(self):
        print("あなたのポイントは ", self.__point, " ptです!")

関数(メソッド)の主作用と副作用

主作用

  • 関数が値を返す

副作用:

  • 状態変更をすること
  • ※ 関数の外にある状態を変更すること。例:インスタンス変数、引数
  • ※ 関数内のローカル変数の変更は副作用とは言えない

影響を限定しよう

  • インスタンス変数の値を変えてしまうと、結果の予測が難しく、保守が大変になる
  • データ(状態)を引数で受け取り、状態を変更せずに、値を関数の戻り値として返すのが理想的

staticメソッド(クラスメソッド)

  • staticメソッドはインスタンス変数を使えないので、データとデータ操作のロジックが乖離するので、低凝集になってしまう
class TestClass(object):

  @classmethod
  def TestMethod():
    pass
  • ログ出力用メソッドや、フォーマット変換用メソッドなど凝集度に無関係なものに使う
  • あるいは、ファクトリメソッドとしてstaticメソッドを使う

共通処理クラス(Common, Util)に気をつける

  • CommonやUtilというクラスに共通処理用のメソッドを置きがち
  • データとロジックがセットになっていないので低凝集になりがち

デメテルの法則

  • 利用するオブジェクトの内部を知るべきではない

尋ねるな、命じろ

  • ほかのオブジェクトの内部状態(つまり変数)を尋ねたり、その状態に応じて呼び出し側が判断したりするのではなく、呼び出し側はただメソッドで命ずるだけで、命令された側で適切な判断や制御するよう設計します。
  • getter/setter(ゲッター/セッター)を頻繁に使っている場合は要注意

早期return

  • 条件分岐のネストを解消する
  • 条件を反転させて、returnを返してしまう
  • 条件分岐が追加になっても、容易に追加が可能
def not_early_return(a, b, c):
    if a >= 0:
        if b >= 100:
            if c >= 300:
                pass

    return a + b + c

def early_return(a, b, c):
    if a < 0:
        return

    if b < 100:
        return

    if c < 300:
        return

    return a + b + c

インターフェース

  • クラス判定用の分岐を書かずに、show_area()で面積を計算して表示できる
class Circle(Shape):
    def __init__(self, radius):
        if radius < 0:
            raise ValueError
        self.radius = radius

    def area(self):
        return self.radius * self.radius * 3.14

class Rectangle(Shape):
    def __init__(self, width, height):
        if width < 0:
            raise ValueError

        if height < 0:
            raise ValueError

        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

def show_area(shape: Shape):
    print(shape.area())

show_area(Circle(2))
show_area(Rectangle(2,3))

継承より委譲

デッドコード、到達不能コード

  • 条件分岐で、どんな条件であっても実行されないコード
  • 可読性を低下させる。将来バグになる可能性がある
  • IDEの静的解析機能でデッドコードを検出する機能があるので、それを使うと良い

マジックナンバー

  • 説明のない数値
  • 修正漏れがあるとバグになる
  • 定数として定義するようにする

nullを返さない、渡さない

  • nullを変数に代入しない
  • nullをメソッドの戻り値にしない

例外を握り潰さない

  • try-catchで例外をキャッチして、何も処理をしないと、エラーを検知するすべがなくなってしまう
  • どこに問題があるのかわからなくなる

関心事にふさわしい命名

  • 「商品」ではなく、注文品、予約品、発送品といったように関心に合わせた命名を行う
  • 「商品」だと色んなところで使われるクラスになってしまい、巨大なクラスになってしまう
  • DataやInfoといったクラス名はつけない
  • Manager、Processor、Controllerといったクラス名は巨大化しがちなのっで要注意

コマンドとクエリの分離

  • 状態の変更(コマンド)とクエリ(状態を返す)は分離する方が使いやすい

メソッドは可能な限り動詞 1語にする

  • メソッドは可能な限り動詞 1語にする。例:add

本書で登場する設計パターン

以下の設計パターンがどういったものなのか、概要を記憶しておくようにする。

  • 完全コンストラク
  • 値オブジェクト
  • ファクトリメソッド
  • ストラテジー
  • ポリシー
  • ファーストクラスコレクション
  • リポジトリパターン

完全コンストラク

  • 不正状態から防護する設計パターン
  • コンストラクタ(インスタンス生成)のガード節で不正値を防ぐ

値オブジェクト

  • 値をクラスとして表現する設計パターン
  • 金額、日付、電話番号など様々な値を扱う
  • さらに、値を扱うためのロジックをメソッドとして備える
  • 値オブジェクトと完全コンストラクタはほぼセットで使う。オブジェクト指向設計の基本形

ファクトリメソッド

class PointClass(object):
    '''
    お客様にpointを付与するクラス
    '''
    __instance = None
    __MIN = 0
    __STANDART_JOINING_CAMPAIGN_POINT = 0
    __PREMIUM_JOINING_CAMPAIGN_POINT = 5000

    def __init__(self, point):
        if point < self.__MIN:
            raise ValueError
        self.point = point

    # 標準会員向け入会ギフトポイント
    @classmethod
    def standard_joining_point(cls):
        initial_point = cls.__STANDART_JOINING_CAMPAIGN_POINT
        return cls(initial_point)

    # プレミアム会員向け入会ギフトポイント)
    @classmethod
    def premium_joining_point(cls):
        initial_point = cls.__PREMIUM_JOINING_CAMPAIGN_POINT
        return cls(initial_point)

    def add_point(self, additional_point):
        addition = PointClass(additional_point)
        point = self.point + addition.point
        return PointClass(point)

def main():
    point_standard = PointClass.standard_joining_point()
    print(point_standard.point)

    point_premium = PointClass.premium_joining_point()
    print(point_premium.point)

    added_point_class = point_premium.add_point(3000)
    print(added_point_class.point)

ポリシーパターン

こちらがわかりやすい。

https://zenn.dev/ryutaro_h/articles/ed58ee31dcd2b4

ファーストクラスコレクション

スキップする。

コードの良し悪しを評価する指標

クラス、メソッドの行数

  • 単一責任の原則を遵守するよう設計されたクラスの行数は大体100行程度、どんなに多くても200行程度
  • Rubyのコード解析ライブラリRuboCopだと、クラスは100行が上限、メソッドは10行が上限になっている

循環的複雑度

  • 循環的複雑度(サイクロマティック複雑度)は、コードの構造的な複雑さを示す指標です。
  • 条件分岐やループ処理が増える、ネストすると複雑さは増大していきます。
  • 循環的複雑度が10以下だと非常に良い構造
  • 分析ツールにより計測可能

凝集度

  • モジュール内におけるデータとロジックの関係性の強さを表す指標
  • メトリクスとしてはLCOM(Lack of Cohesion in Methods)がある
  • 計測ツールで計測可能

結合度

  • モジュール間の依存度合を表す指標
  • 分析ツールにより計測可能

コード評価に利用できるツール

Code Climate Quality

  • コード行数、複雑度などのメトリクスに問題のある個所を可視化

Understand

  • コード行数、複雑度、凝集度、結合度などのメトリクスを計測可能

Visual Studio

  • コードメトリクス計測機能が利用可能