Rubyでspecifiactionパターンを実装
概要
Rubyでspecifiactionパターンを実装した。内容をいかにまとめる。
ソースコード
specificationパターンとは
specificationパターンとは、「複雑な仕様部分を外に切り出す事が出来る」ソフトウェアデザインパターン。
オブジェクトに対する「検証」や「選択」といった用途で利用される。
詳しくは、Eric Evans氏とMartin Fowler氏によるspecifiactionパターンの論文があるのでそちらを参照。
「複雑な仕様部分を外に切り出す事が出来る」ってなに?
コードが複雑な業務のルールの影響を受けてif文だらけになることがある。
「複雑な仕様部分を外に切り出す事」っていうのは、その邪魔なif文をメインのコードからどかして責務を分けること。
また、どかすときに別クラスで実装する。
オブジェクト
今回はゴリラオブジェクトを使って実装していく。 ゴリラには、名前と性別と役職がある。
module GoriraMod class Gorira attr_reader :name attr_reader :sex attr_reader :position def initialize name:, sex:, position: @name = name @sex = sex @position = position end end end
仕様クラスの実装
以下が実装した仕様クラスとなる。
require "./const" require 'date' module GoriraMod class PurchaseBananaSpecifiaction def initialize # 結果を変えてくないから固定値を入れている # 本当は、Date.todayのつもり @current_date = Date.new(2020, 2, 05) end def is_satisfied_by(gorira) # - 11月以降なら、全てのゴリラがバナナを購入可能 return true if @current_date.month >= 11 # - 性別がメスでありポジションが一般スタッフである return true if female_and_common?(gorira) # - 性別がオスでありポジションが社長である return true if male_and_president?(gorira) # 上の仕様に当てはらまなかったゴリラは購入不可 return false end private def female_and_common? gorira gorira.sex == Const::GoriraMod::Sex::FEMALE && gorira.position == Const::GoriraMod::Position::COMMON end def male_and_president? gorira gorira.sex == Const::GoriraMod::Sex::MALE && gorira.position == Const::GoriraMod::Position::PESIDENT end end end
クラス名はxxSpecification(xx仕様)という名前で実装。
判定部分のメソッド名はis_satisfied_by(~によって満たされる)という名前で実装。
is_satisfied_byの中にメインコードに出てきてほしくないif文だらけのコードを突っ込んだ。
compositeパターンを利用して、これにandメソッドやorメソッドを実装して仕様同士を組み合わせるようなやり方もある。
仕様クラスの利用
GoriraContainerというゴリラを格納するクラスにて仕様クラスを利用した。
利用されるたびにnewされるので場合によっては、クラスメソッド化してしまったほうがいいかもしれない。
require "./gorira_mod/specification/purchase_banana_specifiaction" module GoriraMod class GoriraContainer def initialize goriras @goriras = goriras end # [検証] # バナナを購入できるゴリラを保持しているかどうか def has_can_purchase_gorira? specification = PurchaseBananaSpecifiaction.new() @goriras.any?{|a| specification.is_satisfied_by(a) } end # [選択] # バナナを購入できるゴリラのみを抽出する def select_can_purchase_banana_gorira specification = PurchaseBananaSpecifiaction.new() @goriras.select{|s| specification.is_satisfied_by(s) } end end end
検証が仕様を利用してbool値を返却するメソッド、選択が何かを抽出するメソッドになる。
選択で使う仕様はSQLクエリになることもある。その場合、リポジトリ層を使うことになる。
実行する
最後にコードをつなぎ合わせて実行する。
require "./gorira_mod/object/gorira" require "./gorira_mod/object/goria_container" require "./const" gorira_1 = GoriraMod::Gorira.new(name: "ごりお", sex: Const::GoriraMod::Sex::MALE, position: Const::GoriraMod::Position::COMMON) gorira_2 = GoriraMod::Gorira.new(name: "ごりみ", sex: Const::GoriraMod::Sex::FEMALE, position: Const::GoriraMod::Position::LEADER) gorira_3 = GoriraMod::Gorira.new(name: "ごりすけ", sex: Const::GoriraMod::Sex::MALE, position: Const::GoriraMod::Position::PESIDENT) goriras = [gorira_1, gorira_2, gorira_3] container = GoriraMod::GoriraContainer.new(goriras) if container.has_can_purchase_gorira? puts "バナナを購入できたゴリラがいます!" end puts "------------------------" puts "購入できたゴリラを以下に出力" container.select_can_purchase_banana_gorira().each do |e| puts e.name end
結果
バナナを購入できたゴリラがいます! ------------------------ 購入できたゴリラを以下に出力 ごりすけ