技ビス : 技術、ビジネス、スタートアップ

技術、ビジネス、スタートアップに関する情報と話題とアイディアと事例

ちょっともやっとしたのでリファクタリングしてみた

はてぶ見ていたら下記の記事があったのだけど色々モヤッとしたコードだなと思ったのでリファクタリングしてみる。どうしてもDDD界隈のコードは見るとどうしてもモヤッとしてしまう感じがする。

panda-program.com

もともとのコード

元記事のコードは動かないので、最後の一行以外は動くように適当に変更している。

元記事の趣旨は、このコードであればDollarとYenという違う単位のものを計算しようとするとコンパイルエラーになるから良いということである。しかしながら、このコードを見るとぱっと見て下記の欠点がある。

  • DollarとYenは非常に似た性質を持っているのにequalsが重複している。
  • equalsのなかでinstanceofを使っていて結局ランタイムエラーで救うしかなくなっている。せめてそこはequals(other: Dollar)で良いのではないか。
  • この実装だと通貨が増えるごとにTransactionを実装しなければならない

他にも色々ありそうだけど、一読してわかるのはこのくらい。

普通に考えたらこうなるのでは

すなおにMoneyに通貨の種類をもたせればスッキリする。こっちのほうがコードの見通しとしては良いのではないか?型チェックが使えないのはなやましいところではあるけど、通貨を増やすときの変更もいちいち新しい型を作らなくて良いので非常にスッキリするような気がする。Enumを追加するだけですむのである。*1

そもそもモデリング上の値オブジェクトは何を指していたのか

実世界の問題をシステムやソフトウェアにするためには、どうにかしてコンピュータに理解できる形に落とし込んでいく必要がある。この方法論はたくさんあり、要求工学や業務分析として体系化されている[REBOK, BABOK, SR]。PoEAAやDDDも全てではないがこれらの知識体系の一部に位置づけられると考えられる。違いがあるとすると、PoEAAやDDDは実際のコード例を示しながらモデルがどうコードに落とし込まれるかを特に着目している[PoEAA, DDD]。

したがって前提として、PoEAAやDDDで扱われる値オブジェクトの話やモデリングの話は大分システム寄りの話であるということを理解したほうが良いだろう。ドメインエキスパートたる顧客候補との対話は重要ではあるが、私の経験から言うとあの粒度で対話できるのは一定レベルシステムの知識がある人である*2

その前提で、ソフトウェアでどのように値オブジェクトがモデルとして扱われるべきなのかを見てみよう。エリック・エヴァンスはエンティティ、値オブジェクト、サービスの区別をすることがモデル上重要であるとしている[第5章, DDD]。

そして、値オブジェクトを下記のように定義する。ここで注意すべきは、ここでいうオブジェクトはプログラミング上のオブジェクトとは違う概念であり、あくまでモデリング上の「オブジェクト」のことを指している。

あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること。

たとえば色をモデリングしようとしたときに3原色の赤・緑・青で表現できるだろう。もし、りんごの赤と絵の具の赤を特別に区別する必要がなければこの3つのパラメーターだけ持てば良いのである。これを値オブジェクトと呼ぶ。

ここで混乱しやすいのは、モデリング上のオブジェクトとプログラミング上のオブジェクトは実はあまり関係ないのである。

もっと値オブジェクトは自由でよいのでは

前項の定義に当てはめると僕が作ったコードも値オブジェクトである。不変だし、属性として通貨の種類と値のみが注目されるようにできている。

たしかに型チェックの恩恵にあずかれないという欠点はあるように思われるが、ちゃんとランタイムでエラー吐いて止まればそんなに大きな問題にはならないようには思う。そもそも元のコードもランタイムエラーを使うようにできているので殊更型チェックだけに頼るべきであるというのも変な話だろう。

そして、僕の知る限り型チェックを可能な限りガチガチにすべきであると主張しているのはミノ駆動開発かそうでなくても最近の潮流である。これには結構違和感がある。この型原理主義とも呼べるカルト的な所業はいったいどこから現れたのか。誰か詳しい人教えてほしい。

今回サンプルコードを2つ示したが、どっちのほうが見通しが良いだろうか。DollarとYenには重複したロジックがあり、通貨が増えるたびにTransactionに変更を加える必要があるコードは果たして保守性が高いのだろうか?僕の方のコードなら通貨の追加はenumを一行追加すれば終わりである。果たしてどちらのほうが良いコードなのだろうか。

実際のところ、良し悪しはよくわからない。いや、ここまで言っておいて何だそれはと思うかもしれないが、結局の所どういう方向に拡張しうるシステムなのかに依存する問題なのである。通貨がしょっちゅう変わるコードならたぶん僕のサンプルのほうが良いだろう。しかし、通貨ごとに複雑な税金計算ロジックがあったりなんかすると、おそらく元のコードに近い形になるかもしれない。結局の所、このへんはシステムによるのである。

局所的なコードだけ見て良いとか悪いとかいう事自体がナンセンスである。たぶん、値オブジェクトはもっと自由で良い。短くてパット見てわかり、十分にテストされているコードが結局一番良いのである。ガチガチの型原理主義や過ぎたDDDによる最適化は過度なコードの断片化を招くので個人的にはあまり賛成し難い。

蛇足

genericsつかってなんかいい感じにすればもっと楽になるのではと思ったけどあんまりうまく行かなかった。少なくとも多分Transactionを残すならGenericsを使うのが健全なのかなーということは思う。

追記:無理やりgenericsで型を使ってみた

下記のようなコードにするとメンテナンス性を保ちつつ型の力を借りることができそう。ただ、いらないメンバ変数dollarとyenを定義しないといけなくて気持ち悪い。

Typescriptは構造型を採用しているのでClassの名前がちがってもメンバが同じ構造をしていると同じ型であると判定してしまう[faq]。これを避けるために必要ないけどメンバ変数を定義しているのでした。

よく考えると、値オブジェクトを作ろうとして細かくクラスを分割すると似たような構造のクラスがたくさんできるわけなんですが、構造型だとすると実は型チェックすり抜けちゃうパターンあって意味ないんじゃないかなとふと思いました。

追記:phantom typeを使ってみた

ブコメで「誰かをカルトよばわりするのは、せめてPhantomTypeを含めた一般的な(TypeScriptに限らない)型の技法を学んでからでも遅くはない」と言われた。Phantom Typeってなんぞ?ちょっとググってみた。

zenn.dev

なるほど。前項にあげたgenerics使ったコードとほぼ同じやん・・・neverをつかうとたしかにjsにトランスパイルされたときにいい感じに消えるのでneverを使うバージョンを作ってみた。tscにかけてもらえればわかるけど、ムダの少ないjsに変換されますがバッチリ型チェックは効きます。すごくいい感じ。

今回結構typescriptに突っ込んで色々と書いてみたけど、ここまでハック的なテクニックを使うべきなのかは結構趣味が分かれそうな気はする。

どうなんでしょうね?

*1:Typescriptでenumを使うこと自体には色々議論があるのは知っているがここでは触れない

*2:Web系PMは10年以上、コンサルで3年、プロトタイピングする中小企業で取締役3年やってました