Yanonoblog!

こつこつと

if文とelse句のネストを解消する

はじめに

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

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

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

if文が多重にネストした構造

アンチパターン

下記は条件が多重に存在する要件のコードです。

RPGではプレイヤーがメンバーに行動を指示し、魔法を発動するまでの条件分岐コードになります。

# 生存しているか判定
if member.hit_point > 0
  # 行動可能かを判定
  if member.can_act?
    # 魔法力が残存しているかを判定
    if magic.cost_magic_point <= member.magic_point
      member.consume_magic_point(magic.cost_magic_point)
      member.chant(magic)
    end
  end
end
  1. memberhit_pointが0より大きいかどうかを判定し、敵の攻撃を受けて戦闘不能になっている場合は処理を終えます。
  2. can_act?メソッドで状態異常によって戦闘不能になっている場合は処理を終えます。
  3. magicの消費魔力がmemberの魔法力よりも少ないかどうかを判定し、魔法を使える場合はブロック内の処理を実行します。
  4. 魔法力が残存している場合、if文の中の処理が実行されます。memberの魔法力を消費し、magicを唱える処理を実行します。
問題点

プロジェクトの規模が大きくなるにつれて下記のようにネストが深くなったり、処理が大量に必要になるケースもあります。

if 条件
  # 数十~数百行に及ぶ何かの処理
  if 条件
    # 数十~数百行に及ぶ何かの処理
    if 条件
      # 数十~数百行に及ぶ何かの処理
    end
    # 数十~数百行に及ぶ何かの処理
  end
  # 数十~数百行に及ぶ何かの処理
end

そうなるとどこからどこまでがif文の処理ブロックなのか、読み解くのが難しくなり、コードの見通しが悪くなります。

また、変更や追加が必要な場合、関連するすべてのif文を理解して修正する必要があるため、手間がかかり、ミスのリスクも高まります。

コードを読みに来た人すべてが理解に時間を浪費しまい、チーム全体で開発生産性が低下してしまうのです。

改善例: 早期リターン

早期returnとは、条件を満たしていない場合に、returnで結果を返して処理を終了する手法です。

先程のコードに早期returnを適用します。

# 生存していない場合、処理を終了する
return if member.hit_point <= 0

# 早期returnへの変更には、条件を反転させる
# 生存していない場合には処理が続行されないため、条件を反転させて「生存している場合」の条件とする
if member.can_act?
  # 魔法の消費魔力がメンバーの魔力ポイント以下の場合に処理を実行する
  if magic.cost_magic_point <= member.magic_point
    member.consume_magic_point(magic.cost_magic_point)
    member.chant(magic)
    # ここから先の処理を記述する
  end
end

最初のreturn文では、メンバーが生存していない場合に処理を終了することを示しています。

このようなコードの置き換えにより、条件分岐が分かりやすくなり、処理の意図が明確になります。

また、早期returnを利用することで、必要のない処理をスキップし、パフォーマンスの向上を図ることができます。

このようなコードの置き換えにより、条件分岐が分かりやすくなり、処理の意図が明確になります。

また、早期returnを利用することで、必要のない処理をスキップし、パフォーマンスの向上を図ることができます。

他の処理も早期リターンにしたコードです。

# メンバーの体力が0以下の場合、処理を終了する
return if member.hit_point <= 0

# メンバーが行動できない場合、処理を終了する
return unless member.can_act?

# メンバーの魔力ポイントが魔法の消費魔力よりも少ない場合、処理を終了する
return if member.magic_point < magic.cost_magic_point

# 魔力ポイントを消費し、魔法の詠唱を行う
member.consume_magic_point(magic.cost_magic_point)
member.chant(magic)

条件ロジックと実行ロジックを分離できるという利点もあります。

else句を早期returnに置き換え

else句も、見通しを悪化させる要因のひとつです。

アンチパターン

値の範囲に応じて状態を切り替える際にelse句が使われがちで、rubyのコードにすると以下のようになります。

hit_point_rate = member.hit_point.to_f / member.max_hit_point

# メンバーの体力割合に応じて適切な健康状態を返す
if hit_point_rate == 0
  return HealthCondition.dead
elsif hit_point_rate < 0.3
  return HealthCondition.danger
elsif hit_point_rate < 0.5
  return HealthCondition.caution
else
  return HealthCondition.fine
end
改善例

こちらも条件分岐のネストと同様に、早期returnにより解決できます。各ifブロック内の処理を、returnに置き換えます。

hit_point_rate = member.hit_point.to_f / member.max_hit_point

return HealthCondition.dead if hit_point_rate == 0
return HealthCondition.danger if hit_point_rate < 0.3
return HealthCondition.caution if hit_point_rate < 0.5

HealthCondition.fine

returnで返してしまえば、else句は不要になります。

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky

メソッドチェーンのアンチパターン

はじめに

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

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

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

メソッドチェーン

メソッドチェーンとは

「.」(ドット)で数珠つなぎにして、戻り値の要素に次々にアクセスする書き方をメソッドチェーンと呼びます。

Rubyでは、メソッドチェーンを活用することで、複数のメソッド呼び出しを一つの式でつなげることができます。

これにより、階層的なオブジェクトの操作や条件分岐をシンプルかつ読みやすく表現することができます。

他の言語でも用いられる一般的な記法です。

アンチパターン

ゲーム内でメンバーの装備を変更するためのメソッドです。メソッドチェーンを使用しています。

@param memberId 装備変更したいメンバーのID
@param newArmor 装備する鎧

# 鎧を装備するメソッド
def equip_armor(member_id, new_armor)
  # パーティのメンバーリストから指定されたメンバーを取得し、装備変更を行います
  party.members[member_id].equipments.armor = new_armor
end

membersから装備変更したいメンバーを取得し、さらにequipmentsで装備一覧を取得しています。

その中からさらにcanChangeを取得して装備変更可能かどうかを判断し、armorへアクセスして装備変更しています。

この例ではメソッドチェーンを使い、階層構造になっているクラスの、かなり奥深い要素にアクセスしています。この方法も低凝集に陥る、良くない書き方です。

問題点
party.members[member_id].equipments.armor = new_armor

上記のコードではarmorへ代入していますが、代入するコードをどこでも書けてしまいます。似たようなコードが複数箇所に実装される恐れがあります。

それだけではなく、membersやequipmentsなども同様です。どこでもさまざまな要素へアクセス可能となります。

たとえば、members、equipments、canChange、armorにアクセスするコードがさまざまな箇所にいくつも実装されていたとします。

これらの要素に仕様変更が生じた場合、呼び出している箇所すべての影響を調べて回らなければならなくなります。

また、バグが発生した場合も同様に、どこでバグが混入したのか呼び出し箇所をすべて調べて回らなければならなくなります。

このように、影響範囲がいたずらに拡大可能な構造なので、グローバル変数と同様の性質を帯びてきます。より多くの要素に、あらゆる箇所からアクセス可能な構造である点で、単一のグローバル変数よりも悪質だと言われています。

デメテルの法則

デメテルの法則は、オブジェクト間の関係性を制限し、各オブジェクトが自身の責任範囲内でのみ操作することを促す原則です。

本著では以下のように説明されていました。

利用するオブジェクトの内部を知るべきではない、とするもので、「知らない人に話しかけるな」と要約されたりもします。

あるオブジェクトが別のオブジェクトの内部の要素に直接アクセスするのではなく、メッセージを通じて必要な情報を取得するべきです。

デメテルの法則 - 違反例

デメテルの法則に違反しているコード例を挙げます。

class User
  attr_accessor :name, :address

  def print_address
    puts self.address.city
  end
end

class Address
  attr_accessor :city
end

user = User.new
user.address = Address.new
user.address.city = "Tokyo"

user.print_address

Userクラスのprint_addressメソッド内でself.address.cityという連鎖的なアクセスが行われています。

これにより、UserクラスがAddressクラスの内部構造に直接依存しており、デメテルの法則に違反しています。

デメテルの法則 - 改善例

Userクラス内でaddress_cityというプライベートメソッドを定義し、Addressクラスの内部を意識することなく、必要な情報を取得しています。

print_addressメソッドはUserクラスの責務に集中し、Addressクラスの詳細を意識することなく実装されています。

class User
  attr_accessor :name, :address

  def print_address
    puts address_city
  end

  private

  def address_city
    address.city
  end
end

class Address
  attr_accessor :city
end

user = User.new
user.address = Address.new
user.address.city = "Tokyo"

user.print_address

これにより、UserクラスとAddressクラスの結合度が低くなり、それぞれの変更や拡張が容易になります。また、可読性と保守性も向上します。

尋ねるな、命じろ

ソフトウェア設計には、尋ねるな、命じろ(Tell,Don'tAsk.)という有名な格言があります。

この原則によれば、オブジェクトの内部状態を尋ねたり、その状態に基づいて呼び出し側で判断したりするのではなく

呼び出し側は単に命令を行い、命じられた側が適切な判断や制御を行うべきです。

具体的には以下のような設計を行います。

  1. インスタンス変数をprivateに設定し、外部からの直接アクセスを制限します。これにより、内部状態を隠蔽し、カプセル化を実現します。
  2. インスタンス変数に対する制御は、メソッドを介して行います。外部からの命令に対して、適切な処理や判断を行うメソッドを提供します。
  3. 命令された側は、受け取った命令に応じて内部の状態や振る舞いを適切に制御します。内部の詳細な判断や処理は、命令を受けたオブジェクト自身が行うようにします。

このような設計により、オブジェクトの内部状態についての責任がオブジェクト自身に集中し、呼び出し側は単に適切な命令を出すだけで済みます。

これにより、コードの結合度が低くなり、変更への影響範囲が小さくなるため、柔軟性と保守性が向上します。

先程のコードをリファクタリング

最初のコードを本著のやり方に沿って改善した例を紹介します。

equip_armorメソッドでは、装備変更のために新しい鎧を受け取り、can_changetrueである場合にのみ鎧を装備します。

これにより、防具の着脱に関するロジックがEquipmentsクラス内に凝集されます。

deactivate_allメソッドでは、全装備を解除するために各防具をEquipment.EMPTY(空の防具)に設定しています。

# 装備中の防具一覧を表現するEquipmentsクラス
class Equipments
  private

# can_change => 装備の変更が可能かどうかを表すフラグ
  attr_accessor :can_change, :head, :armor, :arm # head、armor、arm => ****それぞれ頭部防具、鎧、腕部防具を表す

  public # 公開する必要があるメソッドだけをpublicにすることで、情報の隠蔽やカプセル化を実現

  # 防具の装備
  def equip_armor(new_armor)
    self.armor = new_armor if can_change
  end

  # 全装備解除
  def deactivate_all
    self.head = Equipment.EMPTY # **Equipment.EMPTY**のように定数を定義しておくことで、空の防具を表す値を共通化できる
    self.armor = Equipment.EMPTY
    self.arm = Equipment.EMPTY
  end
end

このような設計により、防具の着脱に関するロジックがEquipmentsクラスに集約されます。

防具の仕様が変更された場合、Equipmentsクラスに注目することで修正や変更時に、他の箇所を探し回る必要がなくなるため保守性や柔軟性が向上します。

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky

プリミティブ型執着

はじめに

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

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

書籍では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

引数が増えることによる弊害

はじめに

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

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

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

多すぎる引数

低凝集に陥る良くない構造の例として引数が多すぎるメソッドなどがあります。

例 - 仕様

本著ではゲームにおける魔法力(MP)を例に説明されていました。

こういうやつですね。

image

MPに関して、以下の仕様であるとします。

  • 魔法を使うと、MPは一定量減少する。
  • 回復アイテムなどにより、MPは一定量回復する。
  • MPには最大値がある。
  • MPは最大値まで回復可能。
  • 一部の装備品は、MPの最大値を増加させる効果を持つ。
例 - コード
# 魔法力を回復するメソッド
#
# @param current_magic_point [Integer] 現在のMP残量
# @param original_max_magic_point [Integer] オリジナルのMP最大値
# @param max_magic_point_increments [Array<Integer>] MP最大値の増分の配列
# @param recovery_amount [Integer] 回復量
# @return [Integer] 回復後のMP残量
def recover_magic_point(current_magic_point, original_max_magic_point, max_magic_point_increments, recovery_amount)
  current_max_magic_point = original_max_magic_point

  # 装備品のMP最大値増加効果を適用
  max_magic_point_increments.each do |increment|
    current_max_magic_point += increment
  end

  # 回復後のMP残量を計算し、最大値を超えないように制限する
  recovered_magic_point = current_magic_point + recovery_amount
  final_magic_point = [recovered_magic_point, current_max_magic_point].min

  return final_magic_point
end

以下は解説です。

  • 各装備品のMPのプラス値の配列であるmax_magic_point_increments の要素を順に加算し、current_max_magic_point に魔法力最大値の増分を反映させます。
  • 引数に取っているMP残量であるcurrent_magic_point とデフォルトの回復量recovery_amount を加算し、recovered_magic_point に回復後の魔法力残量を計算します。
  • recovered_magic_pointcurrent_max_magic_point のうち、小さい方を選び、最終的な魔法力残量 final_magic_point として設定します。
  • final_magic_point を返します。

このメソッドによって、魔法力の回復処理が行われます。装備品の魔法力最大値増加効果が適用され、最大値を超えない範囲で魔法力が回復されます。

機能しますが、このメソッド構造は良くありません。

問題点

魔法力残量や魔法力最大値、魔法力最大値の増分、回復量がバラバラに渡されています。

バラバラに渡す方法は、不注意で正しくない値を代入してしまう可能性が高まります。

このメソッドでは、魔法力の回復処理を行っていますが、引数が多すぎるため、次のような問題が生じます。

  1. 引数の数が多いため、メソッドの使用方法を覚えるのが難しくなります。
  2. 引数の順序を間違える可能性があり、バグを引き起こす恐れがあります。
  3. メソッドの変更や追加があった場合、多くの箇所で引数を修正する必要があります。
  4. 引数の一部が関連性のない値である場合、メソッドの意図がわかりにくくなります。

これらの問題に対処するためには、メソッドの低凝集度を改善する必要があります。凝集度の高いメソッドは、単一の責務を持ち、少ない引数で動作するように設計されています。

また、回復以外の処理を行っています。

魔法力最大値の増加計算は、回復以外のさまざまなケースでの利用が容易に考えられるのに対し、このようなベタ書きロジックでは重複コードがさまざまな箇所に書かれる事態を招きます。

改善例

本著には具体的な例はなかったため要点を抑えてコードを設計しています。

合っているかはわかりませんが以下のコードになります。MagicPower クラスが魔法力に関連する機能を担当し、それぞれのメソッドが単一の責務する設計です。

class MagicPower
  attr_accessor :current, :max

  def initialize(max)
    @max = max
    @current = max
  end

  def use_magic(amount)
    @current -= amount
  end

  def recover_magic(amount)
    @current = [@current + amount, @max].min
  end

  def increase_max_magic(increment)
    @max += increment
  end
end

メソッドに引数を渡すのは、その引数を使って何か処理をさせたいからです。

引数の量が多いということは、すなわちそれだけ処理させたい内容が膨らむことになります。

処理内容が増えると、ロジックが複雑化したり重複コードが増えるため設計には特に注意が必要です。

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky

出力引数が引き起こす問題

はじめに

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

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

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

出力引数 - アンチパターン

前回の記事で解説した共通処理クラスの例にもあるように、引数の扱いを誤ると低凝集に陥りやすいです。

低凝集を引き起こす

下記のコードでは、ActorManager クラスにおいて位置を移動させるための shift メソッドを定義しています。

class ActorManager
  # ゲームキャラの位置を移動する
  def shift(location, shift_x, shift_y)
    location.x += shift_x
    location.y += shift_y
  end
end

移動対象のインスタンスであるlocationを出力引数として渡し、変更しています。

データ操作対象はLocation、操作ロジックはActorManager、といった形で別々のクラスに定義されているため、この構造も低凝集として重複を生みやすいアンチパターンのコードといえます。

全く同じメソッドが別のクラスに実装される例
class SpecialAttackManager
  # ゲームキャラの位置を移動する
  def shift(location, shift_x, shift_y)
    location.x += shift_x
    location.y += shift_y
  end
end
補足 - 出力引数

関数やメソッドの引数の一種であり、関数内で処理された結果を引数経由で外部に返すために使用されます。

先程のコードを例に取ると、shift メソッドでは位置情報を変更するために location 引数を受け取っています。

この引数はメソッド内で変更され、結果として位置情報が変更された状態で外部に返されます。

一般的には、関数やメソッドが値を返す場合は戻り値を使用し、出力引数は避けることが推奨されます。

戻り値を使用することで、関数の使用側が戻り値を受け取り、処理結果を利用できるため、コードの可読性や保守性が向上します。

引数が破壊的に変更される内容が外部からわからない

下記のコードでは割引額を設定するsetメソッドを使用していますが、出力引数を用いており、どのように変更処理が加えられているか分からずメソッド側のコードを確認しにいく必要が発生します。

# MoneyDataインスタンスを生成
money_data = MoneyData.new(1000)

# DiscountManagerインスタンスを生成
discount_manager = DiscountManager.new

# 金額を設定する
discount_manager.set(money_data)

puts "割引後の金額: #{money_data.amount}"

ロジックは下記の通りです。引数として渡したmoneyの金額値を変更しています。

引数は入力値として受け渡すのが普通です。このように出力値として扱ってしまうと、引数が入力なのか出力なのか、メソッド内部のロジックを読んで確認しなければなりません。

class DiscountManager
  # 割引を適用する
  def set(money_data)
    money_data.amount = 2000
    if money_data.amount < 0
      money_data.amount = 0
    end
  end
end

class MoneyData
  attr_accessor :amount
  
  def initialize(amount)
    @amount = amount
  end
end

メソッドの中身をいちいち気にしなければならない構造は、ロジックを読み解く時間が増え、可読性の低下を招きます。

出力引数として設計せず、オブジェクト指向設計の基本にもとづいてデータとデータを操作するロジックを同じクラスに凝集するのが好ましいです。

改善例: 引数を変更しない構造にする - 割引の実装例

修正したコードでは、MoneyDataクラスにapply_discountメソッドを追加しました。

このメソッドは、割引を適用した新しいMoneyDataインスタンスを返すため、元のインスタンスmoney_dataは変更せずに、割引が適用された新しいインスタンスdiscounted_dataを取得することができます。

# MoneyDataインスタンスを生成
money_data = MoneyData.new(1000)

# DiscountManagerインスタンスを生成
discount_manager = DiscountManager.new

# 金額を設定する
discount_manager.set(money_data)

puts "割引後の金額: #{money_data.amount}"

class DiscountManager
  # 割引を適用する
  def set(money_data)
    money_data.amount = 2000
    if money_data.amount < 0
      money_data.amount = 0
    end
  end
end

class MoneyData
  attr_accessor :amount
  
  def initialize(amount)
    @amount = amount
  end
end

改善例: 引数を変更しない構造にする - 位置移動の実装例

Location クラスの shift メソッドが引数を変更せずに新しい Location インスタンスを返すようにします。

これにより、元の位置情報が変更されずに新しい位置情報が生成されるような構造となります。

class Location
  attr_reader :x, :y
  
  def initialize(x, y)
    @x = x
    @y = y
  end

  def shift(shift_x, shift_y)
    next_x = @x + shift_x
    next_y = @y + shift_y
    return Location.new(next_x, next_y)
  end
end

# Locationインスタンスを生成
location = Location.new(0, 0)

# 位置情報を移動する
new_location = location.shift(10, 20)

puts "移動前の位置情報: (#{location.x}, #{location.y})"
puts "移動後の位置情報: (#{new_location.x}, #{new_location.y})"

shift メソッド内で現在の位置情報 (@x@y) に shift_xshift_y を加算し、新しい位置情報を生成しています。そして、新しい位置情報を表す新たな Location インスタンスを返しています。

これにより、shift メソッドの呼び出し側は元の位置情報を保持したまま、移動後の位置情報を新しいインスタンスとして取得することができます。これにより、引数の変更を避けることができ、コードの可読性や保守性が向上します。

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky

共通処理クラスのアンチパターン(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

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

はじめに

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

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

書籍では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