Yanonoblog!

こつこつと

ファクトリクラスを用いたクラス設計

はじめに

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

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

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

手続き型言語の設計

C言語などの手続き型言語では、データとロジックが別々に設計されます。

この考え方をそのままオブジェクト指向言語で採用すると、データとロジックが別々のクラスになります。

JavaのstaticメソッドやRailsでいうクラスメソッドではクラスののインスタンスを生成せずに使用できるため手続き的な設計ができます。

ただ、インスタンスの生成が不要な上記のメソッドでの設計はお手軽に使われがちで、使用方法を誤ると低凝集問題を引き起こしやすいため注意が必要です。

Railsではオブジェクト指向の原則に基づいて設計することが重視されます。

インスタンスメソッドを使用することで、データとロジックをひとつのクラスに結び付けることができることによりクラスの状態を保持しながらメソッドを実行することができます。

クラスメソッドはクラスの操作や処理を行うために使用され、インスタンスメソッドとの連携によりオブジェクト指向の原則に則った柔軟な設計が可能となります。

初期化ロジックの分散

十分にクラス設計しても、初期化ロジックがあちこちに分散して低凝集になってしまう場合があります。

GiftPointクラス

下記のコードでは、GiftPointクラスを定義してギフトポイントを扱うための機能を提供しています。

class GiftPoint
  MIN_POINT = 0
  attr_reader :value

  def initialize(point)
    if point < MIN_POINT
      raise ArgumentError, "ポイントが0以上ではありません。"
    end
    @value = point
  end

  # ポイントを加算する
  def add(other)
    GiftPoint.new(value + other.value)
  end

  # 残余ポイントが消費ポイント以上であればtrueを返す
  def enough?(consumption_point)
    consumption_point <= value
  end

  # ポイントを消費する
  def consume(consumption_point)
    unless enough?(consumption_point)
      raise ArgumentError, "ポイントが不足しています。"
    end
    GiftPoint.new(value - consumption_point)
  end
end
初期化のケースによっては発生する課題

ポイントの初期化や加算、消費などの操作はすべてGiftPointクラスのメソッドを通じて行われており、初期化ロジックが分散せずにまとまっているため、クラス内で完結した設計が実現されているかに見えます。

ただしインスタンスを初期化する際の以下のケースを見てみます。

# 標準会員の入会ポイント
standard_member_ship_point = GiftPoint.new(3000)

# プレミアム会員の入会ポイント
premium_member_ship_point = GiftPoint.new(10000)

標準会員の入会ポイントを表すstandard_member_ship_pointとプレミアム会員の入会ポイントを表すpremium_member_ship_pointは、

それぞれGiftPointクラスのインスタンスとして生成されています。

上記のように用途がわかれると結果的に、関連ロジックが分散しがちになり、メンテナンスが大変になります。

ファクトリメソッド

こうした初期化ロジックの分散を防ぐには、目的別のファクトリメソッドを用意します。

ファクトリメソッドとは

ファクトリメソッド(Factory Method)は、オブジェクトの生成処理をカプセル化し、インスタンスの生成を担当するメソッドのことをいいます。

具体的なクラスのコンストラクタを直接呼び出すのではなく、専用のメソッドを通じてオブジェクトの生成を行います。

使用例は後ほど後述します。

ファクトリメソッドを用いた改善例

下記が初期化ロジックの分散を解消するコードの例です。

class GiftPoint
  MIN_POINT = 0
  STANDARD_MEMBERSHIP_POINT = 3000
  PREMIUM_MEMBERSHIP_POINT = 10000

  attr_reader :value

  private_class_method :new

  def initialize(point)
    if point < MIN_POINT
      raise ArgumentError, "ポイントが0以上ではありません。"
    end

    @value = point
  end

  def self.for_standard_membership
    new(STANDARD_MEMBERSHIP_POINT)
  end

  def self.for_premium_membership
    new(PREMIUM_MEMBERSHIP_POINT)
  end
end

上記のコードでは、GiftPointクラスに目的別のファクトリメソッドを追加しました。

コンストラクタはprivateに設定されており、外部からのインスタンス生成を制限しています。

代わりに、for_standard_membershipメソッドとfor_premium_membershipメソッドを実装しており、それぞれ標準会員向けの入会ポイントとプレミアム会員向けの入会ポイントを生成します。

ファクトリメソッドの使用例

プレミアムメンバーシップで初期化する場合は、コントローラ内でファクトリメソッドを呼び出してインスタンスを生成することができます。

class UsersController < ApplicationController
  def create
    # プレミアムメンバーシップで初期化する
    gift_point = GiftPoint.forPremiumMembership

    # その他の処理...
  end
end

上記の例では、GiftPoint.forPremiumMembershipというファクトリメソッドを呼び出して、プレミアムメンバーシップ向けのギフトポイントを生成しています。

生成されたギフトポイントは変数gift_pointに代入され、それ以降の処理で利用することができます。

この設計により、インスタンス生成の責務がクラス内に集約されて初期化ロジックが分散することを防ぎ、コンストラクタの意図しない使用や誤った初期化を防止することができます。

ファクトリメソッドは外部から呼び出すことができ、目的別の初期化を行うためのインターフェースを提供します。

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky