🍣

テストとリファクタリングがよい設計を作る -創発的設計の考え方-

2021/05/14に公開

クリーンコードに「創発」という気になる名前の章があったので読んでみた。ざっくりいうと、とりあえず動くものを作ってから後から設計するという考え方で、私も最近はこういった感じで開発している。

創発的設計

よい設計を最初に考えるのではなく、コードを改善しながらよい設計にたどり着くという設計の仕方がある。

ケント・ベック氏は下記の規則に従うと、単純な設計が行えると主張している。

  • 全テストを実行する
  • 重複をなくす
  • プログラマの意図を表現する
  • クラスとメソッドを最小限にする

全テストを実行する

テストを記述することは、設計を改善する。

最初に記述したコードに対するテストが書きにくい場合、2つの可能性が考えられる。

  • 一つのクラスやメソッドでいろいろなことをしすぎている
  • クラスやメソッドの結合度が高い

そういった場合、下記のようにコードを修正することでテストをしやすくできる。

  • クラスを小さくする
  • メソッドを小さくする
  • 1つのクラスが単一の機能を実装するようにクラスを分割する
  • クラス同士の結合度を下げる
  • インターフェースやクラスの依存関係を単純にする

このようにして、テストを単純に記述することを意識すると、自然とSRP(単一責任の原則)やDIP(依存関係逆転の原則)に導かれる。

重複の排除

重複を排除すると、より良い設計にたどり着く事がある。
下記のようなクラスがあるとする。imageはImageクラスのインスタンスである。

class ImageContainer
  def initialize(image)
    @image = image
  end

  def scale_to_one_dimesion(desired_dimesion, image_dimension)
    return if (desired_dimesion - image_dimension) < error_threshold

    scaling_factor = desired_dimension / image_dimension
    scaling_factor = scaling_factor.floor

    new_image = ImageUtilities.get_scaled_image(@image, scaling_factor, scaling_factor)
    @image.dispose
    System.gc()
    @image = new_image
  end

  def rotate(degrees)
    new_image = ImageUtilities.get_scaled_image(image, degrees)
    image.dispose
    System.gc()
    image = new_image
  end
end

明らかに重複したコードがあるので、replaceメソッドに抽出する。

class ImageContainer
  def initialize(image)
    @image = image
  end

  def scale_to_one_dimesion(desired_dimesion, image_dimension)
    return if (desired_dimesion - image_dimension) < error_threshold

    scaling_factor = desired_dimension / image_dimension
    scaling_factor = scaling_factor.floor

    replace_image(ImageUtilities.get_scaled_image(@image, scaling_factor, scaling_factor))
  end

  def rotate(degrees)
    replace_image(ImageUtilities.get_scaled_image(@image, degrees))
  end

  def replace_image(new_image)
    @image.dispose
    System.gc()
    @image = new_image
  end
end

こうしたことで、replaceメソッドは、imageに関する問題のみを扱っていることが明らかになった。すなわち、replace_imageメソッドはImageContainerではなくImageが持つべきメソッドであることがわかる。

重複を排除したことによって、クラスが持つべき責務を明らかにして、あるべき状態に変えることができる。

class Image
  def replace(new_image)
    @image.dispose
    System.gc
    @image = new_image
  end
end

class ImageContainer
  def initialize(image)
    @image = image
  end

  def scale_to_one_dimesion(desired_dimesion, image_dimension)
    return if (desired_dimesion - image_dimension) < error_threshold

    scaling_factor = desired_dimension / image_dimension
    scaling_factor = scaling_factor

    @image.replace(ImageUtilities.get_scaled_image(@image, scaling_factor, scaling_factor))
  end

  def rotate(degrees)
    @image.replace(ImageUtilities.get_scaled_image(@image, degrees))
  end
end

プログラマの意図を表現する

他のプログラマに、自分のコードの意図が分るようにすることも、設計を理解しやすくする上で重要。

  • コードの保守性は、「書き手の意図のわかりやすさ」によって決まる
  • クラス、メソッドを小さくし、適切な名前をつけることで、意図が伝わりやすくなる
  • デザインパターンのような一般に知られた概念を流用することも、意図を示すのに利用できる

クラスとメソッドを最小限にする

コードの重複排除、SRPの実践というのは、やりすぎるとむやみにメソッドとクラスを増やしすぎることになる。多すぎるクラスとメソッドは、コードディングの作業を増やし、構造を複雑にしてしまう。

先に挙げた3つの規則を満たせる中で、メソッドとクラスの数が最小限になるようにする。

参考文献

Robert C.Martin, Clean Code アジャイルソフトウェア達人の技, KADOKAWA.

Discussion