ドメイン駆動設計#1 Advent Calendar 2019 の 16 日目の記事です
この記事はふわーっと副作用・参照透過性・冪等性を理解して、モデルをそれっぽーくリファクタリングするものです
ゆるーっとご覧ください
では早速...
副作用
結果が引数以外に依存して決まる処理のこと
「グローバル変数があるので処理を読んでも結果がわからない」とか「同じ実行の仕方をしても朝と夜で結果が変わる」とか言う感じ
int function add2(int n) {
return this.x + n; // x って何よ?
}
void function init() {
this.x = 2; // これです
}
参照透過性
処理を同じ引数で実行すると同じ結果になること
int function add(int x, int y) {
return x + y;
}
朝でも夜でも mac でも windows でも商用環境でも日本でもアメリカでも、add(2, 3)
は5
- 透過的 ( transparency ) とは「存在するものを存在しないように見せる」感じらしい
- 透過の反対は仮想らしい
- 透過 → あるけど見えないガラス
- 仮想 → ないけど見えるホログラム
- つまり「参照透過性」は「参照があるけどないように見せる」ということ
- 変数の再代入やグローバル変数の参照が処理内になければ、処理内では変数の参照について心配する必要がないので、参照が透過的と言える
冪等性
処理を何回実行しても同じ結果になること
void function bill(int id) {
if billed { /* do nothing */ } else { ... }
}
現状を気にせず叩けば良いので、chef やマイクロサービス等のたくさんの状態を更新する処理と相性が良い
(一部が失敗しても全てが成功するまで叩き直せば良いから)
身近な例だと「オン | オフ」みたいなトグルボタンは冪等性がないけど「オフにする」ボタンは冪等性がある、って感じかな
あとは絶対値とかの値計算もそう
整理
副作用と参照透過性はだいたい裏返しって感じで良いかな?
冪等性はドメイン層と言うよりアプリケーション層やコンポーネント結合の設計に関係しそうな感じかな?
この記事では整理した3つの単語のうち 副作用 に目をつけて、ドメイン層をリファクタリングしてみます
副作用についてもうちょっと
特徴を再整理
- 処理のスコープの外の何かに依存しているとだいたいそう
- スコープ外変数、DB、ファイル、時間、環境変数、os コマンド、http 通信、乱数
- 戻り値がない処理もだいたいそう
- print、setter、insert、update、send
- 何も戻さないということはどこかを書き換えている
- 別に例えば他の private メソッドを呼んではいけないという意味ではない
- 自身にも private メソッドにも副作用がなければ、副作用はない
ドメイン層に副作用があるとどうなるか
例えば時間に依存しているドメイン処理があると、動作確認できる時間が限られてしまう
毎月1日しか動作確認できない請求処理とか超怖いよね
どうすれば良いか
そんなに難しくはない、テストコードを書いてみればわかるはず
テストコードを書くときに DB のセットアップが必要になったり、システム時計を改ざんしたり、ダミーのリクエスト先サーバを立てたりする必要があると、それはもうばっちり副作用がある
でもなくなりはしないよ
どうしてもどこかで副作用は扱わなきゃいけない、それはその通り
だけどその層を限定しようというのが狙いだ
以下の理由により、ドメイン層では(極力)副作用は扱わない方が良いはずなんだ
- ビジネスルールってルールなので、シンプルなただの計算のはずなんだ、引数だけで結果決まりそうだよね?
- 変更に耐えられるように、たくさんのパターンを網羅できるように、ドメイン層のテストコードはあった方が良いよね? けど 副作用があるとテストコードが{書けない|異様に書きづらい}
- ビジネスルールに DB とか os コマンドってそもそも 関係ないよね?
テストコードを書きながらリファクタリングしてみよう
メール本文を組み立てて飛ばす
こんなコードがあったとしよう、残念ながらテストはないんだけども
↓ send(...)
のテスト書けるかな?
渡した引数をごにょごにょして社内ライブラリに渡しちゃってるので、加工が成功したかも社内ライブラリの言う通り渡せたかも、 何もわからないね
戻り値もないしね ( scala's Unit means java's void. )
モックのメールサーバでも立てる?
めんどくさいね、いやだね
↓ ちょっと整理してみよう、テストできるところを緑で、できないところを橙で、副作用がある範囲を赤で色分けしてみるよ
あぁ!なんて危険がいっぱいな処理なんだ!
↓ この処理を呼び出す部分はこんな感じね、副作用がある処理を呼ぶので必然的にこいつも副作用があるよ
↓ とりあえず、タイトルと本文組み立てをsend(...)
から引き剥がしてみようかな?
でもこっちに持ってきても結局副作用の中なのであんまり変わらないね
↓ sub
とbody
を value object にしない限りは ね!
初めて緑が現れたね!
組み立て部分はただのコンストラクタに書き出したので、Sub
とBody
それぞれでテストして品質担保しておけるよ!
Sub
とBody
は 引数によってのみ決まる ってことがはっきりしたね、つまり参照透過性があるんだ
ところで、最初のsend(...)
の引数を覚えている?
def send(to: String, name: String, items: Seq[String]): Unit
それが今はこう変わっているよ
def send(to: String, sub: Sub, body: Body): Unit
String
もらうより全然良さそうだよね?
String
だと「文字列って言われても...メアド?それとも件名?まさかヘッダ?」って混乱しちゃうし、item
の仕様が変わったときにsend(...)
まで直さないといけないしね
↓ ここまで来たら「to
もTo
にしちゃえ、っていうかまるっとまとめてMail
にしちゃえ」と思わないかな?
↓ 副作用がなくて品質担保できているTo
, Sub
, Body
の組み合わせでMail
が作れたので、Mail
もそうだ!
これはつまり 今までsend(...)
の中でごっちゃになっていた「メールの組み立て」と「メールの送信」をきっちり分けられた ってことだね!
send(...)
に目を戻してみよう
↓ あとはLib.sendmail(...)
に正しくMail
を文字列化して渡せているかがテストできれば安心なんだけど、手はないのかな?
もちろん手はあるよ!さっきみたいに文字列化を別のクラスに切り出しても良いんだけど、今度はせっかくなので di の恩恵を受けてみよう
↓ 適当にMailer
とMailerImpl
を作って、そいつで社内ライブラリを扱うようにしてみた
↓ テスト時はこの impl を別で作って、中身をLib.sendmail
じゃあなくてassert
にしてしまう、という手があるよ!
これで「渡したMail
がちゃんと文字列化されてること」もテストできたね
↓ なのでこっちも緑になるぞ!
リファクタリング完了!
モックのメールサーバでも立てる?
それよりはるかに楽に色々テストできたね!
やってみて
素直にテストできる範囲を広げようとコードをいじってただけだけど、ドメイン的に得られたことがある
それはsend(...)
の引数がString
からMail
になったことと、メール組み立てとメール送信が分離されたこと
せっかくMail
ができたので「Mail
って entity? value object?」なんて考えてみると、さらに色々発見があるかも しれない
他にも「メール送信履歴」や「二重送信防止」とか考えるとMailId
みたいなのがないとダメじゃね?とかね
とにかく メールについてあれこれ考える土台ができている はず、これはString
の羅列では行えない
まとめ
こんな感じで既存の実装をちょっと「テストちゃんとしよ」って思っていじると 新しいドメインクラスが生まれる と言うのは、実は結構ある
そして副作用なく小さく品質担保されているコードが増えると、リファクタリングがしやすくなる
こんなアプローチでドメインを磨いていくのも、個人的には結構アリなんじゃあないかな、と思う 楽しいしね
おしまい
明日はどんな内容かなー