パッケージ間の循環依存を自動で検出する

はじめまして。井上です。

私は仙台でフリーランスのエンジニアとして活動しています。
以前は仙台のSIerに勤務していたのですが、ギルドワークス設立と同時に退職し、それ以来ギルドワークスと一緒に仕事をしております。
そのような縁もあり、本ブログで記事を書くことになりました。どうぞよろしくお願いします。

さて、私は長らくJavaでシステム開発を行っていることもあり、以下のような分野に興味があり、仙台でも勉強会を主催したりしています。

  • オブジェクト指向設計
  • デザインパターン
  • TDD(テスト駆動開発)
  • DDD(ドメイン駆動設計)

本ブログでは、主に上記に関連するような記事を投稿していきたいと思います。

パッケージ間の循環依存を自動で検出する

さて、今回はパッケージ間の循環依存の自動検出について取り上げます。

パッケージとは、簡単に言うとクラス群をより大きな単位でまとめる為のメカニズムです。
Javaを使い始めた頃、パッケージ分割の基準が分からず、何となく同一機能に関連するクラスを同一パッケージにまとめたりしていたものの、正直それが良いパッケージ分割なのかずっとモヤモヤしていました。皆さんも同様に悩まれたことがあるのではないでしょうか?

そんなときに出会ったのが、2004年に刊行された「アジャイルソフトウェア開発の奥義」という本です。
(2008年には第2版となる「アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技」が出ています)

この本では、パッケージ内部の凝集度やパッケージ同士の結合度に関する原則や、安定度・抽象度といったパッケージに関するメトリクス情報について詳細に書かれており、私のパッケージ設計についての指針を与えてくれました。

書かれている内容はどれも興味深いものですが、初めて触れる方にはそれら全てを活用するのは難しいと思うので、最初は「非循環依存関係の原則(ADP : Acyclic Dependencies Principle)」について考慮することをお勧めします。先の本では以下のように定義されています。

20.3.1 非循環依存関係の原則 (ADP : Acyclic Dependencies Principle)

パッケージ依存グラフに循環を持ち込んではならない。

ここで簡単なパッケージ依存グラフを見てみましょう。
以下は、DDDにおける理想的なパッケージの依存関係を表しています。

DDDにおけるパッケージの依存関係(理想型)

DDDにおけるパッケージの依存関係(理想型)

この状態では、パッケージ間に循環依存が存在しません。そのため、例えば「インフラストラクチャ」パッケージに含まれるクラスを修正しても、その影響は他のパッケージには及びません。

ここで、もし「ドメイン」パッケージ内のクラスが「インフラストラクチャ」パッケージ内のクラスを参照していたらどうなるでしょうか?

DDDにおけるパッケージの依存関係(循環依存あり)

DDDにおけるパッケージの依存関係(循環依存あり)

この場合、「ドメイン」パッケージと「インフラストラクチャ」パッケージが相互参照しており、循環依存がある状態です。
この状態で「インフラストラクチャ」パッケージに含まれるクラスを修正すると、相互参照している「ドメイン」パッケージだけではなく、「ドメイン」パッケージに依存している「アプリケーション」パッケージと「ユーザインタフェース」パッケージにまで影響範囲が及んでしまいます。

このように、パッケージ間に循環依存がある状態というのは、望ましくない状態であると言えます。

では、パッケージの依存関係をどのようにチェックしたら良いでしょうか?
クラス数/パッケージ数が少ないうちは人力でもなんとかなるかもしれませんが、ある程度の規模になるとそれは非常に困難になります。
そこでツールを活用することにします。先に挙げた本で紹介されているパッケージのメトリクス情報を計測することが出来るツール類がいくつも公開されています。Javaの場合はJDpepend(*1)が代表的です。

(*1) http://clarkware.com/software/JDepend.html

JDpendで嬉しいのは、Java の標準的ビルドツールである Ant / Maven / Gradle を使ってレポートが作成出来ることです。またCI(継続的インテグレーション)ツールのデファクトスタンダードであるJenkinsには、JDependのプラグインが提供されているので、ビルド毎のメトリクスの統計情報を確認することが出来ます。

ここで、JDepend レポートの例を見てみましょう。以下は Apache Commons Lang の JDependレポートです。
パッケージ関する各種メトリクス情報が表示されていることが確認出来ると思います。

http://commons.apache.org/proper/commons-lang/jdepend-report.html

量が多くて最初はどこに着目すれば良いか分からないかも知れませんが、先ほど挙げた「非循環依存関係の原則」に関する情報は「Cycles」の部分です。

以下は、先のJDependレポートの「Cycles」を一部抜粋したものです。

Apache Commons Lang の循環依存情報

Apache Commons Lang の循環依存情報

ここには、パッケージの循環依存がある場合に、そのパッケージ名とそのパッケージが利用しているパッケージ名が一覧で表示されます。
「org.apache.commons.lang3」パッケージと「org.apache.commons.lang3.builder」パッケージが相互参照、つまり循環依存していることが分かります。

このようにJDependレポートを確認すれば循環依存があるか確認できることが分かりました。しかし、逆に言うと、CIでビルドが実行される度にレポートをチェックする必要があります。

それはさすがに面倒なので、そんな時は自動化ですね。嬉しいことに、JDependではパッケージ間の循環依存を検出する為のAPIが提供されています。

以下は、JUnit から該当APIを呼び出して循環依存が無いことを確認するためのテストコード例です。

package test;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.util.Collection;

import jdepend.framework.JDepend;
import jdepend.framework.JavaPackage;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;

public class CycleTest {

    @Rule
    public ErrorCollector ec = new ErrorCollector();

    private JDepend jdepend;

    @Before
    public void setUp() throws Exception {
        jdepend = new JDepend();
        // クラスファイルが出力されるディレクトリを指定
        jdepend.addDirectory("build/classes");
    }

    // (1) 
    @Test
    public void 全てのパッケージが循環依存を持たない() 
            throws Exception {
        jdepend.analyze();
        assertThat(jdepend.containsCycles(), is(false));
    }

    // (2)
    @Test
    public void domainパッケージが循環依存を持たない() 
            throws Exception {
        jdepend.analyze();
        final JavaPackage p = jdepend.getPackage("domain");
        assertThat(p.containsCycle(), is(false));
    }

    // (3)
    @Test
    public void 全てのパッケージが循環依存を持たない_パッケージ名出力() 
            throws Exception {
        @SuppressWarnings("unchecked")
        final Collection<JavaPackage> packages 
            = jdepend.analyze();
        for (final JavaPackage p : packages) {
            ec.checkThat(
                "循環依存しているパッケージ名 : " + p.getName(), 
                p.containsCycle(), is(false));
        }
    }
}

(1)は、全てのパッケージについて循環依存を持たないことを確認します。
(2)は、特定のパッケージ(ここではdomainパッケージ)が循環依存を持たないことを確認します。
(3)は、全てのパッケージについて循環依存を持たないことを確認しつつ、循環依存があった場合に、該当パッケージ名を出力しています。

上記のようなテストコードを自動テストに組み込んでCIを行えば、パッケージ間の循環依存が含まれるコードが含まれている場合に自動テストが失敗するので、パッケージ間の循環依存が検出出来ることになります。

既にCI環境が整っている場合、テストコードを追加するだけなので、是非お試し下さい。

そして、「アジャイルソフトウェア開発の奥義」やJDependのようなツールを活用して、より良いパッケージ分割のヒントにしては如何でしょうか?

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中