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がある。