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
member
のhit_point
が0より大きいかどうかを判定し、敵の攻撃を受けて戦闘不能になっている場合は処理を終えます。can_act?
メソッドで状態異常によって戦闘不能になっている場合は処理を終えます。magic
の消費魔力がmember
の魔法力よりも少ないかどうかを判定し、魔法を使える場合はブロック内の処理を実行します。- 魔法力が残存している場合、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です♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。
メソッドチェーンのアンチパターン
はじめに
本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。
書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。
書籍では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.)という有名な格言があります。
この原則によれば、オブジェクトの内部状態を尋ねたり、その状態に基づいて呼び出し側で判断したりするのではなく
呼び出し側は単に命令を行い、命じられた側が適切な判断や制御を行うべきです。
具体的には以下のような設計を行います。
- インスタンス変数をprivateに設定し、外部からの直接アクセスを制限します。これにより、内部状態を隠蔽し、カプセル化を実現します。
- インスタンス変数に対する制御は、メソッドを介して行います。外部からの命令に対して、適切な処理や判断を行うメソッドを提供します。
- 命令された側は、受け取った命令に応じて内部の状態や振る舞いを適切に制御します。内部の詳細な判断や処理は、命令を受けたオブジェクト自身が行うようにします。
このような設計により、オブジェクトの内部状態についての責任がオブジェクト自身に集中し、呼び出し側は単に適切な命令を出すだけで済みます。
これにより、コードの結合度が低くなり、変更への影響範囲が小さくなるため、柔軟性と保守性が向上します。
先程のコードをリファクタリング
最初のコードを本著のやり方に沿って改善した例を紹介します。
equip_armor
メソッドでは、装備変更のために新しい鎧を受け取り、can_change
がtrue
である場合にのみ鎧を装備します。
これにより、防具の着脱に関するロジックが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です♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。
プリミティブ型執着
はじめに
本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。
書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。
書籍では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
問題点
プリミティブ型執着の問題点は以下の通りです。
- メソッドが特定のデータ型に依存しているため、異なるデータ型の利用が制限される可能性があります。
- メソッドが直接プリミティブ型を操作するため、コードの意図がわかりにくくなります。
- メソッドの変更や拡張が難しくなります。例えば、割引価格を計算するメソッドに税金の計算を追加する場合、プリミティブ型執着のコードでは大幅な変更が必要になる場合がある
コード例 (アンチパターン)
例として、下記の適正価格を判定するための 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です♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。
引数が増えることによる弊害
はじめに
本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。
書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。
書籍ではJavaで説明されていたのですが本記事ではRubyに置き換えながら解説しています。
多すぎる引数
低凝集に陥る良くない構造の例として引数が多すぎるメソッドなどがあります。
例 - 仕様
本著ではゲームにおける魔法力(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_point
とcurrent_max_magic_point
のうち、小さい方を選び、最終的な魔法力残量final_magic_point
として設定します。final_magic_point
を返します。
このメソッドによって、魔法力の回復処理が行われます。装備品の魔法力最大値増加効果が適用され、最大値を超えない範囲で魔法力が回復されます。
機能しますが、このメソッド構造は良くありません。
問題点
魔法力残量や魔法力最大値、魔法力最大値の増分、回復量がバラバラに渡されています。
バラバラに渡す方法は、不注意で正しくない値を代入してしまう可能性が高まります。
このメソッドでは、魔法力の回復処理を行っていますが、引数が多すぎるため、次のような問題が生じます。
- 引数の数が多いため、メソッドの使用方法を覚えるのが難しくなります。
- 引数の順序を間違える可能性があり、バグを引き起こす恐れがあります。
- メソッドの変更や追加があった場合、多くの箇所で引数を修正する必要があります。
- 引数の一部が関連性のない値である場合、メソッドの意図がわかりにくくなります。
これらの問題に対処するためには、メソッドの低凝集度を改善する必要があります。凝集度の高いメソッドは、単一の責務を持ち、少ない引数で動作するように設計されています。
また、回復以外の処理を行っています。
魔法力最大値の増加計算は、回復以外のさまざまなケースでの利用が容易に考えられるのに対し、このようなベタ書きロジックでは重複コードがさまざまな箇所に書かれる事態を招きます。
改善例
本著には具体的な例はなかったため要点を抑えてコードを設計しています。
合っているかはわかりませんが以下のコードになります。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です♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。
出力引数が引き起こす問題
はじめに
本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。
書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。
書籍では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_x
と shift_y
を加算し、新しい位置情報を生成しています。そして、新しい位置情報を表す新たな Location
インスタンスを返しています。
これにより、shift
メソッドの呼び出し側は元の位置情報を保持したまま、移動後の位置情報を新しいインスタンスとして取得することができます。これにより、引数の変更を避けることができ、コードの可読性や保守性が向上します。
参考
続く…
コメント
本記事の内容は以上になります!
書籍の続きのアウトプットも随時更新したいと思います。
プログラミングスクールのご紹介 (卒業生より)
お世話になったプログラミングスクールであるRUNTEQです♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。
共通処理クラスのアンチパターン(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です♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。
ファクトリクラスを用いたクラス設計
はじめに
本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。
書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。
書籍では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です♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。