これはなに
『8.6.4 どのようにログ出力オブジェクトを受け渡すのか? - 単体テストの考え方/使い方』に登場する「環境コンテキスト」という単語について、日本て通常使われている「コンテキスト」と訳文の「コンテキスト」の間に乖離があるように感じた。幸いこの「環境コンテキスト」という単語が『Dependency Injection Principle, Practices, Patterns』という本で語られている(これもまたコンテキスト)ということが明記されていたので、原著の内容に言及するページを探し、どういう意味で使われているのかを調べた。
なお翻訳したのは以下のページ。翻訳はDeeplにかけただけなので、時間を見つけてもうちょい読みやすい日本語に書き換えます。乞うご期待。
The Ambient Context Anti-Pattern - freecontent.manning.com
(なお原著はこのリンクから飛べるページにあるクーポンコードを使うことで最も安く買えます。それでも$40.00くらいするけど。。円安つらい >< )
翻訳
The Ambient Context Anti-Pattern
(『Dependency Injection, Principles, Practices, and Patterns』 by Steven van Deursen and Mark Seemannより解説)
この記事では、環境コンテキストDIのアンチパターンについて、その正体、見分け方、そしてなぜ危険なのかを解説します。
このリンクから『Dependency Injection, Principles, Practices, and Patterns』を37%オフで購入できます! manning.comのチェックアウト時に、割引コードボックスにコードfccseemannを入力してこの本を手に入れましょう!
Ambient Contextのアンチパターンは、Service Locatorと関連している。Service Locatorが無制限のDependencyの集合にグローバルにアクセスできるのに対し、Ambient Contextは単一の強い型付けのDependencyを静的アクセッサで利用できるようにする。
定義
Ambient Contextは、Volatile Dependencyやその動作に静的なクラスメンバーを使用することで、Composition Root外のアプリケーションコードにグローバルアクセスを提供します。
Volatile Dependencyとは、時として望ましくない副作用を伴うDependencyのことである。これには、まだ存在しないモジュールや、実行環境に悪影響を及ぼす要件が含まれることがあります。これらのDependencyは、DIによって対処され、Abstractionsの後ろに隠されているDependencyです。
次のリストは、Ambient Contextアンチパターンが実際に使われていることを示しています。
リスト1 アンビエントコンテキストのアンチパターンの使用(BAD CODE!)
public string GetWelcomeMessage()
{
ITimeProvider provider = TimeProvider.Current; ❶
DateTime now = provider.Now;
string partOfDay = now.Hour < 6 ? "night" : "day";
return string.Format("Good {0}.", partOfDay);
}
Current静的プロパティはAmbient Contextを表し、ITimeProviderインスタンスにアクセスできるようにします。これにより、ITimeProviderのDependencyが隠され、テストが複雑になる。
この例では、ITimeProviderは、システムの現在時刻を取得できる抽象化を提示しています。アプリケーションで時間がどのように認識されるかに影響を与えたい場合があるため(たとえば、テストのため)、DateTime.Nowを直接呼び出したくありません。コンシューマがDateTime.Nowを直接呼び出すのではなく、DateTime.Nowへのアクセスを抽象化の背後に隠すことが良い解決策となります。静的なプロパティやメソッドを通じて、コンシューマがデフォルトの実装にアクセスできるようにすることは、あまりにも魅力的です。リスト1では、CurrentプロパティがデフォルトのITimeProvider実装へのアクセスを許可しています。
Ambient Contextは、Singletonパターンと構造が似ています[2]。どちらも、静的なクラス・メンバーを使用して、Dependencyにアクセスできます。違いは、Ambient ContextではDependencyを変更できるのに対し、Singletonパターンではその特異なインスタンスが決して変更されないようにすることである。
システムの現在時刻にアクセスすることは、よくあるニーズです。ITimeProviderの例をもう少し深く掘り下げてみましょう。
例アンビエントコンテキストによる時間へのアクセス
時間をコントロールする必要がある理由は数多く存在します。多くのアプリケーションでは、時間やその経過に依存するビジネスロジックがあります。前の例では、現在の時間に基づいてウェルカムメッセージを表示するというシンプルなケースを見ました。
時間を扱う必要性は広く浸透しているため、開発者はアンビエントコンテキストを使用して、このようなVolatile Dependencyへのアクセスを簡略化したいと感じることが多い。次のリストは、ITimeProvider Abstractionの例です。
Listing 2 An ITimeProviderAbstraction
public interface ITimeProvider
{
DateTime Now { get; } ❶
}
コンシューマーがシステムの現在時刻を取得できるようにする。
次のリストは、このITimeProvider AbstractionのTimeProviderクラスの単純化された実装を示すものです。
Listing 3 A TimeProvider ambient Context implementation (BAD CODE!)
public static class TimeProvider ❶
{
private static ITimeProvider current = ❷
new DefaultTimeProvider();
public static ITimeProvider Current ❸
{
get { return current; }
set { current = value; }
}
private class DefaultTimeProvider : ITimeProvider ❹
{
public DateTime Now { get { return DateTime.Now; } }
}
}
設定された ITimeProvider の実装にグローバルにアクセスするための静的なクラス ❶。
❷ 実システムクロックを使用する Local Default の初期化
❸ ITimeProvider の Volatile Dependency にグローバルに読み書きできる静的なプロパティ。
実システムクロックを使用するデフォルトの実装
TimeProviderの実装を使用すると、先に定義したGetWelcomeMessageメソッドをユニットテストすることができます。次のリストは、そのようなテストを示しています。
Listing 4 A unit test depending on an ambient Context (BAD CODE!)
[Fact]
public void SaysGoodDayDuringDayTime()
{
// Arrange
DateTime dayTime = DateTime.Parse("2017-05-14 6:00");
var stub = new TimeProviderStub { Now = dayTime };
TimeProvider.Current = stub; ❶
var sut = new WelcomeMessageGenerator(); ❷
// Act
string actualMessage = sut.GetWelcomeMessage(); ❸
// Assert
Assert.Equal(expected: "Good day.", actual: actualMessage);
}
❶ デフォルトの実装を、常に指定されたdayTimeを返すStubに置き換えます。
❷ WelcomeMessageGeneratorのAPIは、そのコンストラクタがITimeProviderが必須Dependencyであることを隠しているため、不誠実である。
❸TimeProvider.Current と GetWelcomeMessage の間には、暗黙の関係がある。
これは、Ambient Contextのアンチパターンの1つのバリエーションです。その他、よく遭遇するバリエーションとして、以下のようなものがあります:
グローバルに設定されたDependencyの動作をコンシューマが利用できるようにするAmbient Context。先ほどの例で言えば、TimeProviderはコンシューマに静的なGetCurrentTimeメソッドを提供し、内部でそれを呼び出すことで使用するDependencyを非表示にすることができる。
アンビエントコンテキストは、静的なアクセサーとインターフェイスを1つの抽象化として統合する。前の例で言えば、Nowインスタンスプロパティと静的Currentプロパティの両方を含むTimeProviderベースクラスが1つあることになる。
カスタム定義の抽象化ではなく、デリゲートが使用されるアンビエントコンテキスト。かなり説明的なITimeProviderインターフェイスを持つ代わりに、Funcデリゲートを使用して同じことを実現することができます。
アンビエントコンテキストには、さまざまな形や実装があります。繰り返しになるが、Ambient Contextに関する注意点は、何らかの静的なクラス・メンバーによってVolatile Dependencyに直接または間接的にアクセスすることである。
アンビエントコンテキストの例は他にもたくさんありますが、この例は一般的で広く普及しているため、私たちがコンサルティングを行った企業では数え切れないほど見かけました。では、なぜ問題なのか、どう対処すればいいのかを説明します。
Analysis of ambient Context
Ambient Contextは、Cross-Cutting ConcernがVolatile Dependencyとして存在し、それが偏在的に使用されている場合に遭遇するのが普通です。このユビキタスな性質が、コンストラクタ注入から離れることを正当化すると開発者に思わせています。Dependencyを隠すことができ、アプリケーションの多くのコンストラクタにDependencyを追加する必要性を回避することができます。
Ambient Contextの問題点は、Service Locatorの問題点と関連している。以下はその主な問題点である:
Dependencyが隠されてしまう。
テストが難しくなる。
Dependencyをそのコンテキストに基づいて変更することが難しくなる。
Dependencyの初期化とその使用方法の間にTemporal Couplingがある。
Ambient ContextでDependencyにグローバルにアクセスできるようにしてDependencyを隠すと、クラスにDependencyが多すぎるという事実を隠すことが容易になる。これは、コンストラクタのオーバーインジェクションのコード臭に関連しており、一般的に単一責任原則に違反していることを示すものである。
クラスが多くのDependenciesを持っている場合、それは必要以上のことをしていることを示しています。理論的には、多くのDependenciesを持つクラスでありながら、変更する理由が1つであることは可能です。クラスが大きくなればなるほど、このガイダンスに従う可能性は低くなります。アンビエントコンテキストを使用すると、クラスが複雑になりすぎてリファクタリングが必要になったという事実を隠蔽することができます。
また、Ambient Contextはグローバルな状態を提示するため、テストを難しくする。リスト4で見たように、あるテストがグローバルな状態を変更すると、他のテストに影響を与える可能性があります。これはテストが並行して実行される場合の話ですが、順次実行されるテストであっても、あるテストがティアダウンの一環として変更を戻すのを忘れると、影響を受けることがあります。テストに関連するこれらの問題は軽減できますが、特別に作られたアンビエントコンテキストと、グローバルまたはテスト固有のティアダウンロジックを構築する必要があります。このため、複雑さが増しますが、代替案はそうではありません。
Ambient Contextを使用すると、Dependencyの異なる実装を異なる消費者に提供することが難しくなります。例えば、システムの一部が現在のリクエストの開始時に固定された時間で動作する必要がある場合、他の(おそらく長時間実行される)オペレーションはライブアップデートされるDependencyを取得する必要があります。Ambient Contextを使用する場合、コンシューマは通常、異なる実装を返すことができるように、Ambient Contextに追加の情報を提供しなければなりません。これはコンシューマーを不必要に複雑にする。
Ambient Contextを使用すると、そのDependencyの使い方が時間的なレベルで結合され、Temporal Couplingと呼ばれるコード臭が発生する。コンポジションルートでアンビエントコンテキストを初期化しない限り、クラスが初めてDependencyを使用し始めると、アプリケーションは失敗する。私たちはむしろ、アプリケーションを速く失敗させたいと考えています。
Ambient Contextは、Service Locatorほど破壊的ではありませんが、任意の数のDependencyを隠すのではなく、1つのDependencyを隠すだけなので、よく設計されたコードベースには適さないものなのです。より良い代替案があり、その代表的なものがコンストラクタ注入です。
今回の記事は以上です。Ambient Contextについてもっと詳しく知りたい方は、こちらのliveBookでご確認ください。
その他、DI関連でよくある記事もご覧いただけます:
topics:
Writing Maintainable, Loosely-Coupled Code
Understanding Property Injection
Understanding Method Injection
Understanding the Composition Root
Understanding Constructor Injection