9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DDDをHaskellで考える 失敗を表現する

Posted at

DDD初心者が拙いHaskellを使って色々考える試みです。

はじめに

先日DDDをHaskellで考える EntityとIdentity、そしてモデル図という記事を投稿しました。
その中で「実行例外」と言うのが出てきたので、今回は失敗に付いて考えてみたいと思います。

Haskellについて

手前味噌ですが僕が今回の試みにあたり軸に考えている部分を最初の投稿に載せています。
参照透過性と副作用、業務ロジックとビジネスロジック、関数とアクションについてはその記事と同じ意味で用います。

失敗について

Haskellで失敗を表現する方法はいくつかあります。

  • Maybeを使う
  • Eitherを使う
  • 例外を使う

他、まま見られる「リターンコードが1」とか「nullを返す」とかは論外なので無視します。

これらの方法で表現される失敗の違いについて考えてみたいと思います。

業務ロジック都合で発生する失敗

システムは正常に動いて、意図するチェックに引っかかった様な場合は「業務ロジック都合で発生した」と捉えます。

  • 未成年なので契約出来ない
  • 複数項目から何らかの値を生成しようと思ったけど組み合わせが悪かった
  • 任意項目の参照を行ったら無かった

例えば上記の様な場合はMaybeEitherを用いて「失敗するかもしれない」事を型で表現します。

何かの情報に基づいて"あるかもしれない"契約出来ない理由を調べるのであれば、Maybeを用いて関数を作ります。

checkContractable :: Foo -> Bar -> Maybe InvalidReason

"成功したら"結果を、"失敗したら"理由が必要であればEitherです。

construct :: Foo -> Bar -> Either FailureReason Baz

"あるかないか"どちらもあり得る参照アクションは、やはりMaybeを使います。

findItem :: UserId -> ItemName -> IO (Maybe Item)

例外は使わない

業務ロジック都合で発生する失敗は例外を使って表現するのは不適切だと考えています。

理由はいくつかありますがひとつ挙げるなら、業務ロジックとは「場合により失敗することもある」と言う事を含めて業務ロジックなので、それを型に現れる様にするのが適切だと考えるからです。

システム都合で発生する失敗

業務ロジック上想定していない失敗についても考えてみます。

  • DBに接続出来ない
  • ユーザに対する商品が見つからない
  • ユーザは必ず商品を1つは購入しているとする場合
  • 外部Httpリクエストのタイムアウト

例えば上記の失敗は「業務ロジックとして想定しているわけではない」ため、例外で表現します。

DBに接続出来ないかも知れないことを型で現したりはしません。

findUser :: UserId -> IO (Either ConnectionException User) -- とはしない

findUser :: UserId -> IO User

DB構造的に見つかるはずのテーブル結合で状態不整合が発生することを型で呼び元に現したりはしません。

findItem :: UserId -> IO (Either InconsistentException [Item]) -- とはしない

findItem :: UserId -> IO [Item]

タイムアウトをEither Leftで返したりはしません。

callApi = do
    ...
    if ...
        then do
            ...
            return $ Right response
        else do
            ...
            return $ Left timeoutException

MaybeやEitherは使わない

意図していない失敗は言葉通り「例外」で表現するのが適切だと考えます。

プロダクトを作るには当然例外処理も設計も必要ですが、例えば業務フロー図には業務上想定したYes/Noの分岐しか現れないと思います。
それと同じで想定していない失敗をいちいち型で明記するのは不適切だと感じるし、冗長だと思っています。

他細かな気になっていること

失敗についての現状の理解は以上です。
あとは補足蛇足の様なものになります。

バリデーションはどっちか

例えばAPIを作っていて、飛んできたパラメータの桁やフォーマットが正しく無い場合、それは例外でしょうか?

曖昧な表現ですが、受け取ってチェックする以上想定しているとも思えるし、外の世界との窓口はシステマチックな話とも思えます。

フォーム部品についてもいずれ試し書きをしてみたいと思っていますが、今は以下の様にEitherを使った関数にしたいと思っています。
理由はまた改めて述べる機会を作りたいと思います。

UserIdForm :: String -> Either Error UserId

RepositoryとMapper

今回何例かあったDBアクセスを行うアクションですが、厳密には例えばSpring FrameworkではRepositoryImplMapperで実現されたりします。

例えば主キーでの参照ですが、その参照結果が「ないかも」「ないと状態不整合だ」と言うのは呼ばれた時の文脈次第で変わります。
ですがMapperはただテーブルとそのプログラミング言語上のデータ構造との変換を行うだけで、文脈を存在させるべきではありません。
参照系の場合はMapperMaybeかリストでただ命じられた参照を行い、それが「ないはずはない」とかはMapperより上の層で行うべきです。

なくてもよい/ないはずがない

それらの制御はRepositoryImplで済ませます。
理由はRepositoryの型で「あるはず」「ないかも」を表現したいからです。

例えば商品を購入したユーザに対する処理で、ユーザIDから商品を参照する場合。これは「あるはず」なので下記の様になります。

RepositoryImpl.hs
findPurchasedItem :: UserId -> IO Item
findPurchasedItem userId = do
    res <- findByUserId userId
    case res of
        (Just x) -> return x
        Nothing  -> throwIO $ userError "not found"

対してユーザの状態は気にしていない処理で、購入可能な商品が「あれば」表示する様な場合は下記の様になります。

RepositoryImpl.hs
findPurchasableItem :: UserId -> IO (Maybe Item)
findPurchasableItem userId = do
    findByUserId userId

どちらも同じMapperfindByUserIdを利用していて、それの戻りはMaybeです。

この様にMapperはアクション名も無機質であるはずないはずなんて知らない、という程度の処理であり、
それを使うRepositoryImplがあるはずないはずを適切に判断し、型に現します。
この際にRepositoryImplには業務ロジックを表現できる名前に出来ることがベストかと思います。

NullObjectパターン

Java等で見られる作りです、いきなりですが、個人的には嫌いです。

interfaceItemを用意し、ExistingItemNonexistentItemがそれを実装する様な作りです。
砕いて言えばただのストラテジパタンだと思っています。

実務で使っていましたが、nonexistentの場合の振る舞いって案外無くて大抵は「用意させられるだけ」の様な感じでした。
大抵の場合はexistingの方だけが現れるし、下手に使うとキャスト例外等を起こす可能性をはらんでいるので、個人的にはメリットを感じていません。

最近はOptionalを使っています、HaskellMaybeに相当します。

上記の通り面倒な割にメリットを感じないのも理由ですが、当然最大の理由は型からわかる情報が少ないからです。

public Item findPurchasableItem(UserId userId) {

これが「必ずひとつ返す」のか「ないかも」を表現しているのかがわからないからです。

おわりに

今回は以上です。

今回の記事はHaskellである事を活かせた感じがしています。
FormRepositoryについてもおいおい踏み込んでいきたいと思います。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?