2014年6月2日月曜日

Google GuiceのBest Practicesを訳してみた - 循環する依存関係を解決する

循環する依存関係を解決する
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 Provider shopProvider;
        @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 件のコメント:

コメントを投稿