追記 (2020-12-27)
より基礎的な説明を Minimal Cake Pattern 再考 にまとめました。Minimal Cake Pattern という言葉を初めて聞く人は、こちらの記事を先に読むことをおすすめします。
Scalaにおける最適なDependency Injectionの方法を考察する 〜なぜドワンゴアカウントシステムの生産性は高いのか〜 で紹介されている Minimal Cake Pattern をもう少し詳しく解説します。
Minimal Cake Pattern それ自体は自明なものですが、いくつかの規約に従わないと混乱を招きます。以下の事柄を覚えておくと良いでしょう。
MixIn に extends
はいらない
trait UsesSomething {
def something: Something
}
trait MixInSomething extends UsesSomething { // <- この継承はいらない
def something = new Something
}
abstract class Service extends UsesSomething {
// ...
}
object Service extends MixInSomething
MixIn に対して、対応する Uses を継承させたいという気持ちを抱く人が少なくないようです。これは特にメリットがないので必要ありません。
継承させておけば以下のタイポを簡単に検出できそうな気がしますが、
trait MixInSomething extends UsesSomething {
def somthing = new Something // フィールド名のタイポ
}
この継承があろうがなかろうが UsesSomething
, MixInSomething
, abstract class Service
は合法で、object Service
で初めてエラーになります。trait MixInSomething extends UsesSomething
は毒にも薬にもなりません。
override
修飾子を付ければタイポの検出はできます。
trait MixInSomething extends UsesSomething {
override def somthing = new Something // 親クラスに somthing がないのでここでコンパイルエラー
}
とはいえ、override
を付けると別のうっかりミスを見つけにくくしてしまうので私は好みません。
trait UsesHogeCreateService {
def hogeService: HogeCreateService
}
trait MixInHogeCreateService {
override val hogeService: HogeCreateService
}
trait UsesHogeDeleteService {
def hogeService: HogeDeleteService
}
trait MixInHogeDeleteService {
override val hogeService: HogeDeleteService
}
abstract class Controller
extends UsesHogeCreateService
with UsesHogeDeleteService
object Controller
extends MixInHogeCreateService
with MixInHogeDeleteService // hogeService が被っているが、override が付いているので Scala の継承の仕様によって粛々と後勝ちになる
これは次に説明するもう一つのアンチパターン「注入するクラス名とインスタンス名が一致していない」との合わせ技ですね。
注入するインスタンス名はクラス名と一致させる
// 良い例
trait UsesSomeClass {
def someClass: SomeClass
}
// ダメな例
trait UsesSomeClass {
def someCls: SomeClass
}
長いからってフィールド名を略すと、上のような問題が起きるし、「あれ、フィールド名なんだったっけ・・・?」ってなるのでお勧めしません。
Uses
に対応する MixIn
は複数あってよい
trait UsesHoge
はクラスに対して一つだけ定義し、その内容は def hoge: Hoge
であるべきです。一方、それに対応する MixIn は一つとは限らないので、トレイトの命名規則などは適当で良いでしょう。
import org.joda.time.DateTime
// 現在時刻を返す君
trait Clock {
def now(): DateTime
}
// システムクロックの現在時刻を返す君
object SystemClock extends Clock {
def now() = new DateTime()
}
// あらかじめ設定した時刻を返し続ける君(ユニットテスト用)
class MockClock extends Clock {
private[this] var currentTime = new DateTime(0)
def setCurrentTime(dateTime: DateTime) = { currentTime = dateTime }
def now() = currentTime
}
trait UsesClock {
def clock: Clock
}
trait MixInSystemClock {
val clock: Clock = SystemClock
}
trait MixInMockClock {
val clock: MockClock = new MockClock
}
なんでも MixIn で解決しようとしない
Minimal Cake Pattern による依存性注入は依存先モジュールを注入するのには便利ですが、単純なフラグなどを注入するのには不向きです。UsesBoolean
とか言われても困ります。
Uses/MixIn トレイトを使わず、直接注入することも検討してください。
abstract class Service extends UsesSomething {
def enableExperimentalFeatures: Boolean
// ...
}
object Service extends MixInSomething {
// devインスタンスでのみ実験的機能を有効にする
val enableExperimentalFeatures = config.isDevEnvironment
}
おまけ
以下は興味のある方だけ読んで頂ければ。
誕生の経緯
Java+Guice でDIする場合、こんな感じにコンストラクタを書いて依存性を注入することになると思います。
// アノテーションは省略
class Service {
private final DependingModuleX dependingModuleX;
private final DependingModuleY dependingModuleY;
public Service(
dependingModuleX: DependingModuleX,
dependingModuleY: DependingModuleY,
// ...
) {
this.dependingModuleX = dependingModuleX;
this.dependingModuleY = dependingModuleY;
}
}
ニコニコアカウントシステムにDIを導入するとき、Guiceを採用するかどうか悩みました。当時のチームはほとんどが新卒で(pab_techさんは別のプロジェクトに攫われていた)、DI経験があるのは私だけ(それも Java+Guice を半年ほど使った程度)でした。結局、うまく Guice を使いこなす自信がなかったので採用は見送ることにしました。
代わりに手動で依存性注入をすることにしました。上のようなコンストラクタによる依存性注入を行い、ファクトリメソッドを手で実装する方針です。若干の面倒臭さはありますが、Guice の学習コストよりは低いと思っていました。
Cake Pattern は私には難しすぎました。実戦での Scala: Cake パターンを用いた Dependency Injection (DI) を読みましたが、何度読み返しても そして、自分型アノテーション (self-type annotation) を用いて UserRepository への依存性を宣言する。
のあたりで挫折しました。Minimal Cake Pattern が生まれた瞬間のことはよく覚えていませんが、この記事のサンプルコードのよくわからない部分を適当に削ったらできたんではないかと思います。
しばらくの間は Minimal Cake Pattern のメリットが明らかでなかったので、コンストラクタによる注入パターンも書かれ続けていました。しかし依存先モジュールが多いクラスが生まれてくると、だんだん Minimal Cake Pattern の便利さが分かってきました。コンストラクタでは引数の順番を気にしなければいけないのに、Minimal Cake Pattern だと MixIn の継承順序はどうでもいいのです。依存先クラスが10個以上あるクラスに対して正しい順番でコンストラクタ引数を与えるのは苦行でしかありませんでした 1 。
こんな感じにてきとーに作られたパターンなので、まぁ自明と言ってもいいんではないかと思います。よろしくお願いします。
-
依存先クラスが10個以上あるという設計がまずおかしいのではという指摘は完全に正しいです。 ↩