概要
運営しているサービスにおいて、ビジネスロジックから特定のデータリソースに対するアクセス、および加工処理を切り出して共通化した話です。
コレは果たしてmodelなのか?
わからん。Entityとかドメインオブジェクトの方がいいのか?
誰か教えてください。
model化前
例えばこんなコードでした。
下記は都道府県のページを管理するクラスで、その都道府県に紐づくイベントを取得しているとします。
module Area
class Pref
def event
events = Api.call('/events', params)
events.each_with_object([]) do |event, memo|
memo << {
name: event[:name],
address: create_full_address(event)
}
end
end
def create_full_address(event)
# いくつかの情報を組み合わせて住所のフル文字列を返す
end
end
end
次に都道府県の配下の市区町村のページができました。下記ではその市区町村に関するイベントを取得しています。
module Area
class City
def event
events = Api.call('/events', params)
events.each_with_object([]) do |event, memo|
memo << {
name: event[:name],
address: create_city_address(event)
}
end
end
def create_city_address(event)
# 住所を短く表示したいので市区町村以降の住所文字列を返す
end
end
end
イベント情報を扱う処理が重複しています。これを下記のように切り出して共通化してみます。
module Area
class Common
class << self
def events(params)
Api.call('/events', params)
end
def create_full_address(event)
# いくつかの情報を組み合わせて住所のフル文字列を返す
end
def create_city_address(event)
# 住所を短く表示したいので市区町村以降の住所文字列を返す
end
end
end
end
呼び元は下記のような感じになります。
require_relative 'common'
module Area
class Pref
def event
events = Common.events(params)
events.each_with_object([]) do |event, memo|
memo << {
name: event[:name],
address: Common.create_city_address(event)
}
end
end
end
end
よしよしと思っていましたが、次に駅のページを管理するクラスが新たにできました。
このページでは住所の情報は不要で、駅に関する情報のみが必要だったとします。
前回area/common.rbのような形でarea系のページで共通化を行なってしまいました。
また、area系のページで取得している情報は不要で、少し欲しい情報も違うため、イベント情報へのアクセスを独自で実装することにしました。
module Traffic
class Station
def event(params)
events = Api.call('/events', params)
events.each_with_object([]) do |event, memo|
memo << {
name: event[:name],
stations: create_stations(event)
}
end
end
def create_stations(event)
# いくつかの駅情報を返す
end
end
end
以上がmodel化前の大体の状況です。
例が長い上にいまいちな気がしてきました。実際にはもう少し入り組んだ状況になっているという言い訳をしておきます。
model化前の問題点
問題点をまとめると、下記のような状況であったと言えます。
- データリソースに対するアクセスをページごとのビジネスロジック内で行うので処理の共通化がしづらい
- ページごとのビジネスロジックが肥大化する
- ビジネスロジックが肥大化した影響でページごとにクラス構成が異なり、どこに何を書いたらよいかがわかりにくい
- データリソースの加工処理が各所に散らばるため、データリソースに変更が行われた際の影響調査がしづらい
社内の便利APIがデータリソースである場合には、ある程度気を利かせたレスポンスが返ってくるため、かっちり切り分けずにビジネスロジック内で呼んでしまうケースはあるのでは?と思います。
最初はシンプルに実装できていても、様々な箇所で様々な利用が始まるとカオスになってゆきます。
model化後
解決策として、modelをちゃんと運用することにしました。
データリソースごとにmodelを切り、そのデータリソースに対するアクセスは必ずmodel経由で行うことにしました。
レスポンスの装飾もmodelに集約することで共通化を図り、どこに実装してよいか明確な状態をつくりました。
具体的には下記です。
1つ1つのイベントを管理するmodelを下記のように定義します。
module Model
class Event
attr_accessor :name, :address
def initialtize(event)
{
'name' => '@name',
'address' => '@address',
}.each do |key, prop|
instance_variable_set(prop, event[key])
end
end
def full_address
# いくつかの情報を組み合わせて住所のフル文字列を返す
end
def city_address
# 住所を短く表示したいので市区町村以降の住所文字列を返す
end
def stations
# いくつかの駅情報を返す
end
end
end
イベントのリストを管理するmodelは下記です。
require_relative 'event'
module Model
module Events
attr_accessor :events
def initialize(events)
@events = events.each_with_object([]) do |event, memo|
memo << Model::Event.new(event)
end
end
class << self
def get(params)
events = Api.call('/events', params)
Model::Events.new(events)
end
end
end
end
利用側はこんな感じです。
module Area
class Pref
def event
events = Model::Events.get(params)
events.each_with_object([]) do |event, memo|
memo << {
name: event.name,
address: event.full_address
}
end
end
end
end
event情報を扱うmodelとして、model/event.rbを定義しました。
こちらはevent情報の保持と装飾を担当し、event情報へのアクセスは全てこのmodelを経由します。
eventを取得するAPIのレスポンスはリスト型で返るので、model/events.rbを定義し、APIのレスポンスはこちらで管理します。
このとき、個々のイベント情報はevent.rbに渡して装飾し、events.rbではその結果をリストとして保持します。
この結果、利用側は特に意識することなく、装飾された項目にアクセスできるようになります。
FATを回避する
以上のようにmodel化することで、データリソースへのアクセスや装飾を集約することができました。
一方でこの設計だとmodelが膨らんでいく懸念もあると思います。
FATなmodelを避けるための取り組みとして、下記のような対策を行っています。
- 装飾メソッドを適切に分離する
- delegateを利用して移譲する
delegateですが、例えばeventの他にfestivalの用な別のmodelがあり、それぞれで同様のメソッドを所持したい場合に有効です。
module Model
class Event
# イベント開催を判定するhold?メソッドはModel::Conditionに移譲し、そちらの実装を使う
delegate :hold?, to: Model::Condition
end
end
下記のように呼べる
module Area
class Pref
def hold_event?
Model::Events.hold?
end
end
end
SpecialThanks
原案提供してくださった @nazomikan さん、推進してくださったHK、SSさん、および部署のメンバーのみなさまありがとうございました!
PR
不動産売却の時は LIFULL HOME'S へ!