Store、Boss、Clerkを含むいくつかのクラスがあるアプリケーションについて考える。
public class Store { private final Boss boss; //... @Inject public Store(Boss boss) { this.boss = boss; //... } public void incomingCustomer(Customer customer) {...} public Customer getNextCustomer() {...} } public class Boss { private final Clerk Clerk; @Inject public Boss(Clerk Clerk) { this.Clerk = Clerk; } } public class Clerk { // Nothing interesting here }
今のところ、依存関係は全く問題ない。Bossを使用してStoreをインスタンス化し、Clerkを使用してBossをインスタンス化する。ところが、販売のためにClerkにCustomerを取得させようとすると、Storeへの参照が必要になる。
public class Store { private final Boss boss; //... @Inject public Store(Boss boss) { this.boss = boss; //... } public void incomingCustomer(Customer customer) {...} public Customer getNextCustomer() {...} } public class Boss { private final Clerk clerk; @Inject public Boss(Clerk clerk) { this.clerk = clerk; } } public class Clerk { private final Store shop; @Inject Clerk(Store shop) { this.shop = shop; } void doSale() { Customer sucker = shop.getNextCustomer(); //... } }
これはClerk -> Store -> Boss -> Clerkという循環をもたらす。Clerkをインスタンス化しようとするとStoreがインスタンス化され、StoreはBossのインスタンスが必要であり、BossはClerkのインスタンスが必要なのだ。
この循環を解決するための方法がいくつかある。
循環を断ち切る(推奨)
しばしば循環は不十分な責務の分割を反映している。そのような循環を断ち切るには、分割したクラスへ依存を抽出する。
この例では、来店する顧客を管理する機能をCustomerLineというクラスに抽出でき、ClerkとStoreへ注入できる。
public class Store { private final Boss boss; private final CustomerLine line; //... @Inject public Store(Boss boss, CustomerLine line) { this.boss = boss; this.line = line; //... } public void incomingCustomer(Customer customer) { line.add(customer); } } public class Clerk { private final CustomerLine line; @Inject Clerk(CustomerLine line) { this.line = line; } void doSale() { Customer sucker = line.getNextCustomer(); //... } }
StoreとClerkはどちらもCustomerLineへ依存しているが、循環は存在しない(StoreとClerkの両方が同じインスタンスを参照するときも)。このことはまた、テントで特売セールをするとき、店員が自動車を販売できることも意味する。ただ単に他のCustomerLineを注入すれば良い。
Providerを使用して循環を解決する
Providerを注入することで、依存関係へ継ぎ目を追加できる。ClerkはまだStoreに依存しているが、必要になるまでShopを参照することはない。
public class Clerk { private final ProvidershopProvider; @Inject Clerk(Provider shopProvider) { this.shopProvider = shopProvider; } void doSale() { Customer sucker = shopProvider.get().getNextCustomer(); //... } }
StoreがSingletonや他の再利用するスコープを指定しない時、shopProvider.get()は新たなShopをインスタンス化し、Bossをインスタンス化し、Clerkをインスタンス化することに注意が必要だ。
ファクトリメソッドを使用して2つのオブジェクトを結びつける
依存するクラス同士がより密接に結びついているときは、上記のメソッドでは解決できない。View/Presenterパターンを使用していると、このような状況に出くわす。
public class FooPresenter { @Inject public FooPresenter(FooView view) { //... } public void doSomething() { view.doSomethingCool(); } } public class FooView { @Inject public FooView(FooPresenter presenter) { //... } public void userDidSomething() { presenter.theyDidSomething(); } //... }
どちらのオブジェクトも相手のオブジェクトを必要としている。このような状況とうまくやるために、AssistedInjectが使える。
public class FooPresenter { privat final FooView view; @Inject public FooPresenter(FooView.Factory viewMaker) { view = viewMaker.create(this); } public void doSomething() { //... view.doSomethingCool(); } } public class FooView { @Inject public FooView(@Assisted FooPresenter presenter) {...} public void userDidSomething() { presenter.theyDidSomething(); } public static interface Factory { FooView create(FooPresenter presenter) } }
そのような状況は、Guiceを使用してビジネスモデルを表現しようとするときに出くわす。そのモデルには、異なる関係を反映した循環があるかもしれない。そのような状況にも、AssistedInjectは効果的だ。
参照
Resolving Cyclic Dependencies
関連
Google GuiceのBest Practicesを訳してみた - 可変性を最小化せよ
Google GuiceのBest Practicesを訳してみた - 直接の依存性のみ注入せよ
0 件のコメント:
コメントを投稿