Yanonoblog!

こつこつと

共通処理クラスのアンチパターン(Common・Util)

はじめに

本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。

書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。

書籍ではJavaで説明されていたのですが本記事ではRubyに置き換えながら解説しています。

共通処理クラス(Common・Util)

共通処理クラスは、共通の処理をまとめるために用意されたクラスです。

一般的にはCommon、Utilなどと呼ばれています。

このクラスには、複数の場所で使用される共通のメソッドや処理を定義します。

共通処理クラスもまた低凝集な構造を持ち、異なるクラスや機能から共通の処理を呼び出すことが容易になるため、使い方によっては関連する処理が散在してしまい、メンテナンスや変更が困難になる場合があります。

例: 共通処理クラス

下記の共通処理クラスでは、calc_amount_including_taxというstaticメソッドが定義されています。

このメソッドは、税抜き金額と税率を受け取り、税込み金額を計算して返します。

class Common
# 税抜き金額と税率を受け取り、税込み金額を計算して返却する
  def self.calc_amount_including_tax(amount_excluding_tax, tax_rate)
    amount_excluding_tax * tax_rate
  end
end

処理の共通化により重複コードの発生を抑える効果は期待できるように思えますが、Railsでいうクラスメソッドは低凝集構造の問題を抱えています。

無関係な共通処理が雑多に置かれるアンチパターン

この実装では、関連性のない処理が一つのクラスにまとめられ、staticメソッドとして提供されています。

下記の例の問題としては、異なる目的や責任を持つメソッドが混在しており凝集度が低くなっています。

また、全ての処理がCommonクラスに集約されることで、クラスの役割が曖昧になり保守性や可読性が低下する可能性があります。

class Common
  # 税込み金額を計算する
  def self.calc_amount_including_tax(amount_excluding_tax, tax_rate)
    amount_excluding_tax * tax_rate
  end

  # ユーザーが退会済みか調べる
  def self.has_resigned?(user)
    # 省略:退会済みかどうかのロジック
  end

  # 商品を注文する
  def self.create_order(product)
    # 省略:注文処理のロジック
  end

  # 有効な電話番号であるかどうかをチェックする
  def self.valid_phone_number?(phone_number)
    # 省略:電話番号のバリデーションロジック
  end
end
改善例: 専用のクラスを作成する

共通処理をまとめるためにCommonクラスを使用するのではなく、専用のクラスを作成することで凝集度を高めることができます。

AmountIncludingTaxクラスは税込み金額に関連する責務を持ち、その属性としてvalueを持っています。

また、初期化ロジックはコンストラクタ内にカプセル化されており、外部からは値の変更ができません。

class AmountIncludingTax
  attr_reader :value

# 初期化時に税抜き金額と税率を受け取って税込み金額を計算
  def initialize(amount_excluding_tax, tax_rate)
    @value = amount_excluding_tax * tax_rate
  end
end

上記のような設計にすることで、共通処理を適切なクラスに分割し各クラスがそれぞれの責務に集中できるようになります。

また、クラス間の結合度が低くなり、保守性やテスト容易性が向上します。

先程の退会処理に関するロジックはUserクラス内に、注文処理に関するロジックはOrderクラス内に、電話番号のバリデーションに関するロジックはPhoneNumberValidatorクラス内に、それぞれ適切に配置することで凝集度を高めることができます。

##### 改善例: 専用のクラスを作成する(退会処理部分)

Userクラスに退会処理に関するロジックを配置しています。

resignメソッドを呼び出すことでユーザーを退会させ、has_resigned?メソッドでユーザーが退会済みかどうかを確認します。

class User
  def initialize(name)
    @name = name
    @resigned = false
  end

  def resign
    @resigned = true
  end

  def has_resigned?
    @resigned
  end
end

##### 改善例: 専用のクラスを作成する(注文処理部分)

Orderクラスに注文処理に関するロジックを配置しています。createメソッドを呼び出すことで注文処理を行います。

class Order
  def initialize(product)
    @product = product
  end

  def create
    # 注文処理のロジック
    # ...
  end
end

上記のようにそれぞれのクラス内に関連するロジックを配置することで、保守性や可読性が向上し、コードの理解や変更が容易になります。

アプリの規模感や要件によっては正解は異なるかもしれませんが、原則としてはオブジェクト指向設計の基本に従い、各クラスが明確な責務を持ち、適切に分離されるように設計することが重要です。

横断的関心事

概要

共通処理は専門のクラスに分けることが好ましいですが、共通処理としてまとめ上げて良いケースとして横断的関心事に関する処理があります。

横断的関心事とは、さまざまなユースケースに広く横断する事柄を指します。

例えば、ECサイトであれば注文、予約、配送、どんなユースケースでも必要となる基盤的な処理も横断的関心事に該当します。

また、もっと代表的な例としては以下の処理なども横断的関心事に該当します。

  • ログ出力
  • エラー検出
  • デバッグ
  • 例外処理
  • キャッシュ
  • 同期処理
  • 分散処理
横断的関心事は静的メソッドとして設計しても良い

例えばログ出力にはインスタンス化する必要がないため、静的メソッドとして設計します。

class Logger
  def self.report(message)
    # ログを出力するロジック
    puts "[REPORT] #{message}"
  end
end

例として、商品を買い物かごに追加する処理で例外が発生した場合に、Logger.reportメソッドを呼び出してエラーメッセージをログ出力します。

begin
  shopping_cart.add(product)
rescue ArgumentError => e
  Logger.report("問題が発生しました。買い物かごに商品を追加できません")
end

こうした横断的関心事の処理をLoggerクラスに集約することで、関連するロジックを共通化し、コードの重複を避けることができます。

また、静的メソッドとして設計することでインスタンス化する必要がなくなり、簡潔な記述が可能となります。

参考

続く…

コメント

本記事の内容は以上になります!

書籍の続きのアウトプットも随時更新したいと思います。


プログラミングスクールのご紹介 (卒業生より)

お世話になったプログラミングスクールであるRUNTEQです♪

https://runteq.jp/r/ohtFwbjW

こちらのリンクを経由すると1万円引きになります。

RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。

もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。

https://twitter.com/outputky