デザインパターン(完全コンストラクタ・値オブジェクト)
はじめに
本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。
書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。
書籍ではJavaで説明されていたのですが本記事ではRubyに置き換えながら解説しています。
デザインパターン(設計パターン)
ソフトウェア設計において直面する問題を、高凝集化などプログラム構造を改善する設計手法をデザインパターン(設計パターン)と呼びます。
デザインパターンは、ソフトウェア開発の実践的な経験やベストプラクティスに基づいて抽象化され、カタログ化されています。
デザインパターン例
本著で紹介されていたデザインパターンの表になります。
完全コンストラクタと値オブジェクトについて本記事で解説します。
設計パターン | 簡単な解説 |
---|---|
完全コンストラクタ | オブジェクトを生成する際に、すべての必要なパラメータを指定するなどして不正状態から防護する |
値オブジェクト | 業務で使用される特定の単位や値のルールを高凝集化し、一貫性と信頼性を確保する |
ストラテジ | 条件分岐を削減し、ロジックを単純化する |
ポリシー | ドメインや機能に関するルールや条件の振る舞いを単純化したりカスタマイズをできるようにする |
ファーストクラスコレクション | コレクションを表すクラスで、コレクションに対する操作をカプセル化する |
スブラウトクラス | スーパークラスから派生したクラスなどで既存のロジックを変更せずに安全に新機能を追加する |
完全コンストラクタ
オブジェクトを生成する際に不正状態から防護するための設計パターンです。
どんな問題解決を解消するか
通常、オブジェクトを生成する際にはデフォルトコンストラクタを使用し、後からインスタンス変数に値を設定する方法が一般的です。
ただ、この方法では未初期化の状態でオブジェクトが生成されて不正な操作やエラーが発生する場合があります。
これを生焼けオブジェクトと呼びます。
生焼けの解決方法
生焼けオブジェクトを防止するにはインスタンス変数をすべて初期化できるだけの引数を持ったコンストラクタを用意します。
コンストラクタ内では、ガード節で不正値を弾きます。
このように設計することで、生成された段階で正常値だけを持つ完全なインスタンスが生成されます。
昨日の記事で紹介したクラスの設計方法がまさに完全コンストラクタを用いたものです。
値オブジェクト
値オブジェクト(Value Object)は、アプリケーション開発において金額、日付、注文数、電話番号など、さまざまな値を扱う際に利用される設計パターンです。
上記のような値を単なる基本データ型ではなく、クラスとして表現することで、各値に関連するロジックを高い結合度でまとめることができます。
どんな問題解決を解消するか
例えば、金額をint型のローカル変数や引数で制御していると、金額計算のロジックが散在してしまい、保守性が低下します。
また、同じint型の「注文数」や「割引ポイント」が、金額用のint型変数に誤って代入される可能性もあります。
解決方法
例えば、金額を表す値オブジェクトでは、加算や減算などの操作をメソッドとして提供します。これにより、金額に関する操作が一つのクラスにまとまり、ロジックの再利用性や保守性が向上します。
例
値オブジェクトはイミュータブル(変更不可)な性質を持つため内部状態が変更されることはありません。
また、等価性(値が同じかどうか)は値そのものに基づいて判定されます。
下記のコードは、金額を表す値オブジェクトである Money
クラスの例です。金額と通貨を保持し、加算・減算・乗算・除算などの操作をカプセル化しています。
class Money attr_reader :amount, :currency def initialize(amount, currency) # 金額が0未満の場合は不正な値として例外を発生させる raise ArgumentError, "金額が無効です" if amount < 0 # 通貨が有効な値かどうかをチェックし、無効な場合は例外を発生させる raise ArgumentError, "無効な通貨です" unless valid_currency?(currency) @amount = amount @currency = currency end def add(money) # 加算対象の通貨が異なる場合は例外を発生させる raise ArgumentError, "通貨が一致しません" unless same_currency?(money) # 金額を加算して新しい Money オブジェクトを生成して返す Money.new(amount + money.amount, currency) end def subtract(money) # 減算対象の通貨が異なる場合は例外を発生させる raise ArgumentError, "通貨が一致しません" unless same_currency?(money) # 所持金が減算対象よりも少ない場合は例外を発生させる raise ArgumentError, "残高が不足しています" if amount < money.amount # 金額を減算して新しい Money オブジェクトを生成して返す Money.new(amount - money.amount, currency) end def multiply(factor) # 金額を指定した倍率で乗算して新しい Money オブジェクトを生成して返す Money.new(amount * factor, currency) end def divide(divisor) # 除算する値が0の場合は例外を発生させる raise ArgumentError, "無効な除数です" if divisor.zero? # 金額を指定した除数で除算して新しい Money オブジェクトを生成して返す Money.new(amount / divisor, currency) end def equal?(money) # 金額と通貨が一致しているかをチェックする amount == money.amount && currency == money.currency end private def valid_currency?(currency) # 有効な通貨かどうかをチェックするロジック # ここでは省略していますが、通貨コードのバリデーションなどが行われる想定です true end def same_currency?(money) # 対象の通貨と現在の通貨が一致しているかをチェックする currency == money.currency end end
コンストラクタで引数のバリデーションを行い、不正な値を防止します。
また、同じ通貨でない場合や不足している場合には例外を発生させるなど、操作に制約を設けることで不正な状態を防止します。
このように値オブジェクトを利用することで、金額に関するロジックを値オブジェクト内に閉じ込めることができます。
値オブジェクトと完全コンストラクタは得たい効果が近いため、ほぼセットで用いられます。
「値オブジェクト+完全コンストラクタ」は、オブジェクト指向設計の最も基本形を体現している構造のひとつといっても過言ではないと本著では記されていました。
参考
続く…
コメント
本記事の内容は以上になります!
書籍の続きのアウトプットも随時更新したいと思います。
プログラミングスクールのご紹介 (卒業生より)
お世話になったプログラミングスクールであるRUNTEQです♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。