Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
11
Help us understand the problem. What are the problem?

posted at

updated at

Organization

Minimal Cake Pattern 再考

かつて Scalaにおける最適なDependency Injectionの方法を考察する 〜なぜドワンゴアカウントシステムの生産性は高いのか〜 という記事が公開されたとき、ぶっちゃけ私は 100 日も経てば忘れられているだろうと思っていました。私の予想に反して 2020 年現在でも Twitter では Minimal Cake Pattern への言及がたまに見られ、中にはこのパターンが(あるいは DI そのものが)難しいと感じる人もいるようなので、今一度このパターンについて整理してみようと思います。

依存性注入とは

Minimal Cake Pattern は 依存性注入 (Dependency Injection, DI) を実現するためのデザインパターンです。ですのでまずは DI についておさらいしましょう。理解済みであればこの節は飛ばして構いません。

例として、時間の計測を行う Stopwatch クラスを考えてみましょう。

Stopwatch.scala

class Stopwatch {

  private[this] var startedAt: Option[Long] = None
  private[this] def now(): Long = System.currentTimeMillis()

  /**
   * 時間の計測を開始します。
   */
  def start(): Unit = {
    startedAt = Some(now())
  }

  /**
   * start() が呼ばれてからこのメソッドが呼ばれるまでに経過したミリ秒数を返します。
   */
  def stop(): Long = {
    now() - startedAt.getOrElse(new IllegalStateException("The stopwatch has not started yet."))
  }
}

このクラスをテストするにはどうすればいいでしょうか?実際に一定時間を計測してみるという実装が考えられます。

StopwatchSpec.scala
class StopwatchSpec extends FlatSpec {
  "Stopwatch" should "measure five seconds" in {
    val stopwatch = new Stopwatch()
    stopwatch.start()
    Thread.sleep(5000)
    val result = stopwatch.stop()
    assert(result === 5000)
  }
}

このテストは良いテストでしょうか?ダメですね。すぐに思いつく限りでも、次の問題点が挙げられます。

  1. 遅い - 必ず 5 秒消費してしまう
  2. 不安定 - result がぴったり 5,000 になる保証がない

ではどうしたらいいでしょうか?現在時刻の取得を別のクラスにくくりだしてみましょう。

Stopwatch.scala(2)

trait Clock {
  def now(): Long
}

object SystemClock extends Clock {
  def now(): Long = System.currentTimeMillis()
}

class Stopwatch(clock: Clock) {

  private[this] var startedAt: Option[Long] = None

  /**
   * 時間の計測を開始します。
   */
  def start(): Unit = {
    startedAt = Some(clock.now())
  }

  /**
   * start() が呼ばれてからこのメソッドが呼ばれるまでに経過したミリ秒数を返します。
   */
  def stop(): Long = {
    clock.now() - startedAt.getOrElse(new IllegalStateException("The stopwatch has not started yet."))
  }
}

そして、テストでは特定の時刻を返すような Clock の実装を使用するのです。

StopwatchSpec.scala(2)
class StopwatchSpec extends FlatSpec {

  class FakeClock extends Clock {
    private[this] var currentTime: Long = 0
    def setCurrentTime(value: Long): Unit = { this.currentTime = value }
    def now(): Long = currentTime
  }

  "Stopwatch" should "measure five seconds" in {
    val clock = new FakeClock()
    val stopwatch = new Stopwatch(clock)
    stopwatch.start()
    clock.setCurrentTime(5000)
    val result = stopwatch.stop()
    assert(result === 5000)
  }
}

これによって、テストが当初抱えていた問題はすべて解決されました。実行に 5 秒かかっていたテストは sleep することなく直ちに終わるようになり、実行時の気まぐれ(stop-the-world GCなど)によってテストが落ちることもなくなりました。このように、プロダクションコードとテストコードで異なるふるまいをさせるために、あるクラスが依存するクラスを外部から「注入」することを依存性注入と呼びます。この例では、 Stopwatch クラスに Clock クラスを注入したわけです。

一般に、DI はテストを「ユニットテスト」にするために行われます。ユニットテストという用語は厳密に解釈されることが少ないように思いますが、本来は対象のクラスやメソッド単体をテストするものを指し、ユニットテストの中でのネットワーク・プロセス間通信やディスクアクセスなどは許されません。

クラスをテストするために、テスタは、しばしばデータベースと対話するコードを書く。これは間違いである。なぜなら単体テストは自分のクラスの境界を超えるべきではないし、特にそのようなプロセス/ネットワーク境界を超えることは許されない。その理由は単体テストスイートで受け入れがたい性能問題が起こりうるからである。
https://ja.wikipedia.org/wiki/%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88

クラス境界を超えたテストには、性能問題の他に安定性の問題があることは既に見たとおりです。テストが成功するためには、プロダクションコードとテストコードが正しく書かれているだけでなく、テストを実行する環境(たとえばデータベースのような)が正しく構築されている必要があります。この環境を構築し使用するのにはコストが掛かり、またテスト失敗時には環境に問題があるのかコードに問題があるのかの切り分けの手間が生じてしまいます。環境に依存しないテストをユニットテストと呼んで区別し、プロダクションコードの論理的な正しさをできるだけユニットテストで保証することで、開発速度を向上できるのです。1

DI はプロダクションコードとテストコードで異なる依存クラスの実体を使えるようにする実装技法で、これによってテストコードに FakeClock のようなテストダブルを注入できます。

依存性注入の手法

上の例では、依存先のクラスである Clock をコンストラクタ引数を介して注入しました。この DI を初めて行う人にとって最も思いつきやすいであろう方法は Constructor Injection と呼ばれているようです。

Constructor Injection だけが DI の実装方法ではありません。あるクラスに依存先のクラスのインスタンスを持たせられればいいだけのことなので、単に setter を定義してもいいです。これは Setter Injection とでも呼ぶべきでしょうが、この方法はその依存先クラスのフィールドを mutable にしなければならなくなってしまうので、あまり好まれません。

他にインスタンスを注入する方法は考えられるでしょうか?抽象クラス、抽象フィールドを使えることにお気づきでしょう。つまりこういうことです。

Stopwatch.scala(3)
abstract class Stopwatch {

  // 抽象メンバーとして clock を宣言する。このままではこのクラスをインスタンス化できないが、
  // clock は参照できるので start() と stop() は実装できる。
  def clock: Clock

  private[this] var startedAt: Option[Long] = None

  /**
   * 時間の計測を開始します。
   */
  def start(): Unit = {
    startedAt = Some(clock.now())
  }

  /**
   * start() が呼ばれてからこのメソッドが呼ばれるまでに経過したミリ秒数を返します。
   */
  def stop(): Long = {
    clock.now() - startedAt.getOrElse(new IllegalStateException("The stopwatch has not started yet."))
  }
}

// プロダクションコードでは抽象メンバーに実体を代入し、これでインスタンス化できるようになる。
class StopwatchForProd extends Stopwatch {
  val clock = new SystemClock
}
StopwatchSpec.scala(3)
// テストコードではテストダブルを注入し、このクラスに対してユニットテストをかける。
class StopwatchForTest extends Stopwatch {
  val clock = new FakeClock
}

できましたね!これは Abstract Member Injection とでも呼んでおきましょう。Scala では抽象メソッド (def) をフィールド (val) で実装できる 2 のですが、Java などでは(注入するインスタンスを何度も生成したくないなら)もうひと工夫必要かもしれません。

ここで止めても良いのですが、注入したい依存オブジェクトがたくさんあった場合を考えてみましょう。その数だけ抽象メソッドを宣言し、実体を代入するのは面倒です。ここでちょっとした hack を加えてみましょう。まず、抽象フィールドの宣言を行うトレイトと、実体の定義を行うトレイト3を定義するのです。

Clock.scala
trait UsesClock {
  def clock: Clock
}

trait MixInSystemClock {
  val clock = new SystemClock
}

trait MixInFakeClock {
  val clock = new FakeClock
}

StopwatchClock に依存していることを示すには、単に StopwatchUsesClock を継承させればいいです。同様に、SystemClock の実体を注入するには MixInSystemClock を継承させましょう。さらに、 Stopwatch を使う誰かのために、UsesStopwatchMixInStopwatch も作ってあげましょう。

Stopwatch.scala(4)
abstract class Stopwatch extends UsesClock {

  def start(): Unit = {
    // UsesClock を継承したため clock を参照できる
    startedAt = Some(clock.now())
  }

  // :
}

trait UsesStopwatch {
  def stopwatch: Stopwatch
}

trait MixInStopwatch {
  // Abstract Member Injection の例では子クラスを陽に定義したが、この書き方なら無名子クラスを短く書ける
  val stopwatch = new Stopwatch with MixInSystemClock
}

大変長らくお待たせいたしました。これがお待ちかねの Minimal Cake Pattern です。あるいは mix-in を使ってインスタンスを注入することから、Mix-in Injection と呼んでもいいでしょう(わたしはこちらの名前のほうが好きです)。

Minimal Cake Pattern

ここで Minimal Cake Pattern の実装規約を整理してみましょう。

1. Uses トレイトの定義

trait UsesClock {
  def clock: Clock
}

SomeClass への依存を表現するトレイトとして trait UsesSomeClass を定義します。このトレイトは一つの抽象メソッドだけを持ち、そのシグネチャは def someClass: SomeClass でなければなりません。特に、メソッド名は依存を表現するクラスの名前の lower camel である必要があります。

2. Mix-in トレイトの定義

trait MixInSystemClock {
  val clock = new SystemClock
}

trait MixInFakeClock {
  val clock = new FakeClock
}

trait MixInStopwatch {
  val stopwatch = new Stopwatch with MixInSystemClock
}

SomeClass を注入するためのクラスとして trait MixInSomeClass を定義します。大抵の場合このトレイト名は対応する トレイトの UsesMixIn に置き換えたものになりますが、この SystemClock, FakeClock のようにインタフェースへの依存を宣言して実装クラスを注入するような場合など、 Uses に対応する MixIn は複数あるかもしれません。もちろん、 MixIn トレイトが注入するインスタンス名は Uses トレイトで宣言した抽象メソッド名と一致しなければなりません。

3. 依存性の宣言と注入

abstract class Stopwatch extends UsesClock {
  // (省略)
}

val stopwatch = new Stopwatch with MixInSystemClock

UsesSomeClass を継承することで SomeClass への依存を表現します。同様に、 MixInSomeClass を継承することで SomeClass インスタンスを注入します。

テストコードでは、注入するテストダブルのために MixIn トレイトを定義するのは冗長です。Abstract Member Injection を使えば十分でしょう。

なぜ Minimal Cake Pattern なのか?

Minimal Cake Pattern を持ち出すまでもなく、Constructor Injection という単純な方法によって DI は達成できます。しかし歴史的経緯を言えば、Constructor Injection が既に使われていたニコニコアカウントシステムにおいてこの Minimal Cake Pattern が編み出され、 Constructor Injection を置き換えていったのです。このパターンの何が優れていたのでしょうか?

Minimal Cake Pattern の長所は依存性の宣言と注入の記述の簡潔さにあります。SomeClass への依存を追加するとき、Constructor Injection ではコンストラクタ引数に someClass: SomeClass を追加し、(Scala では不要ですが)受け取ったコンストラクタ引数をフィールドにセットするというボイラープレートコードが生まれてしまいます。一方 Minimal Cake Pattern では、「SomeClass に依存したい」という要求が extends UsesSomeClass という記述にそのまま対応するのです。これは挙動ではなく意図を記述している感覚に近く、ただ短く書けるという以上に爽快な何かがあります。このパターンが(技術的には全然大したことをしていない割には)受け入れられている理由もそこにあるのでしょう。

特に注目すべき違いは、複数のクラスに依存するような場合、Minimal Cake Pattern では Uses トレイトや MixIn トレイトをどのような順番で継承してもいいのに比べ、Constructor Injection では正しい順番でコンストラクタ引数を与えなければいけないという点です。複数のクラスに依存するとき、それらをどのような順番で注入したいという意図はないわけですから、順番を気にせずに必要なクラスを列挙したいものです。Minimal Cake Pattern ではただそうすればいいのです。Constructor Injection ではこの本質的でない引数順序を意識しなければならず、面倒と言わざるを得ません。

Minimal Cake Pattern の簡潔さの肝はどこにあるかというと、フィールド名の命名パターンにあります。Uses トレイトや MixIn トレイトの中で宣言するフィールドの名前はクラス名の lower camel としたことを思い出してください。このように名前を決め打ちして、注入するインスタンスと注入されるメンバーのバインディングをフィールド名をもとに行うことで、記述量は減り、注入順序も意識しなくてよくなったのです。また、このパターンの簡潔さは Scala の記述力に支えられているところも大きいというのは言うまでもありません。

実のところ、このアイデアはアンチパターンとされています。Effective Java によれば命名パターンよりもアノテーションを使うべきとされており、例えば JUnit3 (テストクラスのメソッドのうち test で始まるものをテストケースとみなす) は JUnit4 (メソッド名に関わらず @Test アノテーションが突いたメソッドをテストケースとみなす) に改められました。Minimal Cake Pattern が命名パターンに依存することの問題は、同じクラスのインスタンスを複数個注入しようとしたときに明らかになります。注入するクラス名とフィールド名が直接対応するので、同じクラスのインスタンスを 2 個注入しようとすると衝突してしまうのです。わたしの経験上そのようなケースに遭遇することは稀ですが、もしそのようになれば Abstract Member Injection を併用するのがいいでしょう。

このように Minimal Cake Pattern は hacky なところがありますが、少なくとも tricky では――プログラマの直感を超えた挙動をすることは――なく、現実の問題の多くをうまく解決します。ちなみに、Effective Java に従ってアノテーションベースの DI を模索していくと、それは結局 Guice になるのではないかと思います。

Guice との比較

Minimal Cake Pattern と Guice との比較は長くなるので、以前の議論の結論を繰り返すのみにとどめます。Minimal Cake Pattern のメリットは正しく依存性注入できていることを静的に検査できること、デザインパターンに過ぎないため外部ライブラリに依存せず、学習コストも少ないことが挙げられます。Guice のメリットはファクトリメソッドの実装を省略し、実装量を削減できる点です。

わたしは Minimal Cake Pattern があれば Guice は不要だと思っていますが、使用するフレームワークが Guice ベースのときは素直に Guice を使うことにしています。実際のところ最近は Play Framework しか使っていないので、ここ数年 Minimal Cake Pattern は使っていません。しかし静的に DI できないことによる苦しみを感じることが多く、Play よ......という気持ちでいっぱいです。

Vanilla DI との比較

「バニラDIについて」への回答 という記事で Constructor Injection と Minimal Cake Pattern の比較が行われていることに 2 年越しに気づいたので、今更ではありますがわたしの意見も述べてみようかと思います。

デメテルの法則違反

これはちょっとよくわかりませんでした。わたしは Swift 経験がないのですが、違反とされていたコード を Scala に直すとこういうことでしょうか。

trait UsesClock {
  val clock: Clock
}

class FileReader extends UsesClock {

  def read(filename: String): (ZonedDateTime, String) = {
    (ZonedDateTime.now(clock), "DUMMY")
  }
}

class Main extends UsesFileReader {

  def main(): Unit = {
    fileReader.clock // ここで clock が参照できるべきではないということか?
  }
}

それはそうなのですが、だったら protected にすればいいじゃない4。あるいはわたしがなにか論点を見落としている......?

trait UsesClock {
  protected[this] val clock: Clock
}

責務過多

DI をやりやすくすることは依存性の肥大を招くことと表裏一体であるという指摘はそのとおりです。それでもわたしが Minimal Cake Pattern を好む理由は 2 つあります。

まず自明な反論として、そのデメリットを補って余りあるほどのメリットが Minimal Cake Pattern がある......というより Constructor Injection がつらすぎることが挙げられます。ユニットテスト原理主義者のわたしは ExecutionContext, Clock, Random などの非決定性をもたらすクラスも DI するので、適度に責務を分解した後であっても 3-5 個程度の依存オブジェクトが生まれることは珍しくありません。これは Contructor Injection するにはちょっとしんどい数です。

論文によれば、メソッドの引数の個数が増えるほど引数の順番を間違えるバグ (argument selection defect) が起きやすいとされています。特に 6 個以上になるとバグの確率が顕著に増えるとされていますが、実装上の面倒さは 5 個以下であっても感じられるでしょう。

image.png
Andrew Rice, Edward Aftandilian, Ciera Jaspan, Emily Johnston, Michael Pradel, and Yulissa Arroyo-Paredes.
2017. Detecting Argument Selection Defects. Proc. ACM Program. Lang. 1, OOPSLA, Article 104 (October 2017),
22 pages.

メソッドやコンストラクタ引数が多い際には Builder パターンや名前付き引数を使うのがベストプラクティスとされていますが、構文上の冗長さは否めません。その書きにくさによって依存性の肥大を検知するというのはもっともですが、十分に責務分割されている場合においてもあえて書きにくさを受け入れる理由はありません。Minimal Cake Pattern は簡潔な構文で引数の順序問題を解決でき、その恩恵は引数が 1 つでもあれば(2 引数以上であれば特に)受けられます。

もう一つの反論は、Minimal Cake Pattern を使ったからといって設計破綻の臭いは隠しきれるものではないという点です。責務が肥大化した場合、否応なくユニットテストを書くのが困難になります。そのようなテストコードでは多くのモックオブジェクトを注入し、モック5の挙動を設定しなければならなくなるでしょう。また、依存先の呼び出しに失敗した場合のテストケースを記述していくと、必然的にテストケースも肥大化し、面倒さを感じるはずです。テストにダミーオブジェクト――実際には参照されることのないオブジェクト――を注入することもまた責務肥大の徴候です。2 つのメソッドを持っているクラスを考えてみましょう。これらのメソッドが全く異なるクラスたちに依存している場合、片方のメソッドのユニットテストを書くときには、もう片方のメソッドが使うクラスのダミーを注入することになるでしょう。それぞれのメソッドを別のクラスに切り分ければ、自然にダミーオブジェクトを注入することもなくなります。

このように、Minimal Cake Pattern を使っても設計破綻への敏感さを保つことは十分可能です。Vanilla DI の目的として

  • 目的1: DI の仕組みを単純かつ簡潔に保つこと
  • 目的2: 設計破綻への感度を上げることで「設計のカナリア」とすること

とありますが、これに沿って言えば Minimal Cake Pattern は Constructor Injection より有意に簡潔であり、設計破綻への感度も十分上げられることから、わたしは Minimal Cake Pattern を好んでいます。

結び

というわけで、クリスマスは過ぎてしまいましたがこのケーキを美味しく召し上がっていただければと思います。よろしくお願いします。


  1. そうは言ってもユニットテストという用語は既に広く使われているので、厳密な意味について合意が取れないこともあるでしょう。そのような面倒を避けるためか、Google は Small Test という言葉を定義しています。わたしは日常会話ではユニットテストという語を使いますが、テストの性質を強調したいときにはこの small test を参照するようにしています。 

  2. 正確には private[this] val で宣言した場合を除き、 val のフィールドへのアクセスもコンパイル時に getter 経由のものに書き換えられます。 

  3. ここまで説明なく Scala コードを引き合いに出しておいて今更ですが、Scala に詳しくない読者においては、trait は多重継承可能な抽象クラスだと思っておけばここでは十分です。 

  4. じゃあなんで Minimal Cake Pattern を説明する人はみんな今まで protected をつけてこなかったんだという話になりますが、わたし周辺ではそもそもフィールドの可視性にあまり注意を払わないことが多いです(メソッドの可視性はちゃんとやる)。あえて public にするメリットは特にないのですが、 stateless なコードが書かれることが多いのでフィールドにアクセスされても大して被害がないのと、そもそもデメテルの法則違反が可能であってもあえて犯す人がいなかったので、デメリットを感じたこともありません。 

  5. テストダブル、フェイク、モック、ダミーの定義はGerard Meszaros によるものを参照しています。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
11
Help us understand the problem. What are the problem?