Yanonoblog!

こつこつと

プリミティブ型執着

はじめに

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

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

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

プリミティブ型執着

コード例 (アンチパターン

下記のコードは、Common クラス内に定義された discounted_price メソッドです。

このメソッドは、定価と割引率を受け取り、割引後の価格を計算して返します。

メソッドの引数と戻り値はいずれもプリミティブ型(整数型や浮動小数点数型)で構成されています。

このようにプリミティブ型を濫用したコードをプリミティブ型執着と呼びます。

class Common
  # 割引価格を計算するメソッド
  # @param regular_price 定価
  # @param discount_rate 割引率
  # @return 割引後の価格
  def discounted_price(regular_price, discount_rate)
    if regular_price < 0
      raise ArgumentError, '定価は0以上である必要があります。'
    end
    if discount_rate < 0.0
      raise ArgumentError, '割引率は0以上である必要があります。'
    end

    # 定価から割引率を適用した価格を計算して返す
    discounted_price = regular_price * (1 - discount_rate)
    return discounted_price.to_i  # 整数型で返す
  end
end
問題点

プリミティブ型執着の問題点は以下の通りです。

  1. メソッドが特定のデータ型に依存しているため、異なるデータ型の利用が制限される可能性があります。
  2. メソッドが直接プリミティブ型を操作するため、コードの意図がわかりにくくなります。
  3. メソッドの変更や拡張が難しくなります。例えば、割引価格を計算するメソッドに税金の計算を追加する場合、プリミティブ型執着のコードでは大幅な変更が必要になる場合がある
コード例 (アンチパターン

例として、下記の適正価格を判定するための is_fair_price メソッドでは、プリミティブ型のみを使用して実装することで、定価のバリデーションが重複しています。

プリミティブ型に執着すると、関連するデータやロジックがバラバラになり、凝集度が低下します。

class Util
  # 適正価格かどうかを調べるメソッド
  def is_fair_price(regular_price)
    # 定価のバリデーションを実施
    if regular_price < 0
      raise ArgumentError.new("定価は0以上である必要があります")
    end

    # 適正価格かどうかの判定ロジックを実装
    # ... ここに適正価格の判定ロジックを追加 ...

    # 適正価格であればtrueを返す
    return true
  end
end
改善例 - ハッシュやオブジェクトを使用する

プリミティブ型の代わりにハッシュやオブジェクトを使用することで、関連するデータをまとめて渡すことができます。

これにより、引数の数を減らすことができ、凝集度を高めることができたり、回収時の影響を小さくすることができるため保守性が向上します。

# 適正価格かどうかの判定ロジックを実装
def is_fair_price(product)
  validate_regular_price(product[:regular_price])

  discounted_price = product[:regular_price] * (1 - product[:discount_rate])
  
  if discounted_price >= product[:min_fair_price] && discounted_price <= product[:max_fair_price]
    return true
  else
    return false
  end
end

# 定価のバリデーションを実施するメソッド
def validate_regular_price(regular_price)
  if regular_price < 0
    raise ArgumentError.new("定価は0以上である必要があります")
  end
end
改善例 - 具体的なクラスとして設計する

さらに保守性や拡張性を持たせる改善例が割引料金、定価、割引率を一つ一つのクラスとして扱う方法です。

定価クラスRegularPriceの中に、バリデーションをカプセル化します。割引率も同様にクラス化します。

割引料金を計算するDiscountedPriceクラスでは、RegularPriceクラスのインスタンスとDiscountRateクラスのインスタンスを受け取り、それぞれのクラスが持つ関連性の高いロジックを凝集して割引料金を計算しています。

# 定価クラス
class RegularPrice
  attr_reader :amount

  def initialize(amount)
    validate_amount(amount)
    @amount = amount
  end

  private

  def validate_amount(amount)
    raise ArgumentError, "定価は0以上である必要があります" if amount < 0
  end
end

# 割引率クラス
class DiscountRate
  attr_reader :rate

  def initialize(rate)
    validate_rate(rate)
    @rate = rate
  end

  private

  def validate_rate(rate)
    raise ArgumentError, "割引率は0以上1以下である必要があります" unless rate >= 0 && rate <= 1
  end
end

# 割引料金クラス
class DiscountedPrice
  attr_reader :amount

  def initialize(regular_price, discount_rate)
    @amount = calculate_discounted_price(regular_price, discount_rate)
  end

  private

  def calculate_discounted_price(regular_price, discount_rate)
    regular_price.amount * (1 - discount_rate.rate)
  end
end

# 使用例
regular_price = RegularPrice.new(1000)
discount_rate = DiscountRate.new(0.2)
discounted_price = DiscountedPrice.new(regular_price, discount_rate)

puts "割引料金: #{discounted_price.amount}"

これにより、各クラスが独立して値のバリデーションや計算ロジックを持ち、重複や散在したコードを避けることができます。

また、クラスを使用することでデータと関連する振る舞いをひとまとめにし、より明確かつ柔軟なコードを実現することができます。

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky