条件による分岐をハードコーディングせずに実現する方法について考えてみました。
ただし、ここで試してみた方法はライブラリやフレームワークとして再利用するというよりも、再実装せずにふるまいを変えることができる電子回路のジャンパピンのような仕組みのイメージです。
最初に分岐をハードコーディング
まずはハードコーディングして実現したいことを整理します。
例は入力で与えられる Map の中の path
の値を元に呼び出す関数を切り替える簡単な実装です。
本体。
params.path
の値によって Foo
, Bar
, Baz
を呼び分けます。
該当しない場合は Error
の関数を呼び出します。
defmodule MyApp do def create(params) do case params.path do "/foo/" <> _ -> MyApp.Foo.create(params) "/bar/" <> _ -> MyApp.Bar.create(params) "/baz/" <> _ -> MyApp.Baz.create(params) _ -> MyApp.Error.create(params) end end def update(params) do case params.path do "/foo/" <> _ -> MyApp.Foo.update(params) "/bar/" <> _ -> MyApp.Bar.update(params) "/baz/" <> _ -> MyApp.Baz.update(params) _ -> MyApp.Error.update(params) end end end
分岐後に呼び出される関数を定義した MyApp.Foo
の実装です。
Bar
, Baz
, Error
は名前が異なるだけの同じ内容になるので省略します。
defmodule MyApp.Foo do def create(_params) do "Foo.create/1" end def update(_params) do "Foo.update/1" end end
テストコード。
defmodule MyAppTest do use ExUnit.Case doctest MyApp describe "create/" do test "call create /foo/1", do: assert MyApp.create(%{path: "/foo/1"}) == "Foo.create/1" test "call create /bar/1", do: assert MyApp.create(%{path: "/bar/1"}) == "Bar.create/1" test "call create /baz/1", do: assert MyApp.create(%{path: "/baz/1"}) == "Baz.create/1" test "call create /abc/1", do: assert MyApp.create(%{path: "/abc/1"}) == "Error.create/1" end describe "update/1" do test "call update /foo/1", do: assert MyApp.update(%{path: "/foo/1"}) == "Foo.update/1" test "call update /bar/1", do: assert MyApp.update(%{path: "/bar/1"}) == "Bar.update/1" test "call update /baz/1", do: assert MyApp.update(%{path: "/baz/1"}) == "Baz.update/1" test "call update /abc/1", do: assert MyApp.update(%{path: "/abc/1"}) == "Error.update/1" end end
ここから分岐をはがしてゆきます。
呼び出しを値に置き換える
最初に、関数の呼び出しを分岐から分離します。
分岐は利用するモジュールの取得のみにして、呼び出しには Kernel.apply/3
を利用するように変更しました。
defmodule MyApp do def create(params) do module = case params.path do "/foo/" <> _ -> MyApp.Foo "/bar/" <> _ -> MyApp.Bar "/baz/" <> _ -> MyApp.Baz _ -> MyApp.Error end apply(module, :create, [params]) end def update(params) do module = case params.path do "/foo/" <> _ -> MyApp.Foo "/bar/" <> _ -> MyApp.Bar "/baz/" <> _ -> MyApp.Baz _ -> MyApp.Error end apply(module, :update, [params]) end end
分岐を値に置き換える(1)
次に、分岐を表の検索に置き換えます。
条件と利用するモジュールの対応を表として分離し、条件を元にモジュールを検索することで分岐を置き換えます。
defmodule MyApp do def create(params) do table = [ [~r"^/foo/", MyApp.Foo], [~r"^/bar/", MyApp.Bar], [~r"^/baz/", MyApp.Baz], [~r".*", MyApp.Error] ] [_, module] = Enum.find(table, fn [pattern, _] -> params.path =~ pattern end) apply(module, :create, [params]) end def update(params) do table = [ [~r"^/foo/", MyApp.Foo], [~r"^/bar/", MyApp.Bar], [~r"^/baz/", MyApp.Baz], [~r".*", MyApp.Error] ] [_, module] = Enum.find(table, fn [pattern, _] -> params.path =~ pattern end) apply(module, :update, [params]) end end
分岐を値に置き換える(2)
二つの関数でそれぞれ表を作成しましたが、これを一段入れ子を深くして一つの表にまとめてしまいます。
defmodule MyApp do @table %{ create: [ [~r"^/foo/", MyApp.Foo, :create], [~r"^/bar/", MyApp.Bar, :create], [~r"^/baz/", MyApp.Baz, :create], [~r".*", MyApp.Error, :create] ], update: [ [~r"^/foo/", MyApp.Foo, :update], [~r"^/bar/", MyApp.Bar, :update], [~r"^/baz/", MyApp.Baz, :update], [~r".*", MyApp.Error, :update] ] } def create(params) do dispatch(:create, params) end def update(params) do dispatch(:update, params) end def dispatch(action, params) do [_, module, function] = Enum.find(@table[action], fn [pattern, _, _] -> params.path =~ pattern end) apply(module, function, [params]) end end
値を設定ファイルに移動する
最後に、分離した表をモジュールの実装から移動します。
ここで、移動先は config ファイルにします。
中でも config/runtime.exs
はアプリケーションの起動時に評価されるので、再コンパイルすることなしにふるまいを変更するのに最適です。
defmodule MyApp do def create(params) do dispatch(:create, params) end def update(params) do dispatch(:update, params) end def dispatch(action, params) do table = Application.fetch_env!(:my_app, :routing_table) [_, module, function] = Enum.find(table[action], fn [pattern, _, _] -> params.path =~ pattern end) apply(module, function, [params]) end end
config/runtime.exs
の定義です。
import Config config :my_app, :routing_table, %{ create: [ [~r"^/foo/", MyApp.Foo, :create], [~r"^/bar/", MyApp.Bar, :create], [~r"^/baz/", MyApp.Baz, :create], [~r".*", MyApp.Error, :create] ], update: [ [~r"^/foo/", MyApp.Foo, :update], [~r"^/bar/", MyApp.Bar, :update], [~r"^/baz/", MyApp.Baz, :update], [~r".*", MyApp.Error, :update] ] }
抜き身の Map を使う素朴な実装で、ライブラリなどで利用する仕組みとしてはあまりよいものではありません。 それでも、冒頭の話のようにジャンパピンとして割り切れば悪くはなさそうです。