依存性循環とは
漫然とプログランミングをやっていると、ついクラス間の参照(依存性)に循環が発生してしまいます。オブジェクト指向プログラミングの原則では依存性の向きは一方向であるべきであり、循環は悪しき存在とされています。循環があるとクラス間の結合が密になり、改造に対して脆弱になります。例えば ClassA が ClassB を参照し、ClassB も ClassA を参照するとクラス間の依存性に循環が発生します。ここで両クラスが別のパッケージに属しているとパッケージ間の依存性循環にもなります。
このように二つのクラス間で相互参照が発生している場合はまだ分かり易いのですが、これが A → B → C → A の様に三つ、またはそれ以上のクラスを介して循環していると直感ではなかなか判別できません。
Android Studio にはこの依存性の循環を検出する仕組みが備わっています。この記事は IntelliJ IDEA にもそのまま適用できる内容ですが、自分のフィールドが Android Studio なので、Android Studio をベースに説明していきます。
public class ClassA {
private static final String TAG = ClassA.class.getSimpleName();
ClassA() {
Log.d(TAG, "Reference to " + ClassB.class.getSimpleName());
}
}
public class ClassB {
private static final String TAG = ClassB.class.getSimpleName();
ClassB() {
Log.d(TAG, "Reference to " + ClassC.class.getSimpleName());
}
}
public class ClassC {
private static final String TAG = ClassC.class.getSimpleName();
ClassC() {
Log.d(TAG, "Reference to " + ClassA.class.getSimpleName());
}
}
ほんとんど中身の無いクラスです。コンストラクタの中で無理矢理他のクラスを参照して、 A → B → C → A の依存性循環を作り出しています。
これらのクラスを以下の様に別々のパッケージに配置します。
Inspection で検出する
この状態で Inspection を実施すると、Dependency Issues の下に、以下の様に依存性の循環が検出されます。これを見ると分かるように、クラス間の循環(Cyclic class dependencies)が三つ、パッケージ間の循環(Cyclic package dependencies)が三カウントされています。
検出された項目を選択すると、どのクラス、パッケージに対して循環が発生しているかが分かります。しかしここからはコードのどの部分なのかというところまでは分かりません。
Analyze ツールで検出する
Inspection で調べる方法とは別にもう一つ別の方法があります。メニューの Analyze → Analyze Cyclic Dependencies... から依存性循環検出ツールを使う方法です。この方法はパッケージ間の循環を検出します。なのでクラス間に循環はあるが、パッケージ間にはないような場合には検出できません。しかし結果を見るとどのクラスで循環が発生しているかまで知ることができます。実行すると以下のような結果を得ます。
三つのペインに分かれています。上左ペインには循環が発生しているパッケージが全て表示されます。ここで一つのパッケージを選択すると、上右ペインにその依存先のパッケージやクラスが表示されます。少し分かりにくいのは、左に表示されるのは直接の依存先だけでなく、間接的な依存先も全て表示されていることです。更に自分自身も表示されています。右ペインでパッケージやクラスを選択すると、下ペインに循環に関わるコードが表示されます。
また左ペインで三つのパッケージが表示されていますが、この例の場合、これらどれも同じ循環に関わるものです。この検出結果は慣れるまで、なかなか意味が分かりにくいかもしれません。もう少し工夫して、依存性グラフが視覚的に分かるような表示になるといいのですが。
パッケージ間の依存性循環を解決する
パッケージ間の循環ついては背景にクラス間の循環があるわけですが、まずはパッケージ間のみで考えます。パッケージ循環はクラスをパッケージ移動するだけで消えてしまうことがあります。極端な話、全てのクラスを同じパッケージに収めてしまえば、パッケージ間の循環は無くなります。これ結構見落しがちな点かもしれません。そこまで極端でなくても、パッケージデザインが適切でなく、パッケージ分割を見直すことで解決できることがあります。教科書やウェブページには詳細なプログラミングテクニックを教えてくれる膨大な情報がありますが、ことパッケージデザインに関しては、指針を与えてくれる情報はほとんどありません。パッケージデザインというのは Java プログラミングにおいて結構重要な要素であるにも係わらず、意外と軽視されている気がします。この点に昔から不満を抱えていました。自分なりに工夫してきましたが、まだこれであれば鉄板という域に達していません。どうすれば良いか模索している段階です。
クラス間の依存性循環を解決する
クラス間の依存性循環を解決するのは結構厄介です。まずオブジェクト指向プログラミングには DIP (Dependency Inversion Principle, 依存性逆転の法則) というテクニックあります。これは依存性の矢の向きを反転させてしまう魔法のようなテクニックです。一種のデザインパターンと言ってもいいかもしれません。ここでは詳細は書きません。各自調べてください。この DIP を依存性のどこかに適用すると循環を断ち切ることができます。しかしこの方法の欠点は、あまり多用し過ぎるとコードの見通しが悪くなってしまうことです。
もう一つの方法はクラスのデザインを見直すことです。循環が発生しているということは何かしらデザインに問題があるとも言えます。循環を解決するには結構大胆にクラスを作り変える必要があるかもしれません。しかしそうすることで、クラスの構造が見違えるようにスッキリするのはよくあることです。
まとめ
パッケージにしろクラスにしろ依存性循環を解決するのは結構大変な作業です。そのせいか世の中には循環に対してあまり注意を払わないコードが数多く見られます。Android Studio/IntelliJ IDEA には、あまり洗練されているとは言えませんが、循環を検出する機能が標準で搭載されています。これらを駆使して依存性循環を駆逐しましょう。
0 件のコメント :
コメントを投稿