✉️

メールのドメインモデリング

2021/06/03に公開

概要

システムを作っているとメールを送信するようなことは多いと思います。
以前DDD的にモデリングしようと思い、メールをドメインモデル、メールの送信機能をドメインサービスとして定義・実装をしました。
しかし、メールやメールを送信するというのが本当にドメイン知識なのか?と疑問を持ったので、どうモデリングする(しない)のが良いか改めて考えたので、個人的な見解をまとめてみたいと思います。

ドメインに依る

身も蓋もないですが、どうモデリングすべきかはドメインによります。
例えばメーラーやメール配信サービスを開発する場合はまさにメールがドメインモデルでしょう。これは疑いようが無いように思えます。

今回は、何かしらの通知手段としてメールを使うようなシステムの場合にどうモデリングすればよいのかに焦点を当ててみます。

メールをドメインモデルとした場合の問題点

概要で記載したモデリングを図にすると以下のような感じです。

package ドメイン <<Rectangle>> {
    class メール
    interface メールサービス {
        void メール送信(メール)
    }
}

package アプリケーション <<Rectangle>> {
    class なにかのメール送信ユースケース
}

なにかのメール送信ユースケース -> メール: 作成
なにかのメール送信ユースケース -> メールサービス: 送信

このモデリングでは、このドメインにおいてメールという概念があるのはわかりますが、どんな内容を通知するメールなのかはユースケースのコードを見ないとわからないという問題があります。

ではメールのタイプ毎に1つずつモデルとして定義するとどうでしょうか?

package ドメイン <<Rectangle>> {
    class メール
    class 残業申請通知メール
    class 残業申請承認通知メール
    interface メールサービス {
        void メール送信(メール)
    }
}

package アプリケーション <<Rectangle>> {
    class 残業申請通知メール送信ユースケース
    class 残業申請承認通知メール送信ユースケース
}

メール <|-- 残業申請通知メール
メール <|-- 残業申請承認通知メール
残業申請通知メール送信ユースケース -> 残業申請通知メール: 作成
残業申請通知メール送信ユースケース -> メールサービス: 送信
残業申請承認通知メール送信ユースケース -> 残業申請承認通知メール: 作成
残業申請承認通知メール送信ユースケース -> メールサービス: 送信

このドメインにおいてどういったメールがあるのかはわかるようになりました。
しかし、このモデリングをしたとき、おそらく各種メールクラスにはメールのタイトルや、メールの本文を持つことになるのではないでしょうか?

各種メールクラスのコンストラクタに必要な情報を引き渡し、クラス内部でタイトルや本文を生成するようなモデリングにした場合、ドメインモデルがどのようにメールを表示すべきかという表示に関する責務(メールの文章の構築など)を負うことになってしまいます。これはドメインモデルとしての関心事から外れますし、多言語対応やHTMLメールにしたいとなった場合は更にドメインモデル内の処理が表示のためのロジックで埋まってしまいます。

ドメインモデルに本文などの構築を辞めさせ、ユースケースクラスが構築した文章をメールオブジェクトに設定するようにするとどうでしょうか?
今度は「残業申請通知メール」という概念があるんだなと言うのはわかりますが、メールで何の情報が渡されるのかはやはりわからなくなってしまいます。

ドメインモデルとは、ドメインに関する知識を語る存在であるべきです。
「なんだか得たいの知れないもの」になってしまっては、ドメインモデルとしては不適切だと言えます。

メール送信は「通知する」というユースケース

こうしてみると、メールとは本当にドメインモデルなのでしょうか?

大抵の場合、メールを送るシステムにおいては、メールというのはドメインモデル(ドメイン知識)そのものではなく、通知を行うための手段、すなわちアプリケーションのユースケースに付随する概念ではないでしょうか?

つまり、ドメイン知識として存在するのは通知する内容を保持する別の概念であり、そのモデルがもつ情報を届けるユースケースがメールで送信するなのではないか?と考えました。

例えば、上記の例で出した残業申請であれば、残業申請ドメインに存在するのは残業申請残業申請承認というモデルであり、それらをメールで通知するというユースケースがアプリケーションにあると言ったところではないでしょうか。

package ドメイン <<Rectangle>> {
    class 残業申請
    interface 残業申請リポジトリ
}

package アプリケーション <<Rectangle>> {
    class 残業申請通知メール {
        + 申請内容
        + 送信先
        + 送信元
        + ・・・
    }
    class 残業申請通知メール送信ユースケース
    interface メールサービス
}

残業申請通知メール送信ユースケース -> 残業申請リポジトリ: 残業申請取得
残業申請通知メール送信ユースケース -> 残業申請通知メール: 作成
残業申請通知メール送信ユースケース -> メールサービス: 送信

アプリケーション層(ユースケース)には、通知の主な内容となるドメインモデルの取得、メール送信を行うために必要な情報の取得(通知先のメールアドレスなど)、それらを加工してメールオブジェクトの構築を行う責務を負わせます。

こうすることでドメインには残業申請という業務に関する知識だけが残り、アプリケーション層で、これらに関する通知をこのアプリケーションではメールで行うというユースケースを表現出来ます。

メールで送信するというのは実はアプリケーション毎の要件であり、今回はたまたまメールだっただけで、残業申請というドメイン自体にメールの概念があるわけではないのではないでしょうか。(もちろんドメインによってその見方は変わるでしょう)

このモデリングであれば、残業申請の通知をSMSで送りたい、Push通知で送りたいという要件が増えても、残業申請のモデルを使って各種通知を作成するユースケースを増やすだけで、ドメインモデルに手を加える必要はありません。
どんな手段で通知するかは、残業申請の業務内容、すなわちドメインそのものとは関係がないはずです。

通知には「表示」が伴う

上記のモデリングにはまだ問題があります。

メールであれ、Push通知であれ、通知というのは結局のところ情報を「表示する」ためのものです。
アプリケーション層(ユースケースクラス内)でメールの表示内容までを構築し、送信するというのは、アプリケーション層の責務を逸脱しています。

最初はプレーンなテキストメール送ってたけど、やっぱりHTMLメールで送信したい、文言変えたいなど、メールと言えど表示を司る部分には変更が入る可能性が高いといえます。
その度にアプリケーション層を変更していては、表示以外の処理にも影響が出かねません。
変更の入りやすい処理をより外側のレイヤーに押し出す、これがClean Architectureの狙いの一つでもありました。

ともすると、アプリケーション層で負う責務は、メール送信のための情報収集・構築までで、それを実際にどういった文章やレイアウトでメール送信するかはプレゼンテーション層に押し出すべきではないでしょうか?

package ドメイン <<Rectangle>> {
    class 残業申請
    interface 残業申請リポジトリ
}

package アプリケーション <<Rectangle>> {
    class 残業申請通知メール {
        + 申請内容
        + 送信先
        + 送信元
        + ・・・
    }
    class 残業申請通知メール構築ユースケース
    interface 残業申請通知メール構築入力ポート
    interface 残業申請通知メール構築出力ポート
}

package プレゼンテーション <<Rectangle>> {
    class 残業申請通知メール送信コントローラ
    class 残業申請通知メール送信プレゼンタ {
        void メール表示内容構築()
    }
    interface メール送信ポート
}

package インフラストラクチャ <<Rectangle>> {
    class SendGridメール送信ポート
    class Gmailメール送信ポート
}

残業申請通知メール構築入力ポート <|.. 残業申請通知メール構築ユースケース
残業申請通知メール送信プレゼンタ ..|> 残業申請通知メール構築出力ポート
残業申請通知メール送信コントローラ -> 残業申請通知メール構築入力ポート: IN 残業申請ID
残業申請通知メール構築ユースケース -> 残業申請リポジトリ: 残業申請取得
残業申請通知メール構築ユースケース -> 残業申請通知メール: 作成
残業申請通知メール構築ユースケース -> 残業申請通知メール構築出力ポート: OUT 残業申請通知メール
残業申請通知メール送信プレゼンタ -> メール送信ポート: メール送信
SendGridメール送信ポート ..|> メール送信ポート
Gmailメール送信ポート ..|> メール送信ポート

プレゼンテーション層でアプリケーション層から受け取ったデータオブジェクトを元にメールの表示内容を構築するようにすれば、同じユースケースクラスを使って、プレーンテキストで構築するプレゼンタ、HTMLメールを構築するプレゼンタなど、様々な出力方式を、アプリケーション層に手を加えることなく実装できます。

まとめ

メール送信っていかにもシステムの機能的な話であまりドメイン層に入れたく無いけど、これはこれでシステムを取り巻くドメインの知識なのだろうか?とモヤッとしていましたが、自分的には上記の考え方がスッキリするかなと思いました。
もちろんこれが正解というわけでもなく、最初に書いたようにメールそのものがドメイン知識であるパターンももちろんあると思います。

ドメインモデルの探索は、答えの無い永遠の旅だと思います。

Discussion