一般的にプログラムにおいて循環依存は良くないことと言われています*1。
とはいえ、Javaは循環依存をしていてもコンパイルエラーになることはありません。
なので気が付かない間に循環依存をしてしまっているケースはあるように思います。
とりあえず、パッケージの循環依存については、チェックするやり方がありますので、それを活用するのが良いと思います。
(本記事はmaven
のjdepend-maven-plugin
の使用例でもあります。)
blog.guildworks.jp
ところで、私もチェックを実施したところ、ちょこちょこと循環依存をしてました。
これからも同じように気がつけば循環依存をしてしまっているケースが発生すると思います。反省も含めて、どういう循環依存の実装をしてしまって、それをどういう風に対処したのか、というのを記しておきたいと思います。
コマンドパターンのような実装をしたい
デザインパターンのコマンドパターンのようなことをしたいと思って実装したら循環依存エラーになりました。
やりたかったこと
HogeController
:共通の呼出窓口となるクラス。
FugaCommandHandler
:Commandを実行するクラス。
FugaCommandManager
:Commandを管理するクラス。
Piyo1Command
、Piyo2Command
:Commandクラス。
FugaCommandManager
で、Commandクラス(Piyo1Command
、Piyo2Command
)を登録して、
各CommandクラスはCommandInterface
を実装して統一的な操作が出来るようにします。
また各Commandは個別機能ということでパッケージにまとめておきます。
機能としては、HogeController
から呼び出したFugaCommandHandler
がFugaCommandManager
に登録した各Commandを実行します。
この例ではクラスが少ないので同一パッケージにしても良いかもしれないのですが、各Commandについては複数のクラスを使用しているので、個別のパッケージを設けることにしました。
循環参照あり
GitHub - vermeer-blog-circular-reference/sample1-error: 循環参照サンプル1(循環参照エラー)
パス構成
org
└─vermeer1977
└─sample
└─circular_reference
│ CommandInterface.java
│ EnvironmentItem.java
│ FugaCommandHandler.java
│ FugaCommandManager.java
│ HogeController.java
│ Main.java
│
└─command
├─piyo1
│ Piyo1Command.java
│
└─piyo2
Piyo2Command.java
ぱっと見た感じだと問題なさそうな構成に思っていましたが循環依存が発生しました。
循環依存になったと思われる理由
HogeController
にて取得した実行環境情報EnvironmentItem
を各Commandで使用するのですが、それが原因だと思われます。
対処例
FugaCommandManager
を独立したパッケージに分割しました。 こうすることで、HogeController
のパッケージとPiyo1Command
のパッケージが直接依存する関係が、仲介するパッケージを経由する状態になり循環依存のエラーがなくなりました。
発想としては、UMLの関連クラスを緩衝材として実装するイメージ、もしくはDTOを仲介させることで直接参照をしないようにするイメージです。
GitHub - vermeer-blog-circular-reference/sample1-ok: 循環参照サンプル1(循環参照エラーの解消)
パス構成
org
└─vermeer1977
└─sample
└─circular_reference
│ CommandInterface.java
│ EnvironmentItem.java
│ FugaCommandHandler.java
│ HogeController.java
│ Main.java
│
└─command
│ FugaCommandManager.java
│
├─piyo1
│ Piyo1Command.java
│
└─piyo2
Piyo2Command.java
副産物
分割したことで、今後、Commandクラスを増やした時に改修するクラスがパッケージレベルで整理されているので見通しも良くなった気がします。
呼出元のクラスを引数にしたい
やりたかったこと
呼出元の情報を元に呼出先で編集処理をしたい。
循環参照あり
GitHub - vermeer-blog-circular-reference/sample2-error: 循環参照サンプル2(循環参照エラー)
パス構成
org
└─vermeer1977
└─sample
└─circular_reference
│ Main.java
│
├─callee
│ Callee.java
│
└─caller
Caller.java
循環参照になった理由
説明の余地のないくらい、しっかりした循環依存です。
対処例
呼出先のクラスCallee
で実際に操作したい機能を満たすインターフェースを作成して、引数の型を変更しました。
修正前は呼出元クラスCaller
のインスタンスを呼出先クラスCallee
で参照したため循環依存になります。
修正後は呼出先クラスCallee
で必要となる振舞いを定義したインターフェースを呼出元クラスCaller
に実装することで循環依存が解消されます。
参照する向きを変更したイメージです。
(ちょっと今回の例だと、何のどの概念をインターフェースとしたのか分かりにくいのが難点ですが・・)
GitHub - vermeer-blog-circular-reference/sample2-ok: 循環参照サンプル2(循環参照エラーの解消)
パス構成
org
└─vermeer1977
└─sample
└─circular_reference
│ Main.java
│
├─callee
│ Callee.java
│ CalleeInterface.java
│
└─caller
Caller.java
実装の抜粋
callee
パッケージにCallee
クラスのメソッドの仮引数の型をCaller
クラスからCalleeInterface
に変更する
<修正前>
public Callee(Caller caller) {
this.caller = caller;
}
<修正後>
public Callee(CalleeInterface caller) {
this.caller = caller;
}
Caller
にCalleeInterface
をimplements
する
public class Caller implements CalleeInterface {
・・・
}
副産物
インターフェースを介することで、どのようなメソッド(振舞い)が呼出先として必要であるのか見通しが良くなりました。クラスのままだと、どのメソッドが使われているのか使用箇所の調査をしないと判別できません。
とはいえ、呼出元が呼出先の抽象概念の一部である、というのは少々宜しくないような気もします。基本的には素直に循環依存を解消するのであれば1つ目の例のように仲介するクラスを設けて、呼出元で中継するクラスを作成して呼出先に渡す、というのが良さそうに思います。可読性が落ちない程度で、仲介パッケージを作らなくても問題ないくらいの複雑度であれば、こういうやり方もありそうだ、という例にはなるように思います。
さいごに
正直なところ、始めは自分の作成した自分だけが想定される開発者なので循環参照の対応は無理しなくても良いかなと思っていました。ですが、実際、改修することで、見通しが良くなったように思います。
むかーしの話ですが、大規模SIer案件ではコードにインターフェースが使用されているものは、ほとんど*2ありませんでした。そういうこともあって「フレームワークなど抽象度の高い機能を実装しないのであればインターフェースを使うことはないだろうな」と思っていましたが、実際に自分で手を動かしてみると、そんなことは無いな、と思うようになりました。本記事もその一例のように思います。