Rubyでspecifiactionパターンを実装

概要

Rubyでspecifiactionパターンを実装した。内容をいかにまとめる。

ソースコード

github.com

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

結果

バナナを購入できたゴリラがいます!
------------------------
購入できたゴリラを以下に出力
ごりすけ