Runner in the High

技術のことをかくこころみ

Elmにおける依存性逆転(DIP)の表現

この記事を読んでなるほどな〜と思ったので記事にしてみる。

medium.com

依存性逆転とは

雑にいうと実装ではなくインターフェイスに依存させ、モジュール間の依存関係を疎結合にする手法。英語ではDependency-Inversion Principleと呼ばれ、頭文字をとってDIPとすることが多い。

www.martinfowler.com

ElmではDIPをどう表現するか

一般的な静的型付け言語ではインターフェイス相当の言語機能が提供されている。たとえばScalaだとtraitだし、Javaだとinterfaceあたり。しかしElmにはそれらがない。

そこで、Elmでは型エイリアスを使ってインターフェイスっぽい表現をする

type alias Score
    = Int

type alias PersistScore msg
    = Score -> Cmd msg

上のPersistScoreインターフェイス相当になっている。Scoreを受け取り、なんらかの手段で永続化を行うCmd型に変換するインターフェイスである。

もう少し詳しく

ellieで用意されているTODOアプリを例にする。実際のコードのリンクは以下。
https://ellie-app.com/7Z52XPNmbfca1

IncrementDecrementに加えて、データの永続化を行うSaveというメッセージが新しく定義されている。Saveメッセージを受け取ると、何らかの方法で現在のcountの永続化を実行する。

type Msg
    = Increment
    | Decrement
    | Save


type alias Persist msg =
    Int -> Cmd msg


update : Persist msg -> Msg -> Model -> ( Model, Cmd msg )
update persist msg model =
    case msg of
        Increment ->
            ( { model | count = model.count + 1 }, Cmd.none )

        Decrement ->
            ( { model | count = model.count - 1 }, Cmd.none )

        Save ->
            ( model, persist model.count )

update関数は第一引数に永続化のためのアダプタを受け取るため、main関数での繋ぎこみの際に実装を差し込む。ここでは実装はlocalStorageへの永続化を行うlocalStoragePersisterになる。

port localStoragePersister : Int -> Cmd msg


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update localStoragePersister
        , subscriptions = \_ -> Sub.none
        }

この実装はupdate関数がPortに依存しないため、テストの際に以下のようなモックの関数を渡してupdate関数内部でのPort呼び出しの挙動などをテストできるようになる。Portに依存しなくなることでピュア度が増すし、テストもしやすくなる。

-- Portの実装をモックする関数

mockPersister : Int -> Cmd msg
mockPersister _ =
    Cmd.none

このようなPortの抽象化は、Portをたくさん使うプロダクトになればなるほど、意外に必要な場面が出てくる。

完全な抽象化は難しい

しかし、この抽象化はあまり完全なものであるとは言えない。

たとえば、実際にありえるかは分からないが「開発環境ではLocalStorageへ永続化するが、本番環境ではWebAPIへ永続化する」のような機能を抽象化するのは少し無理がある。

そもそも、PortはElmランタイムに処理を投げて終わりであるのに対して、HTTPリクエストの場合にはTaskの結果としてResult型を受け取る必要がある。 メッセージとしてリクエストの結果を受け取らないことが許されてないため、そもそもPortとTaskでモノが違うだろというハナシになってくる。 PortとのデータのやりとりがTaskで行えるようになればちょっとは可能性が出てくるが、あまり可能性はないと考えてよいだろう。