ドメイン駆動設計と関数プログラミングをElixirで

  • 23
    Like
  • 0
    Comment

はじめに

この記事は Elixir Advent Calendar 2016 - Qiita の 15 日目の記事です。

また、以下の記事の Elixir 版となっておりますので、こちらもご覧ください。

ドメイン駆動設計と関数プログラミングをScalaで - Qiita

(一部上の記事と内容が重複する部分もありますが、この記事のみでも伝えたいことが伝えられるようにと思い、コピペして記載しています)

この記事では、ドメイン駆動設計がどのように関数プログラミングと結びつくのか、非常にざっくりではありますが、Elixir で表現してみたいと思います (ちなみに、私の Elixir 歴は3ヶ月ほどですので、サンプルコードに間違いや改善点がありましたら、コメントいただけるとありがたいです)。

@type, @spec 等のディレクティブは省略していますが、Dialyzer を使った方がより堅牢なプログラムになると思いますので、書くようにしましょう。

環境

  • Erlang/OTP 18 [erts-7.3]
  • Elixir 1.3.1
  • Phoenix 1.2.0

ドメイン駆動設計について

Eric Evans による、2003年に出版された "Domain-Driven Design" (以下、DDD本)という書籍に基づくソフトウェア設計手法です。

詳しい解説は省きますが、主な特徴は、

  • ユビキタス言語を用いて定義されるドメインモデル
  • モデル駆動設計とモデルに一致した実装
  • ドメインを他と分離するためのレイヤー化アーキテクチャ
  • エンティティ、値オブジェクト、サービスといったデザインパターン

といったところです。

用語についてはこちらも参照してください。

ドメイン駆動設計の用語と解説(抜粋版) - Qiita

関数プログラミングについて

私は「関数プログラミング入門」と「Scala関数型デザイン&プログラミング ― Scalazコントリビューターによる関数型徹底ガイド」いう本で学んでいます (と、もちろん、FRDM本も)。

特徴として理解しているのは、

  • 副作用フリーで参照透過な関数
  • 代数的データ型
  • 不変性

あたりです。

DDD本にも、「値オブジェクトは不変である」という記述や、「副作用のない関数」という節があり、キーワードレベルでも共通部分がありますね。

Elixir について

Erlang VM (BEAM) 上で動作する、関数型プログラミング言語です。

変数には再束縛が可能ですが、値を変えることはできません。

iex(1)> n = 2
2
iex(2)> n = 3
3
iex(3)> m = %{x: 1, y: 2}
%{x: 1, y: 2}
iex(4)> m.x = 3
** (CompileError) iex:7: cannot invoke remote function m.a/0 inside match
iex(5)> m = %{m | x: 3}         
%{x: 3, y: 2}

(4) のように Map の中身を書き換えようとするとエラーになりますが、(5) のようにすると、更新できます (厳密には m のコピーが作られて m に再束縛されるので不変性は保たれています)。

また、作者の José Valim さんが Ruby on Rails のコミッタであるせいか、文法が Ruby に似ていることも特徴です。

FRDM本におけるドメイン駆動設計に沿った実装例

以下、FRDM本に載っているサンプルコード (銀行口座のアプリケーション) から抜粋して (一部改変しています)、ドメイン駆動設計と Scala によるファンクショナルな実装の例を紹介します。

サンプルアプリケーションは Phoenix で動かしています。

値オブジェクト

プリミティブなデータ型をラップした残高クラスの例です。

balance.ex
defmodule DDDExample.Balance do
  defstruct amount: 0
end

Elixir の構造体はイミュータブルなので、外部から Balance.amount を変更することはできず、イミュータブルな値オブジェクトになります。

値オブジェクトに関しては、Elixir で自然に実装できます。

エンティティ

インスタンスが識別可能なクラス。入金と出金の責務を持つ口座クラスが例示されています。

account.ex
defmodule DDDExample.Account do

  alias DDDExample.Balance, as: Balance

  defstruct no: "", name: "", dateOfOpening: nil, balance: %Balance{}

  def debit(account, amount) do
    if (account.balance.amount < amount) do
      {:error, "Insufficient balance in account"}
    else
      {:ok, %{account | balance: %{account.balance | amount: account.balance.amount - amount}}}
    end
  end

  def credit(account, amount) do
    {:ok, %{account | balance: %{account.balance | amount: account.balance.amount + amount}}}
  end
end

オブジェクト指向プログラミングでは、口座に対する入金処理や出金処理の場合、口座オブジェクトの残高プロパティを変更するのが一般的かと思いますが、関数プログラミングでは、上の例でも分かるように、debit メソッドにしても credit メソッドにしても、Account オブジェクトをコピーしています。また、{:ok, ...} {:error, ...} のタプルを使ってエラーハンドリングしているところは Elixir の特徴です (:ok, :error は慣習的な命名みたいなので、合わせた方がよさそうです)。

呼び出し側はこんなかんじになります (Phoenix のコントローラーから呼び出してみました)。

account_controller.ex
defmodule DDDExample.AccountController do
  use DDDExample.Web, :controller
  use Timex

  import IfOk

  alias DDDExample.Account, as: Account

  def index(conn, _params) do
    account = %Account{no: "a1", name: "john", dateOfOpening: Timex.today}
      |> Account.credit(100)
      |> if_ok(fn acc -> Account.debit(acc, 20) end)

    case account do
      {:ok, account} -> render conn, "index.html", account: account
      {:error, reason} -> render conn, "error.html", reason: reason
    end
  end
end

Elixir ではパイプ演算子 (|>) によって前の関数の戻り値を次の関数の第一引数に自動的に割り当てるので、 Scala編では別のインスタンス変数が出てきていた箇所がいくぶんすっきりしています。

ただ、タプルで返ってきた戻り値から Account オブジェクトを取り出すところで if_ok1 という関数を使っていますが、アリティが 2 の関数を引数に入れるときは無名関数をいったんつくってから呼んであげないといけなかったので、若干見づらくなっていますね。

サービス

DDD本では、サービスの説明として、

サービスとは、モデルにおいて独立したインタフェースとして提供される操作で、エンティティと値オブジェクトのようには状態をカプセル化しない。

とあります。つまり、サービスは、状態を持たない操作の集合であると言えると思うので、これも Elixir でファンクショナルに実装できそうです。

AccountService.ex
defmodule DDDExample.AccountService do

  alias DDDExample.Account, as: Account
  import IfOk

  def transfer(from, to, amount) do
    Account.debit(from, amount)
      |> if_ok(fn acc -> {:ok, {acc, Account.credit(to, amount)}} end)
      |> if_ok(fn {acc, t} -> {:ok, {acc, elem(t, 1)}} end)
  end
end

transfer メソッドを呼び出してみます。

account_controller.ex
  def index(conn, _params) do

    from = %Account{no: "a1", name: "john", dateOfOpening: Timex.today, balance: %Balance{amount: 100}}
    to = %Account{no: "a2", name: "jane", dateOfOpening: Timex.today, balance: %Balance{amount: 100}}

    ret = AccountService.transfer(from, to, 30)
    case ret do
      {:ok, {from, to}} -> render conn, "index.html", account: from, account2: to
      {:error, reason} -> render conn, "error.html", reason: reason
    end
  end

ret の2番めの要素は、1番目の要素が :ok であれば {from, to} のタプルです。

状態を伴う計算について

モナドに関しては、分からないこともまだ多いんですが、関数プログラミングと題しておきながら触れないわけにはいかないと思いましたので、取り上げてみます。

State モジュールはこちらを利用しました。

monad/state.ex at develop · rmies/monad · GitHub

先に呼び出し側のコードを示します (本では、バッチで実行される処理を想定しているようですが、すぐに結果を見たかったので Phoenix のコントローラーで実行してみました)。

account_controller.ex
defmodule DDDExample.AccountController do
  use DDDExample.Web, :controller
  require Monad.State, as: State
  import State
  alias DDDExample.Balance, as: Balance
  alias DDDExample.TransactionService, as: TransactionService

  def index(conn, _params) do
    balances = %{
      "a1" => %Balance{},
      "a2" => %Balance{},
      "a3" => %Balance{},
      "a4" => %Balance{},
      "a5" => %Balance{}
    }

    transactions = [
      %Transaction{accountNo: "a1", amount: 100},
      %Transaction{accountNo: "a2", amount: 100},
      %Transaction{accountNo: "a1", amount: -500},
      %Transaction{accountNo: "a3", amount: 100},
      %Transaction{accountNo: "a2", amount: 200}
    ]

    updatedBalances = State.run(balances, TransactionService.updateBalances(transactions))

    render conn, "index.html", balances: elem(updatedBalances, 1)
  }

a1 から a5 までの口座に対して、その日に実行されたトランザクションによる入出金を適用して残高を更新する処理です。

後述しますが、State.run(balances, TransactionService.updateBalances(transactions) の戻り値は {nil, Map} のタプルなので、テンプレートに渡すときに Kernel.elem/2 で取り出しています。

サービスのコードはこちらです。

transaction_service.ex
defmodule DDDExample.TransactionService do
  require Monad.State, as: State
  import State
  alias DDDExample.Balance, as: Balance

  def updateBalances(transactions) do
    State.modify(fn(balances) ->
      List.foldl(transactions, balances, fn(tran, acc) ->
          Map.update(acc, tran.accountNo, %Balance{}, fn(b) -> %Balance{amount: b.amount + tran.amount} end)
      end)
    end)
  end
end

Scala編では Monoid オブジェクトを使っていましたが、今回の Elixir では力及ばず割愛しました。

modify メソッドは以下で定義されているように、State モナドが内包しているデータ (上記例では 口座番号と空の Balance の Map) に対して、関数を適用するだけの関数です。

monad/state.ex at develop · rmies/monad · GitHub

このようなやり方の何が嬉しいのか、という点について、FRDM本では以下のように述べています。

State helps you carry your model state, so that you don’t have to do it in your application code. It’s all about delegating the plumbing logic to the monad itself. And it does this in a completely generic manner so that you can reuse the State monad and its combinators for managing any kind of state.

(意訳)

「State を使うと、モデルの状態を管理しやすくなり、状態を更新するロジックをモナドに閉じ込めておくことができます。State モナドとコンビネータはどんな状態でも管理できるので、再利用できます。」

たしかに、上記の updateBalances には、状態を変更するロジックはありません。 と言いたいところですが、Scala 編のコードに対して、Monoid を使わずに Map.update を呼び出しているため、いまいちそれっぽくないですが、State.modify/1 を用いることで、状態の更新をモナドに包むことができます。

エンティティのところでも書きましたが、関数プログラミングでは、状態を保持する代わりに、関数を適用した別のインスタンスをつくることになりますが、それは無用のものであり、不具合の温床ともなりうるので、それを排除した書き方ができるのなら、積極的に使っていきたいです。

まとめ

まだ少し曖昧な部分もありますが、値オブジェクト、エンティティ、サービスの実装例を見てきて、ドメイン駆動設計のモデルを関数型のパラダイムで表現することは、とても自然に感じられました。

DDD本にも、

モデル駆動設計がオブジェクト指向である必要はないが、(中略) モデルの構成要素が表現豊かに実装されることにかかっているのは確かだ。

とありますが、正直、Scala に比べると Elixir はやや表現力という点では見劣りするかんじでした (マクロで拡張すればなんでもアリなのかもしれないですが、言語仕様レベルにおいては、という意味で)。

ProtocolBehaviourdefoverridable@derive 辺りを使いこなせるようになれば、また違った景色が見えてくるのかもしれません。来年も Elixir で色々書いてみようと思います。

参考

Monads in Elixir