SharePie 素振り

DDDのしなやかな設計の章の最後に出てきた Share Pie(おすすめ8章にも出てきた)を Rubyで素振り。

Value Object, Side Effect Free Function, Assertions, Closure Of Operations などの組み合わせになっている。詳しくは本文を。

# -*- coding: utf-8 -*-

class Share
  attr_accessor :owner, :amount

  def initialize(owner, amount)
    @owner, @amount = owner, amount
  end
end

class SharePie
  attr_accessor :shares
  def initialize(&block)
    @shares = {}
    block.call(self) if block_given?
  end

  def amount
    @shares.values.inject(0.0) do |result, share|
      result + share.amount
    end
  end

  def amount_by owner
    @shares[owner] ? @shares[owner].amount : 0.0
  end

  def add(owner, amount)
    @shares[owner] = Share.new(owner, amount)
  end

  #  パイの結合
  def plus(othere_pie)
    owners = (self.shares.keys + othere_pie.shares.keys).uniq
    owners.inject(SharePie.new) do |result, owner|
      result.add(owner, self.amount_by(owner) + othere_pie.amount_by(owner))
      result
    end
  end

  # パイの差
  def minus(othere_pie)
    owners = (self.shares.keys + othere_pie.shares.keys).uniq
    owners.inject(SharePie.new) do |result, owner|
      result.add(owner, self.amount_by(owner) - othere_pie.amount_by(owner))
      result
    end
  end

  # 比率分配する
  def prorated(amount_to_prorate)
    basis = amount
    @shares.keys.inject(SharePie.new) do |result, owner|
      share = @shares[owner]
      prorated_share_amount = amount_to_prorate * (share.amount / basis)
      result.add(owner, prorated_share_amount)
      result
    end
  end
end

describe SharePie do
  let(:pie_a) {
    SharePie.new { |p|
      p.add "nameA", 10.0
      p.add "nameB", 20.0
      p.add "nameC", 30.0
    }
  }

  let(:pie_b) {
    SharePie.new { |p|
      p.add "nameA", 1.0
      p.add "nameB", 2.0
      p.add "nameD", 4.0
    }
  }

  context "plus" do
    let(:expecteds) {
      [["nameA", pie_a.amount_by("nameA") + pie_b.amount_by("nameA")],
       ["nameB", pie_a.amount_by("nameB") + pie_b.amount_by("nameB")],
       ["nameC", pie_a.amount_by("nameC") + 0.0],
       ["nameD", 0.0                      +  pie_b.amount_by("nameD")]]
    }

    subject{ pie_a.plus(pie_b) }

    it "plusした結果の share pie の share 一覧が期待通りであること" do
      subject.shares.keys.should == expecteds.map {|owner, amount| owner }
      expecteds.each do |owner, prorated_share_amount|
        subject.amount_by(owner).should == prorated_share_amount
      end
    end
  end

  context "minus" do
    let(:expecteds) {
      [["nameA", pie_a.amount_by("nameA") - pie_b.amount_by("nameA")],
       ["nameB", pie_a.amount_by("nameB") - pie_b.amount_by("nameB")],
       ["nameC", pie_a.amount_by("nameC") - 0.0],
       ["nameD", 0.0                      -  pie_b.amount_by("nameD")]]
    }

    subject{ pie_a.minus(pie_b) }

    it "minusした結果の share pie の share 一覧が期待通りであること" do
      subject.shares.keys.should == expecteds.map {|owner, amount| owner }
      expecteds.each do |owner, prorated_share_amount|
        subject.amount_by(owner).should == prorated_share_amount
      end
    end
  end

  context "prorated" do
    let(:pie) {
      SharePie.new { |p|
        p.add "nameA", 10.0
        p.add "nameB", 20.0
        p.add "nameC", 30.0
        p.add "nameD", 40.0
      }
    }

    let(:expecteds) {
      [["nameA", 1000 * pie.amount_by("nameA") / pie.amount],
       ["nameB", 1000 * 20.0 / (10.0 + 20.0 + 30.0 + 40.0)],
       ["nameC", 300],
       ["nameD", 400]]
    }

    subject{ pie.prorated(1000) }

    it "比率で分配できること" do
      expecteds.each do |owner, prorated_share_amount|
        subject.amount_by(owner).should == prorated_share_amount
      end
    end
  end

  context "amount" do
    subject {
      SharePie.new { |p|
        p.add "nameA", 10.0
        p.add "nameB", 20.0
      }
    }
    its(:amount){ should == 30.0 }
  end
end


10章には、もう一つ応用例が。 内容物(例:砂や化学薬品)を納めるコンテナが仕様を満たしているかのルール記述を論理演算子を使って簡潔かつ解りやすく表記する方法の例が出てくる。Specificationパターンの応用例であり、Value Object、Assertionsなど の例になっている。昔やったプロジェクトの、容器の種類の話を思い出した。

TDDの Moneyとか、アナパタの Quantity とか、Value Objectの例は、よく出てきて知っていはいるんだが、実プロジェクトで Value Object 使いこなしてない。。。 概念の輪郭をリファクタリングを通じて、つかむ練り込みが足りないのかな。

対象ドメインのふるまいを配置先は、選択として、Service, Entity, Helperの他に、Value Objectがある。