Edited at

なぜDependency Injectionは、それほど悪いものではないのか

関数型プログラミングは好きですか? 私は嫌いです。副作用がなく不変オブジェクトを使ったプログラミングは好きですが、関数型プログラミングは嫌いです。自分が難しいテクニックを使えるほど頭が良いということを示すためだけに関数型プログラミングを使うのは、いい加減やめるべきだと思います。関数型プログラミングだろう手続き型だろうと、大切なのはわかりやすく保守しやすいコードを書くことであるはずです。

難しいテクニックを知っている人が偉い、より難解なコードを書く人が偉いという技術至上主義が、「for文禁止」のような傲慢さを生み出している原因ではないでしょうか。かつてデザインパターンがたどった道が、関数型プログラミングに続いているように思います。

xkcd


なぜ依存性注入は、悪いものなのか

(この節のコードはこの動画を参考にしたものです)

そうした人たちは次なる標的として、DIコンテナーをターゲットに定めました。

関数型プログラミングでは、関数は純粋であるべきです。ある入力に対し、常に同じ出力を返さなくてはなりません。

IoCコンテナーの目的はそもそも、あるモジュールの内部の実装を、外部から柔軟に書き換えられるようにすることです。そもそも目的からして、関数型プログラミングの趣旨とは対立しているわけです。

@ApplicationScoped

public class BusinessLogic{

private final Config config;

@Inject
public BusinessLogic( Config config ) {
this.config = config;
}

public Name readName() {
var parts = config.getName().split( " " );
return new Name( parts[0], parts[1] );
}

public Age readAge() {
return new Age( config.getAge() );
}

public Person readPerson() {
return new Person( readName(), readAge() );
}
}

各メソッドはConfigに依存しています。Configはメソッドの引数として直接渡されるのではなく、DIコンテナーによって間接的に取得されます。非純粋な関数は副作用につながります。副作用はよくないですよね?

ではどうすればいいかと言えば、間接的な依存を、直接的にしてしまえばいいわけです。全ての依存関係を直接、引数として渡すようにすればよいのです。DIコンテナーに任せていたConfigインスタンスの取得を、引数に移動させましょう。

def readName(config: Config): Name = {

val parts = config.name.split(" ")
Name(parts(0), parts(1))
}

理論的には結構な話ですが、実際のアプリケーションは依存関係に満ちています。設定、ユーザー情報、セッション、DBアクセス、キャッシュサーバーの呼び出し、外部API、メッセージキュー……。実際にやってみると山のような数の引数を関数ごとに引き連れる、Javaを馬鹿にできないような大量のボイラープレートが出来上がります(それでもなお、そうすべきだと主張する人も多いことは確かです)。

この問題の一般的な解決策は、リーダーモナドやステートモナドを使うことです。モナドを使うことで、関数は純粋なまま、コンテキストにアクセスできるようになります。

(def read-person

(domonad reader-m ; wtf is this?
[name read-name
age read-age]
{:name name :age age}))

これで、美しいコードになりました。めでたしめでたし。


コンテキスト

実際のところ、モナドを使ったコードはIoCコンテナーを使ったコードより、どれだけ優れているのでしょうか? 分かりやすさは主観的なものですが、「何が何でもモナドを使わなければ!」と思うほど圧倒的にわかりやすいとは言えないと思います。単に関数を純粋にしたいという理由だけで、モナドを導入する価値はあるのでしょうか?

いずれにせよ、これだけ多くの「モナドとは何ぞや」記事があふれている時点で、モナドは簡単でもわかりやすいものでもないと結論付けて差し支えないでしょう(こう言うとオブジェクト指向も解説記事がたくさんあるじゃないかと反論されるかもしれませんが、オブジェクト指向も十分難しいものです。私はいまだにJavaでクラスを継承する正しい方法がわかりません)。そして難しいのはそれ自体悪いことです。モナドが解決しようとしていた問題よりもずっと大きな問題が生じてしまいます。

そもそもの問題は依存関係をなくし、関数の中で表現することでした。ですがここで一つの疑問が生じます。どうしてDIを使ったプログラミングモデルでは、そもそもConfigをパラメーターとして渡していなかったのでしょうか。

答えは簡単で、Configはパラメーターではないからです。アプリケーションの実行におけるコンテキストだからです。パラメーター渡し方式では呼び出し側が、コンテキストの文脈を決定します(引数に何を渡すのかを決めるのは当然呼び出し側ですよね)。DIコンテナーでは、呼び出し元も呼び出される側も、コンテキストに関して責任を持ちません。まさにDIコンテナーが責任を負うのです。これこそまさに全てをパラメーターで渡そうとする弊害であり、多くの関数が自身では使う必要のないパラメーターを持つ原因なのです。


リーダーモナド


  1. リーダーモナドはコンテキストを渡す一般的な手法だ

  2. 基本的に、モナドの中に暗黙的な読み込みをラップする

  3. アドバンテージ: 読み込みが型として抽象化される

  4. しかし、私の信じるところでは、これはスズメを大砲で打つようなものだ

  5. モナドはシーケンス化に関わるものであり、コンテキストを渡すこととは関係がない

What to Leave Implicit by Martin Odersky (58分付近)



なぜ依存性注入はそれほど悪いものではないのか

前節をまとめると、コンテキストは入力ではなく、従ってコンテキストは引数で渡されるものではないということになります。

依存性注入は、引数を渡すことではないのです。パラメーターからコンテキストを一緒くたにして関数の入力とみなすと、どれがコンテキストでどれがパラメーターなのか見通しが悪くなってしまいます。

モナドはコンテキストを抽象化し、パラメーターと分離するための手段になりますが、代わりにモナドそのものという複雑さが発生します。

そして何よりもDependency Rejectionの問題は、本来の課題であった「コンテキスト」の扱いに取り組む代わりに、教条主義的にあらゆる問題が関数型プログラミングなら解決できると考えているところでないでしょうか。

DIはパラメーターとコンテキストをはっきり区別します。パラメーターは関数に渡され、コンテキストはコンストラクターを通してDIコンテナーから渡されます。依存関係が明確な形で、コンストラクターに示されています。DIはその根本的な部分では、とても単純です。おまけに、DIコンテナーを使ったコードは、「普通の」コードという利点があります。その言語を習得しているプログラマーであれば、誰であれ自然に読むことができます(余談ですが、まさにこの理由で私はJavaでフィールドインジェクションを使うことを良く思っていません)。

何をもってコンテキストとパラメーターを分けるのかは、実はかなり曖昧です。上で例に挙げたConfigにしても、コンテキストとみなすよりパラメーターとして扱ったほうがわかりやすい場面も当然あるかと思います(例えば、JSON形式のコンフィグ文字列をConfigインスタンスにパースするのは、関数であるほうが自然でしょう)。しかしそれでもなおこの議論が有効なのは、その関数において何がコンテキストで何がパラメーターなのかを、コードを読む人物に明確な形で示すことが、認知負荷(cognitive load)を減らすのに貢献するからです。

リーダーモナドは、コンテキストとパラメーターを分離するという明確なメリットがあります。DIコンテナーも同様です。そしてDIはモナドよりもはるかにとっかかりが容易です。


Scalaのimplicit

もちろんScalaにはScalazがあって、Scalazを使わないとJavaプログラマーとみなされて馬鹿にされる(!)わけです。興味深いことに、Scalaにはほかにもimplicitという機能があって、コンテキストの抽象化が可能になっています。先ほどコード例に使った動画で、オデルスキー教授がプレゼンしていたコードを引用します。

type Configured[T] = implicit Config => T

def readPerson: Configured[Option[Person]] =
attempt(
Some(Person(readName, readAge))
).onError(None)

implicitは、引数を手書きする手間を省きながら、コンテキストのパラメーター渡しを可能にします。ついでにコンパイラーとカリー化のおかげで、クロージャーを作るより効率的です。そしてDIと同じく、モナドとは違って「変な」コードを書く必要もありません。

implicitがうまく働くのかは、Scalaプログラマーではない私には判断しかねます。しかし、いくつか懸念点が挙げられます。


  • 容易に間違った使い方がなされる - 本来implicitにするべきでないものをimplicitにすると、簡単にカオスに陥る(もっともこれはDIでも同様なので、フェアではないかもしれません)

  • 大抵の場合、explicitなほうがimplicitであるよりいい(Zen of Pythonも同じことを言っていますよね)。

  • コンテキストを型で表現しようとする - これは私がHaskellのような言語に親しんでいないだけなのかもしれませんが、データ構造以外のものを型に表現しようとするのに違和感を覚えます。例えばここで例外をPossibly型として抽象化しているのが、副作用に基づくtry-catch構文よりわかりやすいものなのか、確信が持てません。将来的には依存型が流行るようになるのでしょうか。

とはいえ、ここで言いたかったのはコンテキストを扱うには様々な技法があるということです。モナドだけが唯一の正しいやり方ではないのです。


Java EE

Java EEにおいてDI機能を担っているのがJSR 365、CDIです。以前、仕事でJava EEを使った時の印象は最悪でした。最悪というのはつまり、EJBよりはましだという意味です。

ところが、最近ふと仕様書を斜め読みして、その印象が大きく変わりました。恥ずかしながら、私はネットで調べただけの知識だけで、詳しい内容を知らないままCDIを批判していたのです。ひょっとすると皆様には常識だったのかもしれませんが、忘れたふりをして紹介させてください。

一般的なCDIのコードは、このポストの最初で示したような見た目をしています。@Injectを付けたコンストラクターのパラメーターとして依存するモジュールを記述していきます。

class BillingService {

private final CreditCardProcessor processor;
private final TransactionLog transactionLog;

@Inject
BillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}

public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
...
}
}

(この例はGuiceからとってきたもので、正確にはJSR 365ではありませんが……。GuiceのWikiはDIを学ぶのに非常に有用なリソースだと思いますので、ぜひ見てみてください)

どの言語のDIフレームワークでも、だいたい同じような見た目になるのではないでしょうか。テストの時はCreditCardProcessorの実装を切り替えて、実際のカードが課金されなくても済むようにできます。

ところで、Java EEではHttpServletRequestクラスを、DIの対象にすることができます。私はこれがずっと不思議で仕方ありませんでした。IoCは抽象モジュールに依存することで、実装を切り離すための仕組みでのはずです。しかしリクエストは単なるデータです。それこそ関数型プログラミングのように、引数で渡すのが自然ではないでしょうか? 実際、生のサーブレットでは引数で受け取りますよね。例えば、シングルトンのモジュールにリクエストをインジェクトできるというのは、一見とても奇妙に感じませんか?

実装レベルでは、実際には注入されるインスタンスはプロキシを経由して取得されます。プロキシがコンテキストごとに適切なインスタンスを入れ替えるので、より広いスコープのモジュールにインジェクとすることが可能になっています。でも、わざわざそんなつくりにする必要があるのでしょうか?

つまりCDIは、単なるDIフレームワークというわけではないのです。単なるIoCコンテナーとして機能だけでなく、積極的にコンテキストを抽象化して扱うための機能を提供するという側面も持っているのです。

下記のコードは仕様書のサンプルから抜粋したものです。

@SessionScoped @Model

public class Login implements Serializable {
@Inject Credentials credentials;
@Inject @Users EntityManager userDatabase;

private CriteriaQuery<User> query;
private Parameter<String> usernameParam;
private Parameter<String> passwordParam;

private User user;

@Inject
void initQuery( @Users EntityManagerFactory emf ) {
CriteriaBuilder cb = emf.getCriteriaBuilder();
usernameParam = cb.parameter( String.class );
passwordParam = cb.parameter( String.class );
query = cb.createQuery( User.class );
Root<User> u = query.from( User.class );
query.select( u );
query.where(
cb.equal( u.get( User_.username ), usernameParam ),
cb.equal( u.get( User_.password ), passwordParam )
);
}

public void login() {
List<User> results = userDatabase.createQuery( query )
.setParameter( usernameParam, credentials.getUsername() )
.setParameter( passwordParam, credentials.getPassword() )
.getResultList();
if ( !results.isEmpty() ) {
user = results.get( 0 );
}
}

public void logout() {
user = null;
}

public boolean isLoggedIn() {
return user != null;
}

@Produces @LoggedIn User getCurrentUser() {
if ( user == null ) {
throw new NotLoggedInException();
} else {
return user;
}
}
}

Javaらしい冗長さに満ちていることは確かですし、気持ち悪い部分も多いことは認めます(initQueryとか)。ですが目を細めて、コンテキストの制御という観点から見たとき、このクラスは実に完璧に動作します。

@SessionScopedはインスタンスがHTTPセッションごとにインスタンス化されることを示します。

<f:view>

<h:form>
<h:panelGrid columns="2" rendered="#{!login.loggedIn}">
<h:outputLabel for="username">Username:</h:outputLabel>
<h:inputText id="username" value="#{credentials.username}"/>
<h:outputLabel for="password">Password:</h:outputLabel>
<h:inputText id="password" value="#{credentials.password}"/>
</h:panelGrid>
<h:commandButton value="Login" action="#{login.login}" rendered="#{!login.loggedIn}"/>
<h:commandButton value="Logout" action="#{login.logout}" rendered="#{login.loggedIn}"/>
</h:form>
</f:view>

素晴らしいのは、このクラスを使うクラスは、セッションの状況やデータベースとの接続を、全く気にする必要がないことです。ただLoginクラスを@Injectするだけで、裏で何が行われているのかは全く感知しなくても、コンテキストにアクセスできるのです。Loginクラスがセッションスコープであることすら意識する必要がありません。これは関数型プログラミングには真似できない芸当です(Loginクラス自体ステートフルなオブジェクトとして、ログインという処理のコンテキストを体現しているのです。)。このクラスは可変オブジェクトですが、そもそもログイン状態というのは本質的に変わるものです。その変化が直接、コードに対応する形でモデル化されています。

ここでは詳細に取り上げませんが、ほかにもライフサイクルフック、イベント、インターセプター等、強力な機能が提供されています。

これが、JavaのDIが単なるDIではなく、Context and Dependency Injectionという名前になっている理由なのです。

実際のところ、今でも私がJava EEでプログラムを書きたいかと言われるとかなり疑問です。冗長さという欠点は深刻ですし、全てのケースでここで挙げたようなコンテキスト制御がうまくいくとも思えません。可変な状態制御は一歩間違えるだけで悲劇になります。それでもなお、JSR 365の仕様を作った人が、コンテキストという問題に真剣に注意を払い、きわめて注意深く設計を行ったことは明らかです(こんなことを書くこと自体、とても失礼なことなのですが)。何事もよく知らずに批判するのはよくないなと反省した次第です。


まとめ

Dependency Rejectionの問題はコンテキストの問題を無視していることです。モナドはコンテキストを抽象化する方法としては難しすぎます。DIは完璧ではありませんが、比較的わかりやすい方法で、コンテキストを扱うことが可能です。Javaを使っているからといって、モナドを知らないからといって、劣等感を感じる必要はありません。

このポストを読んでJakarta EEを採用してプロジェクトが炎上しても、私は責任を持ちません。