[Ruby]責任と依存関係
オブジェクト指向の概念や各キーワードはなんとなく勉強していたのですが理解がぼんやりしており、今一度体系的に学んでみようと「オブジェクト指向設計実践ガイド」を読みました。自分が設計に迷ったときに立ち返るものを残せればと思い書いてみました。
疎結合高凝集
継続的に変更が容易なアプリケーションが作るためには、疎結合で高凝集なプログラムを書かなければならないとよく聞きます。プログラムにおける結合と凝集とは何か、順番に見ていきます。
高凝集と責任
オブジェクト指向設計実践ガイドに以下の記載があります。
クラス内のすべてがそのクラスの中心的な目的に関連していれば、そのクラスは凝集度が高い、
高凝集とはクラス内のプログラムの目的が統一されている状態といえそうです。
単一責任の原則
続いて責任という言葉について見ていきます。オブジェクト指向設計実践ガイドではSOLIDについても紹介されており、単一責任の原則に関して以下の記載がありました。
クラスがすることはすべて、そのクラスの目的に関連することを求めるのです
上記から、 高凝集なクラス == 単一責任のクラス
ということが分かりました。
それでは単一責任なクラスとそうでないクラスの違いについて考えていきます。
クラスが単一責任であるかの見極め方について、こちらもオブジェクト指向設計実践ガイドから抜粋します。後者が自分としては分かりやすくしっくりきたので、本記事ではそちらを判定基準に採用します。
あたかもそれに知覚があるかのように仮定して問いただすことです。
1文でクラスを説明してみることです。
ActiveRecordを使用したシンプルなクラス設計の例で考えていきます。
単一責任の話をしているのに、クラスの主語がでかすぎるなとも思いますが、一旦そこは考えずで。
「商品プランのマスタ情報を扱うクラス」として、Planクラスを作成しました。
商品タイプを扱う type_id
と 日数を扱う days
の2つのカラムを保持しています。
また、Planクラスは type_id と days の値をもとに文字列を返す、name
メソッドを持っています。
現時点で「商品プランのマスタ情報を扱うクラス」から逸脱していないため、 単一責任のクラスである
と言えそうです。
class Plan < ApplicationRecord
validates :days, inclusion: { in: [7, 30] }
enum type_id: { premium: 0, standard: 1 }
def name
"#{type_id.capitalize} #{days}" # Ex. 「Premium 7」
end
end
ここで、注文情報を取り扱うOrderクラスを追加してみます。
Orderクラスは、状態を取り扱う status
カラムを持ち、注文ごとにプランを選択できるようPlanクラスに紐付けられています。
Planクラスに、キャンセルされていない注文を取得する not_canceled_orders
を追加しました。
PlanクラスにOrderクラスの責任範囲が入り込んでおり、「商品プランのマスタ情報を扱うクラス」から逸脱してしまいました。
つまりこの状態は 単一責任のクラスではない
と言えそうです。
class Plan < ApplicationRecord
has_many :orders, dependent: :destroy
validates :days, inclusion: { in: [7, 30] }
enum type_id: { premium: 0, standard: 1 }
def name
"#{type_id.capitalize} #{days}" # Ex. 「Premium 7」
end
def not_canceled_orders
orders.where(status: [:in_process, :fixed])
end
end
class Order < ApplicationRecord
belongs_to :order
enum status: { in_process: 0, fixed: 1, canceled: 99 }
end
例えばOrderクラスにもう1つステータスが追加された場合に、 Plan#not_canceled_orders
にも変更を加える必要が出てきます。(where.not
を使うこともできますが、あくまでサンプルなので一旦そこまでは考えないでおきます。)
1つの変更要件に対して、余計な変更点が増えてしまうため、変更が容易なプログラムとは言えません。
普通に書くとこうはならんだろと言いたくなる、かなり拙いサンプルになってしまいましたが、実例を通して原則を概念として理解できました。
疎結合と依存
疎結合についても、オブジェクト指向設計実践ガイドに以下の記載がありました。
依存を減らすための具体的なコーディング技法をそれぞれ説明します。これらの技法は、結合を切り離すことにより依存を減らしていきます。
疎結合とはクラスの依存先が減らされた状態のことを指していそうです。
依存先を適切に管理することで疎結合なプログラムを書くことができることが分かりました。依存先の管理方法について考えてみます。
こちらもActiveRecordを使用したクラス設計を例に考えていきます。
企業とそれに所属するユーザを表す、 CompanyクラスとUserクラスを作成しました。
Companyクラスは企業名を表す、 name
カラムを保持しています。
Userクラスは名前を表すため、 姓(last_name
)と名(first_name
)をそれぞれ保持しており、role_type
でユーザのロールが定義されています。
class Company < ApplicationRecord
has_many :users, dependent: :destroy
validates :name, presence: true
end
class User < ApplicationRecord
belongs_to :company
validates :last_name, presence: true
validates :first_name, presence: true
enum role_type: { member: 0, owner: 1 }
end
依存関係を整理する
企業がオーナーの名前を取得するために、owner_name
メソッドをCompanyクラスに追加しました。また、ユーザが所属している企業の企業名を取得するために、company_name
メソッドをUserクラスに追加しました。
class Company < ApplicationRecord
has_many :users, dependent: :destroy
validates :name, presence: true
def owner_name
owner = users.owners.find_by(role_type: :owner)
"#{owner.last_name} #{owner.first_name}"
end
end
class User < ApplicationRecord
belongs_to :company
enum role_type: { member: 0, owner: 1 }
def company_name
company.name
end
end
まずは依存の方向について考えます。UserはCompanyクラスのインスタンスに依存しています。ユーザが所属企業に依存することは正しい依存方向と言えます。
一方、CompanyクラスもUserクラスに依存しています。Userがlast_name
とfirst_name
を持つことをCompanyクラスが知ってしまっている状態です。この依存方向は正しいとは言えなそうです。
依存方向を見極める観点として、 ActiveRecordの場合はbelongs_to xxx == 親モデルに依存する
と考えているのですが、明確に言語化ができていません。もし、抽象化した定義を言語化できる方がいたら教えていただけますと幸いです。
Company#owner_name
の部分をリファクタリングして、User#last_name
とUser#first_name
への依存を解消します。
owner_name
ではなく、owner
に変更し、User#full_name
を追加しました。
class Company < ApplicationRecord
has_many :users, dependent: :destroy
validates :name, presence: true
def owner
owner = users.owners.find_by(role_type: :owner)
end
end
class User < ApplicationRecord
belongs_to :company
enum role_type: { member: 0, owner: 1 }
def company_name
company.name
end
def full_name
"#{last_name} #{first_name}"
end
end
この修正によりUserクラスのnameに関する変更に対して、Companyクラスが影響を受けなくなりました。
現状のサンプルコードだと、企業に対してオーナーが1名であることはどこで制限するのかという疑問も出てきますが、今回の記事では触れません。
ちなみに、 Company.owner
の部分は以下のようにも書き換えられると思います。(動作確認はしてません)
has_one :owner, -> { find_by(role_type: :owner) }
実際のドメインでここまでシンプルに解決できる問題はなかなかないかもしれませんが、実例を通して依存関係の概念に付いても掴むことができました。
記事の内容に関して間違えている部分や、これも覚えておくといい!などありましたら教えていただけると嬉しいです。
Discussion