Edited at

オブジェクト指向設計実践ガイド メルマガ実例

More than 1 year has passed since last update.

オブジェクト指向設計実践ガイドの内容をふまえ、

今まで手続き的に書かれていたメルマガ送信処理をリファクタしたことで修正や拡張がとても簡単になったので、個人的な振り返りのために簡単にまとめてみました。


要件

・メルマガは新着アイテム ランキングアイテムなどいくつかの区切り(unit)を持つ

・新しいunitは随時追加できるようにしたい。

・メルマガ内のアイテムは重複しないようにしたい。


登場人物

正式なデザインパターンとは少し違うかもしれませんが、FactoryパターンBuilderパターンのような役割という意味で、クラス名を命名しています。

MailMagazine・・・メルマガクラス。メルマガには新着 ランキングといった枠組みがあり、それを unitと命名している。

xxUnitFactory・・・各ユニットを作るクラス(Factoryの役割)。

MailMagazineBuilder・・・メルマガを組み立てるクラス(Builderの役割)


MailMagazineクラス

概要で説明した通り、メルマガは複数のunit(新着 ランキング等)を持ちます。

メルマガ内の各unitOpenStruct(title, itemsを持つ)です。

OpenStructを利用したのは、各unitはタイトルやアイテム以外にも追加情報が必要になることが想定されるため、構造化データを使うことでデータ追加などを柔軟にできるようにしておきたかったからです。

class MailMagazine

# units = {key: OpenStruct, key: OpenStruct, ...}
attr_accessor :units

def initialize
@units = {}
end

def add_unit(key:, unit:)
units[key] = unit
end

# メルマガに入っている全アイテムを取り出す。重複判定に必要なため。
def all_items
units.values.inject([]) {|all, unit| all.concat(unit.items)}
end

# メール送信処理は別クラスに用意すべきかもしれない
def send_mail(user)
# 仮でunitsの中を表示する
p "#{user.name}におすすめのアイテム"
units.each do |key, unit|
p unit[:title]
unit.items.each { |item| p item }
end
end
end


UnitFactoryクラス

UnitFactoryクラスでは共通の振る舞いとしてUnitFactoryFunctionableをincludeします。

UnitFactoryFunctionableにはユニットを作るという抽象コードを定義し、具体的な内容(title items)はinclude元で再定義します。

module UnitFactoryFunctionable

attr_reader :mail_magazine, :exclude_items

#
# 重複しないように除外アイテム(exclude_items)を指示する(引数で渡す)のではなく、
# 相手を信頼し自分自身(mail_magazine)を渡すと勝手に処理してくれるのが理想的
#
def initialize(mail_magazine:, post_args:)
@mail_magazine = mail_magazine
@exclude_items = mail_magazine.all_items
post_initialize(post_args) # フックメソッド
end

#
# prepareは抽象コードであり、
# テンプレートメソッド(title, items)を使ってinclude元で具体化する
#
def prepare
OpenStruct.new({title: title, items: items})
end

def key
self.class.name.to_sym
end

private

#
# フックメソッド
#
def post_initialize(args)
nil
end

#
# テンプレートメソッド
#
def title
''
end

def items
[]
end
end

ユニット作成クラスでは、テンプレートメソッドtitle itemsを用意するだけです。

class NewArrivalUnitFactory

include UnitFactoryFunctionable

attr_reader :something

#
# 必要に応じてフックメソッドをoverrideする
#
def post_initialize(args)
@something = args[:something]
end

#
# テンプレートメソッド実装
#
def title
'新着アイテム'
end

def items
# 本当はここでexclude_itemsを使って既にメルマガに入っているアイテムは除くようにする
["item_A [#{something}]", "item_B [#{something}]"]
end

end


MailMagazineBuilderクラス

最後に「メルマガを組み立てる」Builderです。

メルマガには複数のテンプレートがあることを想定して委譲を使っています。

class MailMagazineBuilder

attr_reader :unit_factories, :mail_magazine

def initialize(unit_factories:)
@unit_factories = unit_factories
@mail_magazine = MailMagazine.new
end

def add_units_to_mail_magazine
unit_factories.each do |hash|
unit_factory_class = hash[:unit_factory_class]
post_args = hash[:post_args]
unit_factory = unit_factory_class.new(mail_magazine: mail_magazine, post_args: post_args)
mail_magazine.add_unit(key: unit_factory.key, unit: unit_factory.prepare)
end
end
end

各Builderは、必要なunit群を定義するだけで、メルマガ組み立て処理はMailMagazineBuilder に委譲しています。

class PatternAMailMagazineBuilder

extend Forwardable

def_delegators :@mail_magazine_builder, :add_units_to_mail_magazine, :mail_magazine

# [railsの場合]
# attr_reader :mail_magazine_builder
# delegate :add_units_to_mail_magazine, :mail_magazine, to:mail_magazine_builder

def initialize
@mail_magazine_builder = MailMagazineBuilder.new(unit_factories: unit_factories)
end

# 必要なunitを定義
def unit_factories
[
{ unit_factory_class: NewArrivalUnitFactory, post_args: { something: '新着!'} }
]
end
end


実行

実行の手順としては、

Builderの役割であるPatternAMailMagazineBuilderを通してメルマガを構築しメールを送る、という流れです。

mail_magazine_builder = PatternAMailMagazineBuilder.new

mail_magazine_builder.add_units_to_mail_magazine # メルマガ構築手続き
mail_magazine = mail_magazine_builder.mail_magazine # メルマガ完成

user = OpenStruct.new({name: 'name'}) # 本当はUserクラス
mail_magazine.send_mail(user)

最後に、

上記のコードではオブジェクト指向設計実践ガイドに書かれている以下を意識して使いました。


  • 抽象コードによるインタフェース定義とテンプレートメソッドによる具体化

  • フックメソッド(テンプレートメソッド)

  • 委譲

  • ダックタイプ

  • 構造化データ

コードは以下に置いてあります。

https://github.com/shshimamo/mail_magazine

別の書籍も読んでいる途中なので比較などしていければと思います。