読者です 読者をやめる 読者になる 読者になる

kotas.tech

こたすの技術的なチラ裏 ( Twitter: @ksaito )

Ruby + Padrino で DDD (1)

長年、仕事では PHPer + JSer + α だったのですが、今年に入って Rubyist に入門しています。

Web アプリを1から作るにあたって、自分の中では DDD が熱かったので、ActiveModel を回避するために Rails 以外の選択肢を模索して、Padrino に行き着きました。

Ruby + Padrino で DDD をやるにあたって、どうやっていったか記録しておこうと思います。いくつかに分けて書く予定。

Padrino のモデル層について

Padrino は、SinatraRails ライクな Helper や Mailer やリロード機構やら、色々便利な皮をかぶせたもので、モデル層は ORM 使ってもいいし、使わなくてもいいよ、みたいなスタンスのフレームワークです。

今回は DDD やりたかったので ORM は使わない事にしました。(※かなり茨の道なので、あまりオススメできません)

ValueObject と Entity

DDD における ValueObject といえば、何かの値を表現するために用いられ、値のみで識別されるオブジェクト。Entity といえば、ライフサイクルを通して一貫した ID を持ち、ID で識別されるオブジェクト。

識別については、素直に ==hash をオーバーライドして、それぞれ attribute を使ったり、id を使ったりしました。

VO の識別に使われる attribute については、attr_reader をオーバーライドして定義された getter を記録するようにしました。

実際に使われてるコードはもっと手を加えてますが、イメージとしてはこんな感じです。

module DDD
  class Object
    class << self
      def attr_reader(*names)
        attributes.concat(names)
        super(*names)
      end

      def attributes
        @attributes ||= []
      end
    end
  end

  class ValueObject < Object
    def ==(other)
      self.class.attributes.all? do |attr|
        send(attr) == other.send(attr)
      end
    end
    alias_method :eql?, :==

    def hash
      self.class.attributes.map { |attr| send(attr) }.hash
    end
  end
end

class Color < DDD::ValueObject
  attr_reader :r, :g, :b

  def initialize(r, g, b)
    @r, @g, @b = r, g, b
  end
end

red1 = Color.new(255, 0, 0)
red2 = Color.new(255, 0, 0)
blue = Color.new(0, 0, 255)

p red1 == red2  # true
p red1 == blue  # false

Entity の ID

ID の一意性をどう担保するか、という問題と、リポジトリ実装の都合上 DB の AUTO_INCREMENT に頼るのは避けたかったので、ID 生成は自前で実装する事にしました。

UUID を使っても良かったんですが、データサイズ的にやはりつらさがあるので、unsigned 64bit int で TwitterSnowflake を参考に自作してみました。

64bit を分割して

  • タイムスタンプ (41ビット): 2014-01-01 00:00:00 +00:00 からの経過ミリ秒数
  • シャード番号 (12ビット): サーバープロセスごとに変更する
  • シーケンス番号 (11ビット): ID生成の度インクリメントされ、毎ミリ秒ごとリセットされる

という構造の ID を作っています。

外部に ID 生成用サーバーを持たなくてもローカルで何とか出来るため安上がりで済み、毎ミリ秒 2048 個の ID を作成できて、しかもプロセスごとに重複しないため、ユニーク性をそれなりに保証できるなど便利です。

タイムスタンプがミリ秒で 41 ビットと聞くと、一見心許なく感じますが、約70年間は枯渇しません。

((1 << 41) - 1) / 1000 / 60 / 60 / 24 / 365 = 69

Immutability

Ruby で immutable というのは、あんまり見かけないんですが setter 定義を縛る事で一応可能です。

immutable にするメリットは色々あると思いますが、やはり大きいのは気軽に引数で引き渡し回せるようになる事と、テストのしやすさだと思いました。

Entity を immutable にするかは迷ったんですが、ものは試しでやってみる事にしました。

状態を変えたい時のために、以下のようなメソッドを基底クラスに定義していて

def transform(&block)
  cloned = dup
  cloned.instance_eval(&block)
  cloned
end

状態の変更は以下のように行えるようにしました。

def pay
  transform do
    @paid = true
    @paid_at = Time.now
  end
end

以下のように更新後のインスタンスが返ってきます。

ticket.paid # false
paid_ticket = ticket.pay
paid_ticket.paid  # true
ticket.paid # false

dup するのでややコストがありますが、やはり状態が変わると別インスタンスになるというのは、状態毎に適切な名前を付けられますし、一度作ったインスタンスの状態変化を気にしなくて済むのでありがたいですね。

つづく

(あれ、ここまで Padrino ほとんど関係ないな…)