システム開発で思うところ

Java EEを主にシステム開発をしながら思うところをツラツラを綴る

パッケージの循環依存の対処

一般的にプログラムにおいて循環依存は良くないことと言われています*1

とはいえ、Javaは循環依存をしていてもコンパイルエラーになることはありません。
なので気が付かない間に循環依存をしてしまっているケースはあるように思います。

とりあえず、パッケージの循環依存については、チェックするやり方がありますので、それを活用するのが良いと思います。 (本記事はmavenjdepend-maven-pluginの使用例でもあります。)

blog.guildworks.jp

ところで、私もチェックを実施したところ、ちょこちょこと循環依存をしてました。

これからも同じように気がつけば循環依存をしてしまっているケースが発生すると思います。反省も含めて、どういう循環依存の実装をしてしまって、それをどういう風に対処したのか、というのを記しておきたいと思います。

コマンドパターンのような実装をしたい

デザインパターンのコマンドパターンのようなことをしたいと思って実装したら循環依存エラーになりました。

やりたかったこと

  • HogeController:共通の呼出窓口となるクラス。
  • FugaCommandHandler:Commandを実行するクラス。
  • FugaCommandManager:Commandを管理するクラス。
  • Piyo1CommandPiyo2Command:Commandクラス。

FugaCommandManagerで、Commandクラス(Piyo1CommandPiyo2Command)を登録して、 各CommandクラスはCommandInterfaceを実装して統一的な操作が出来るようにします。
また各Commandは個別機能ということでパッケージにまとめておきます。

機能としては、HogeControllerから呼び出したFugaCommandHandlerFugaCommandManagerに登録した各Commandを実行します。

この例ではクラスが少ないので同一パッケージにしても良いかもしれないのですが、各Commandについては複数のクラスを使用しているので、個別のパッケージを設けることにしました。

循環参照あり

GitHub

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

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

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

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パッケージにCalleeクラスのメソッドの仮引数の型をCallerクラスからCalleeInterfaceに変更する

<修正前>

    public Callee(Caller caller) {
        this.caller = caller;
    }

<修正後>

    public Callee(CalleeInterface caller) {
        this.caller = caller;
    }
  • Caller

CallerCalleeInterfaceimplementsする

public class Caller implements CalleeInterface {

・・・

}

副産物

インターフェースを介することで、どのようなメソッド(振舞い)が呼出先として必要であるのか見通しが良くなりました。クラスのままだと、どのメソッドが使われているのか使用箇所の調査をしないと判別できません。

とはいえ、呼出元が呼出先の抽象概念の一部である、というのは少々宜しくないような気もします。基本的には素直に循環依存を解消するのであれば1つ目の例のように仲介するクラスを設けて、呼出元で中継するクラスを作成して呼出先に渡す、というのが良さそうに思います。可読性が落ちない程度で、仲介パッケージを作らなくても問題ないくらいの複雑度であれば、こういうやり方もありそうだ、という例にはなるように思います。

さいごに

正直なところ、始めは自分の作成した自分だけが想定される開発者なので循環参照の対応は無理しなくても良いかなと思っていました。ですが、実際、改修することで、見通しが良くなったように思います。

むかーしの話ですが、大規模SIer案件ではコードにインターフェースが使用されているものは、ほとんど*2ありませんでした。そういうこともあって「フレームワークなど抽象度の高い機能を実装しないのであればインターフェースを使うことはないだろうな」と思っていましたが、実際に自分で手を動かしてみると、そんなことは無いな、と思うようになりました。本記事もその一例のように思います。

*1:言語によってはコンパイルエラーになったりするみたいです。

*2:というか全く