📌

[コード設計]高凝集な設計を目指す

2022/11/28に公開

初めに

エンジニアとして働き始めてから数ヶ月経ち業務に携わる中で、コード設計について学ぶ必要性があると思い、「良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方」を手に取り、学んでいます。本記事はこちらを参考に書いています。
今回は 「凝集度」 について、概念と低凝集の問題点。対する高凝集にするテクニック、について取り上げます。


前提

凝集度とは

凝集度とは、「モジュール(クラス)内における、データとロジックの関係性の強さを表す指標です

それぞれ凝集度が高い・低いとは以下の構造を表します。

  • 凝集度が高い(高凝集): クラス内のデータと、関連のあるロジックが、同じクラス内に閉じ込められている構造
  • 凝集度が低い(低凝集): クラス内のデータと、関連のあるロジックが、別のクラスにバラバラに書かれている構造


低凝集の問題

設計では、高凝集であると変更にも強く理想的と言えます。低凝集によって起こる弊害の一部として、以下のものが挙げられます。

  • 重複コード
    すでに実装済みの機能であっても一箇所にコードがまとまっていないため、別のメンバーが未実装だと誤解し、同じロジックを別の場所に書いてしまう
  • 修正漏れ
    仕様変更が重複コードに該当する場合、全てのコードを把握し修正を加える必要があり、修正漏れが起きやすい
  • 可読性低下
    コードが分散されていることによって、関連のあるロジックを探し出すのに膨大な時間がかかる




本題

低凝集に陥りやすい設計

staticメソッドの濫用

staticメソッドは、インスタンスを生成せずに使用できるメソッドです。また、インスタンス変数やクラス変数などは参照できず、独立した関数でもあります。
凝集度の観点でいうとデータクラスと合わせて使用されることで、データとロジックが別々になり低凝集になりやすいです。


staticメソッドを使用する機会としては、データを用いないようなログ出力やフォーマット変換など、凝集度に無関係なものに用いるのが良いと本書では挙げられています。


初期化ロジックの分散

コンストラクタをpublicにすると、インスタンス生成コードが至る箇所で使われ、保守がしづらくなります。


多すぎる引数

引数が多いということは、すなわちそれだけ処理させたい内容が多いことを指します。処理内容が増えるとロジックの複雑化や、ブロックの処理内容が他のところでも使用されているなど、重複コードが生まれやすく低凝集を引き起こします。




高凝集にするテクニック

意味のある単位ごとにクラス化する

先ほど述べた「多すぎる引数」に対するものになります。データをただ引数として扱うのではなく、その引数をインスタンス変数として持つクラスへ設計変更します。


privateコンストラクタ + ファクトリメソッド

「初期化ロジックの分散」に対するものになります。例として、ECサイトでよくある新規入会時のポイント付与の設計に用いています。

コンストラクタをprivateにすることで初期化をクラス内でのみ行えるようにし、生成用のメソッドを別に用意することで初期化ロジックを凝集させることができます。



以下はコード例です。本書ではJavaで書かれているのですが私自身Pythonがメインなので、Pythonに置き換えてみました。

class GiftPoint:
    __MIN_POINT = 0
    __STANDARD_MEMBERSHIP_POINT = 3000 # 通常時のポイント
    __PREMIUM_MEMBERSHIP_POINT = 5000 # キャンペーン時のポイント
    
    def __init__(self, point):
        if(point < self.__MIN_POINT):
            raise ValueError('ポイントが0以上ではありません')
        
        self.gift_point = point

    @classmethod
    def for_standard_member_ship(cls):
        return GiftPoint(cls.__STANDARD_MEMBERSHIP_POINT)
    
    @classmethod
    def for_premium_member_ship(cls):
        return GiftPoint(cls.__PREMIUM_MEMBERSHIP_POINT)

point = GiftPoint.for_premium_member_ship()
print(point.gift_point)
#=> 5000 


置き換えてみた所感として、 Pythonではコンストラクタである__init__メソッドを、privateにする方法がない(?)のが残念...というところです(もしあれば、ご指摘いただけると嬉しいです)




尋ねるな、命じろ

ソフトウェア設計において有名な格言とのことです。

インスタンス変数の値を尋ね、その値に応じて呼び出し側で処理を分けるのではなく、呼び出し側はただ命令(メソッドを呼び出す)し、命令された側が詳細な判断や制御を行うようにするします。
そのためにインスタンス変数をprivateにし、外部からアクセスできなくなる設計にします。




参考

Discussion