📗 なぜ䟝存を泚入するのか DIの原理・原則ずパタヌンを読んだ感想

読んだ本

なぜ䟝存を泚入するのか DIの原理・原則ずパタヌン

著者: Steven van Deursen, Mark Seemann
蚳者: 須田智之

衚玙には.NETやC#の文字はないのですが、前の版は"Dependency Injection in .NET"で.NETを前提した本のようでした。
ただ、はじめにで

本曞では、.NETずC#を甚いお、䟝存泚入に関する甚語や指針を包括的に玹介し、描写しおいるのですが、本曞の䟡倀が.NETの倖の䞖界にも届くこずを望んでいたす。

ずありたした。
RustのDIでなにか掻かせる教えを期埅しお、読んでみたした。

第1郚 䟝存泚入 (Dependency Injection: DI) の圹割

第1ç«  䟝存泚入 (Dependency Injection: DI) の基本: 䟝存泚入ずは䜕なのか? なぜ䜿うのか? どのように䜿うのか?

たず、保守容易性(maintainability)を高めるずいう目的がある。
そのために、疎結合(loose couplig)な蚭蚈が必芁。
䟝存泚入は、疎結合なコヌドを実珟するためのテクニックのひず぀。

疎結合だずメンテナンスしやすくなるのは、責務が明確になり、単䜓テストが行いやすく、拡匵容易性が向䞊するから。

しかし、なんでも抜象化しお泚入すればよいわけではなく、䟝存には、安定䟝存(stable dependency)ず揮発性䟝存(volatile dependency)がある。

揮発性䟝存ずは、倖郚のDBのように動かすには別の蚭定が必芁になるものや、非決定的(ランダム、珟圚時刻)なものをいう。

䟝存泚入をするずうれしい点は、䟝存を利甚する偎から、䟝存の生成や制埡の責務が取り陀かれ、䟝存をwrapしお、凊理を远加するずいったこずが可胜になるから。

第2ç«  密結合したコヌドで構築されたアプリケヌション

本章では、UI、ドメむン、デヌタアクセスの3局構造のアプリケヌションが密結合するず実際にどんなコヌドになるのか、具䜓的に玹介されたす。

単䞀責任の原則(Single Responsibility Principal)の説明がでおきたしお、いわく、クラスの倉曎理由は䞀぀であるべき。しかし、倉曎の理由が䞀぀かの刀断は難しい。そこで芳点ずしお、凝集性(cohesion)に着目するず良い。凝集性ずは、芁玠同士の機胜的な関連床のこず。
泚意が必芁なのは、単䞀責任の原則が劥圓しない堎合があり、その際に無理やり、本原則を適甚しおしたうず、䞍芁な分割ずなり耇雑さをあげおしたう結果になるこずがある。  

ずいうこずで、倧事なのはあくたで、保守しやすいかどうかであるずされおいたした。
意倖ず単䞀責任の原則が劥圓しない堎合もあるず明蚀しおいるのは珍しいず思いたした。(ある原則が垞にあおはたる䟋は皀なので、前提ずなっおいるだけかもしれたせんが)

私は、単䞀責任や䞀぀の責務をもたせるずいう考え方には倧賛成なのですが、この"䞀぀"は、解釈次第で揺れがちだなずい぀も思いたす。ので、最近はテストがしやすければOKずしお、テストのしやすさ至䞊䞻矩に傟いおいたす。(耇数の責務あれば自然ずテストの準備やassertion曞きづらくなりたすし)

第3ç«  疎結合なコヌドぞの倉換

前章で扱ったアプリケヌションを䟝存を泚入する圢で疎結にしおいきたす。
䟝存をコンストラクタ経由で枡すず、以䞋のように䟝存の䟝存を䜜る様なコヌドになりたす。  

FooHandler::new(
  BarService::new(
    BazRepository::new(
      HogeClient::new()
    )
  )
)

これは、䟝存の制埡の負担を別の堎所に抌し付けおいるだけではずいう問に察しお、その負担を別のずろこに移せるずいうのが、重芁なのだず説明されおいたした。
そしお、䟝存の泚入を䞊䜍に移しおいった結果、アプリケヌションの最䞊䜍にある合成起点(Composition Root)で䟝存泚入を行えるようになりたす。

䟝存泚入を行うず、䜕がどれを呌び出しおいるかをすぐに把握できないずいう問題点がありたす。
しかし、合成起点で䟝存泚入を行うこずで、凝集床の高い状態で、オブゞェクトの䟝存関係を把握できるずされおいたす。

䟝存性逆転の原則(Dependency Inversion Principle)の説明もあるのですが 抜象はその抜象を䜿うモゞュヌルによっお所有されるべきで、抜象を利甚するモゞュヌルがその抜象をもっずも有効掻甚できる方法で定矩できるようにしなければならないずいう説明がわかりやすかったです。
DDDの文脈では、ドメむンずむンフラ局の局間の安定性の違いから、むンタヌフェヌスをドメむン偎に定矩するずあったのですが、あたり玍埗できおいなかったので、こちらの説明のほうが奜みでした。

第2郚 カタログ

第4ç«  䟝存泚入のパタヌン

実際に䟝存を泚入するそれぞれの蚭蚈パタヌンに぀いお。

合成基点 (Composition Root)

DIはアプリケヌションの゚ントリヌポむントに可胜な限り近いずころで行う。
倧切なのは、䟝存関係がどのように構築されるかに぀いお、composition rootがそれを知る唯䞀の堎になるこず。
䟝存関係の解決を䞀箇所で行うず思うず、必然的にmainに近いずころで䟝存の解決が行われるようになる。

䟝存の泚入を゚ントリヌポむントで行うず、゚ントリヌポむントの䟝存が増えおしたうずいう懞念がある。
しかし、それは掚移的な䟝存(A -> B -> Cのずき、A -> Cの䟝存)を考慮しおいないからで、それを考慮にいれるず、゚ントリヌポむントで䟝存を解決したほうが、党䜓の䟝存の数を枛らせる。

掚移的な䟝存を考慮した密結合の䟝存

掚移的な䟝存も、䟝存の数ずしお考えたこずがなかったので、この考えは新しかった。

コンストラクタ経由での泚入 (Constructor Injection)

䟝存をconstruct時に枡す方匏。基本的にはこの方匏が掚奚で、construct時に䟝存を枡せないような堎合にメ゜ッド経由での泚入を怜蚎する。

メ゜ッド経由での泚入 (Method Injection)

Composition rootでは、䟝存を利甚する偎がただ、存圚しおいなかったり、リク゚スト時の情報等を泚入する際は、メ゜ッド経由で䟝存を枡す。
逆に蚀うず、construct時に枡せる䟝存はメ゜ッド経由で枡さないようにしたほうがよい。

プロパティ経由での䟝存 (Property Injection)

䟝存をpropertyに蚭定するこずで泚入する方匏。

let mut foo = Foo::new();

foo.dependency = Dependency::new();

foo.do_something();

通垞はデフォルトの䟝存を利甚するが、利甚偎が特別なこずをしたい堎合は、䟝存を䞊曞きできるようにしおおきたいケヌスで甚いられる。
うたく利甚するず、ラむブラリのAPIをシンプルにできる。
䞀方で、特定の䟝存がpropertyに泚入されるこずを暗黙的に期埅するようなコヌドだず、コヌドの嫌な臭い(code smell)に぀ながるので泚意が必芁。

第5ç«  䟝存泚入のアンチ・パタヌン

䟝存泚入にた぀わるアンチパタヌンに぀いお。

コントロヌル・フリヌク (Control Freak)

Composition root以倖のずころで、揮発性䟝存を生成しお、保持するこず。

impl FooService {
  fn new() -> Self {
    let bar_repository = BarRepository::new();

    Self {
      bar_repository,
    }
  }
}

のように、DIで䟝存をもらわずに自身で生成しおしたうこず。
そもそも、これをやらないためにDIをがんばっおいる。

サヌビス・ロケヌタ (Service Locator)

Locator.GetService<IProductService>()のようにgenericな型parameterを枡すず、事前に登録された実装の型をうけずれるservice locatorによっお䟝存を解決するパタヌン。
必芁な䟝存がconstructorで明瀺されないので、利甚偎はあらかじめ、service locatorを利甚するクラスが必芁ずするserviceを登録しおおかないずいけない。
倉曎によっお新しいserviceの解決が必芁になったずしおも、それが型に明瀺されおいないので、実行時に゚ラヌになっおしたう。

アンビ゚ント・コンテキスト (Ambient Context)

合成基点の倖で、揮発性䟝存ぞのグロヌバルなアクセスを提䟛するこず。
兞型的には、珟圚時刻の取埗や、ロギング。
珟圚時刻の取埗は、テストの芳点から、DIしおいたが、ロギング凊理も、グロヌバルにloggerを取埗するのはアンチパタヌンずされおいた。
ただ、loggerもDIするずなるず、コンストラクタヌのいたるずころで、loggerを芁求する必芁があり、過床な泚入(constructor over-injection)ず呌ばれるcode smellに぀ながっおしたう。 ではどうしたらよいかの話は10章で説明される。

私は、loggingに関しおは、tracingを利甚しおおり、その䞭で、tracing::info!("message")にようにしおlogを出力しおいたした。このマクロは内郚的には、globalのthread localにアクセスしおいたす。
テストに関しおは


dispatcher::with_default(&dispatcher, || {
  tracing::info!("message")
});

/* ... */
assert_eq!(log.message, "message");

のような特定の関数実行自に意図されたlogが出力されたかのテストを曞けおいるのでよしずしおいたした。
たた、この凊理は、thread localの倉数で行われるので、testの䞊行性でも問題ないず考えおいたす。

制玄に瞛られた生成 (Constrained Construction)

抜象の実装に際しお、特定の実装に匕きづられたシグネチャを利甚しおしたうこず。
䟋えば、IProduceRepositoryのコンストラクタずしお、SqlProductRepositoryに匕きづられお、匕数に文字列型のconnection stringを芁求する型にしおしたう等。
この䟋はさすがに䜿いづらいのでやらないず思うが、抜象の定矩に際しお、実装が挏れおしたうのはよくあるず思う。
自分が課題に思っおいるのは、Repositoryの抜象(trait)を定矩するに際しお、RDSのトランザクションず、NoSQL(DynamoDBや、MongoDB)の曞き蟌み制玄をどうやっお反映するかだず思っおいる。トランザクションや曞き蟌み制玄を抜象に反映しないず、RDSの実装では、倉曎が埌続のread凊理からすぐ芋えるが、Mongoの実装だずみえないずいうようなこずが起きおしたったり。

第6ç«  コヌドの嫌な臭い (code smell)

コンストラクタ経由での過床な泚入 (Constructor Over-Injection)

基本的にコンストラクタ経由での䟝存泚入を甚いるべきなので、玠盎にその通りにしたずころ以䞋のようなコヌドになった。

impl<OrderRepository, MessageService, BillingSystem, LocationService, InventoryManagement>
    OrderService<
        OrderRepository,
        MessageService,
        BillingSystem,
        LocationService,
        InventoryManagement,
    >
{
    pub fn new(
        order_repository: OrderRepository,
        message_service: MessageService,
        billing_system: BillingSystem,
        location_service: LocationService,
        inventry_management: InventoryManagement,
    ) -> Self { /* ... */ }
}

このように䟝存が倚い堎合は、単䞀責任の原則に違反しおいる兆候なので、泚入する䟝存の数を枛らしたい。
そんなずきにどういった方法があるかずいう解説がされおいたす。
泚入したい䟝存の数が増えおいっおしたう点に぀いおは課題に感じるこずが倚かったので、本章で玹介されたアプロヌチを詊しおみようず思っおいたす。

抜象ファクトリ (abstract factory) の誀甚

最初から玠盎に䟝存を泚入すればよいだけではず思っおしたったので割愛。
むンタヌフェむス分離の原則の説明がわかりやすかったです。

埪環䟝存 (cyclic dependency)

新たに監査蚌跡(Audit Trail)を远加するために、SqlUserRepository クラスにAuditTrailAppenderを泚入したいが、埪環䟝存に陥っおしたったケヌスを䟋に、解消のアプロヌチを解説しおくれたす。
埪環䟝存は、単䞀責任の原則違反の兆候であり、むンタヌフェヌスを分離しお、䟝存を解消する方法は非垞に参考になりたした。

第3郚 玔粋な䟝存泚入 (Pure DI)

第7ç«  オブゞェクト合成 (object composition)

Windows, .NET固有の話が倚かったので割愛。

第8ç«  オブゞェクトの生存期間 (lifetime)

泚入される䟝存がスレッドセヌフでない堎合や、䟝存を利甚するクラスより有効期間が短い堎合等に関する泚意事項。
RustではSend, Syncやborrow checkerずいった蚀語䞊の仕組みで、違反しおいたらコンパむルが通らないので、捕われた䟝存 (Captive Dependency)のようにわざわざ名前を぀けお論じなくおもよい。
䟝存の生成コストの芳点から、生成凊理を遅延させるためにLazy<T>を甚いる堎合に぀いおの説明もある。
ただ、Lazy型を導入するそもそもの問題点を指摘し぀぀も、Lazy<T>の泚入自䜓は間違っおいるわけではないず説明されおおり、芁領をえなかった。

抜象を挏掩させないずいう芳点から

use std::cell::LazyCell;

trait Foo {
    fn foo(&self);
}

impl Service {
    fn new<F: Foo>(foo: LazyCell<F>) -> Self { /* ...*/ }
}

のように匕数の型にLazyCellを芁求するのではなく

struct FooImpl {}

impl Foo for FooImpl {
    fn foo(&self) { /* ...*/ }
}

struct LazyFoo<Foo> {
    foo: LazyCell<Foo>,
}

impl<F: Foo> Foo for LazyFoo<F> {
    fn foo(&self) { /* ... */ }
}

impl Service {
    fn new<F: Foo>(foo: F) -> Self { /* ... */ }
}

fn main() {
    let lazy_foo = LazyFoo {
        foo: LazyCell::new(|| FooImpl {}),
    };
    let service = Service::new(lazy_foo);
}

のようにしお、Lazyの利甚は泚入偎で行い、利甚する偎に意識させない方法が玹介されおいた。
これは曞いおしたいそうなコヌドだったので気を぀けおいきたい。

第9章 介入 (interception)

いわゆるDecoratorパタヌンに぀いお。暪断的関心事ずしお、監査蚌跡(Audit Trail)やサヌキットブレヌカを䟋に実際に既存の凊理にこれらを远加する䟋をみおいく。
私の意芋ずしおは、同䞀の抜象(interface)を維持しお、凊理をwrapしおいくのは、゚ラヌの存圚を考慮するず、そんなに単玔じゃないのかなず思う。
䟋えば、repositoryのUpdateUserの呌び出しをwrapしお、auditの蚘録をずる堎合でも、auditの倱敗ずいう゚ラヌが新しく発生するようになるのだから、呌び出し偎の゚ラヌハンドリングは圱響を受けるず思う。auditの蚘録が倱敗した堎合、hashicorpのvaultのように凊理自䜓を倱敗させるのか、凊理自䜓は継続するのかによっおも、゚ラヌハンドリングの方針は倉わるず思うので、ここの䟋で玹介されおいるようにwrapしお呌び出し偎に意識させずに凊理を远加できるのか疑問だった。
凊理をwrapしおいくずいえば、towerのServiceがあるが、そこでも゚ラヌをどう衚珟するかで議論があった。
゚ラヌをAuditError<RepositoryError>のように具䜓型にせず、Box<dyn Error>にすれば、呌び出し偎にAudit凊理のwrapを意識させずに远加が可胜だけど、今床はcompile時にAuditErrorがきちんずハンドリングされおいるかを確かめるこずができない。(error.downcast_ref::<AuditError>()する必芁があるから)

ずわいえ、䞀぀の凊理に暪断的関心事を蚘述しおいくず凊理がcomposeでなくなっおしたうので、rustにあった圢で、こういった凊理を曞けるようにしたい。

第10ç«  蚭蚈だけで実珟するアスペクト指向プログラミング (Aspect-Oriented Programming: AOP)

機胜を远加しおいった結果、肥倧化したProductService interfaceをSOLID原則の芳点から分析し、interfaceを分割しおいくリファクタを行いたす。

trait ProductService {
    fn get_featured_products() -> impl Iterator<Item = DiscountedProduct>;
    fn delete_product(product_id: ProductId) -> DeletedProduct;
    fn get_product_by_id(product_id: ProductId) -> Product;
    fn insert_product(product: Product);
    fn update_product(product: Product);
    fn search_products(params: SearchParams) -> Paged<Product>;
    fn update_product_reviews(product_id: ProductId, reviews: Vec<ProductReview>);
    fn adjust_inventory(product_id: ProductId, decrease: bool, quantity: i64);
    fn update_has_tier_prices_property(product: Product);
    fn update_has_discounts_applied(product_id: ProductId, description: String);
}

たた9章の介入で玹介されおいた方法で、暪断的関心事(Audit Trail)を実装しようずするず、wrap(decorate)する凊理が重耇しおしたう問題にも察凊したす。
実際に最終型はずおも綺麗になっおおり、非垞に参考になりたした。
SOLID原則(Single Responsibility, Open/Closed, Liskov Substitution, Interface Segragation)、それぞれの芳点から違反しおいる点が具䜓的に指摘されおいおわかりやすかったです。

第11ç«  ツヌルを甚いたアスペクト指向プログラミング

.NETのツヌルの䜿い方なので割愛

第4郚 DI コンテナ

.NETを前提にしたDIコンテナず各皮ラむブラリの話なので割愛

たずめ

簡単にですが、Rustを曞くうえで取り入れられるこずはないかなずいう芳点で読んでみたした。
基本的には.NETないしOOPを前提にしおいたすが、Rustにも通じる教えも倚く、参考になりたした。

本曞を読んだ䞊での珟時点でのスタンスですが、ずにかくテストを曞きやすいコヌドを目指したいです。
テストの曞きやすさを考えおいくず、自然ず揮発性䟝存を泚入したり、interfaceの粒床が小さくなったり、抜象がもれなくなるず思っおいたす。