PofEAAのユニットオブワークを実装してみた


マーチン・ファウラー著のエンタープライズアーキテクチャーパターン(以下PofEOAA)に載っていた、ユニットオブワーク(UnitOfWork)というパターンを理解するために、Rubyで実装してみたという話です。

ユニットオブワークとは

PofEOAAでは次のように記載されています。

ビジネストランザクションの影響を受けるオブジェクトのリストを保持しつつ、変更点の書き込みと並行性の問題の解決を調整する。
引用: マーチン・ファウラー. エンタープライズアプリケーションアーキテクチャパターン (Japanese Edition)

ドメインロジックで、オブジェクトに複数の変更を加えたいとき、トランザクションをどの範囲で設定したらよいか悩むことがあると思いますが、そうした問題に対処できそう、ということでこのユニットオブワークの理解を深めることにしました。

実装方法

オブジェクトの変更を、それぞれ追加、変更、削除を記録しておくコレクションに追加しておき、コミットメソッドが呼ばれたら一気に変更を反映する、というやり方をします。

ソースコード

githubのこちらのリポジトリにも置いてあります。

ユニットオブワークのソースコード
require 'singleton'

# Mapperはサンプルなので何もしません
class SomeMapper 
    def self.insert(obj)
        puts "追加処理をしました"
    end

    def self.update(obj)
        puts "更新処理をしました"
    end

    def self.getSomeDomainObject()
        return SomeDomeinObject.create
    end
end

# これがユニットオブワークの実装です
class UnitOfWork 
    include Singleton

    def initialize
        @newObjects = []
        @dirtyObjects = [] # 変更を保存しておく場所。Cleanな状態から変更されたのでDirtyです。
        @removedObjects = []
    end

    def registerNew(domainObj) 
        @newObjects << domainObj
    end

    def registerDirty(domainObj)
        @dirtyObjects << domainObj unless @dirtyObjects.include?(domainObj)
    end

    def registerRemoved(domainObj)
        return if @newObjects.delete domainObj
        @dirtyObjects.delete domainObj
        @removedObjects << domainObj unless @removedObjects.include?(domainObj)
    end

    def registerClean(domainObj)
    end

    def commit()
        insertNew()
        updateDirty()
        deleteRemoved()
    end

    def insertNew
        @newObjects.each do |o|
            SomeMapper.insert o
        end
    end

    def updateDirty
        @newObjects.each do |o|
            SomeMapper.update o
        end
    end

    def deleteRemoved
        # 略
    end
end

# ユニットオブワークに記録されるドメインオブジェクトの基底クラス
class DomainObject 
    def markNew
        UnitOfWork.instance.registerNew self
    end

    def markClean
        UnitOfWork.instance.registerClean self
    end

    def markDirty
        UnitOfWork.instance.registerDirty self
    end

    def markRemoved
        UnitOfWork.instance.registerRemoved self
    end
end

# ドメインオブジェクトの実装
class SomeDomeinObject < DomainObject
    def self.create
        obj = SomeDomeinObject.new
        obj.markNew # ここでユニットオブワークに追加を記録している
        return obj
    end

    def setSomeValue(newValue)
        @some_value = newValue
        markDirty() # ここでユニットオブワークに変更を記録している
    end
end

# ここからは実際にサンプルを実行するところ
dobj = SomeMapper.getSomeDomainObject()
dobj.setSomeValue("test")
UnitOfWork.instance.commit()

まとめ

PofEOAAに載っているサンプルは、ただ記録して、それを反映するだけのシンプルなものでした。
実際に並列性の問題を解消するためには、ロックの仕組みを入れていかなければなりませんが、これについては書かれていませんでした。

PofEOAAにはこの他に、軽ロックや重ロックなどのパターンが説明されているので、これらと組み合わせて並列性の問題を解決しましょう、ということなのかもしれません。

重ロックというとヘビーメタルみたいですね。