進化するアーキテクチャーと新方式の設計

設計のためのリファクタリング

コードの中に隠れている設計を発見し、利用する

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 進化するアーキテクチャーと新方式の設計

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:進化するアーキテクチャーと新方式の設計

このシリーズの続きに乞うご期待。

私は「Test-driven design, Part 1」と「Test-driven design, Part 2」の中で、テストを行うことによって新しいプロジェクトの設計をより適切に行えることを説明しました。「Composed Method と SLAP」では Composed Method と SLAP (Single Level of Abstraction Principle) という 2 つの重要なパターンを紹介し、この 2 つのパターンによって全体としてどのようなコードの構造を目指せばよいかを示しました。この 2 つのパターンについては頭に入れておいてください。既存のソフトウェア・プロジェクトを扱う場合、その中に隠れている設計要素を発見し、利用するためには、リファクタリングが必要です。Martin Fowler は彼の有名な著作である『リファクタリング ― プログラムの体質改善テクニック』の中で、リファクタリングのことを、「既存のコード本体を再構成し、外部から見た動作を変更せずに内部の構造を変更するための、規律ある手法」と定義しています (「参考文献」を参照)。リファクタリングとは、ある目標を念頭に置いて構造を変換することです。どのようなプロジェクトにとってもその理想となる目標は、コード・ベースが容易にリファクタリングできるものであるということです。この記事では、コードの中に隠れている活用されていない設計を見つけるために、どのようにリファクタリングを使用すればよいかについて説明します。

ユニット・テストは基本的なセーフティー・ネットであり、ユニット・テストを行うことでコード・ベースを自在にリファクタリングできるようになります。プロジェクトのコード・カバレッジが 100 パーセントであれば、何も心配せずにコードをリファクタリングすることができます。それほどのレベルのテストを行っていない場合には、過度のリファクタリングは危険です。ローカルな変更は適用が容易であり、その効果を即座に確認できますが、変更箇所から遠く離れたところに生ずる副次的な問題によって開発者は苦しめられる羽目になります。ソフトウェアには想定外の結合ポイントが生じがちであり、コードの一部分に対するちょっとした変更がコード・ベース全体に波及し、変更箇所から何百行も離れたところにエラーを引き起こす場合があります。しかし、広範にわたってユニット・テストを行うようにすることで、自信を持ってコードを変更し、変更箇所から遠く離れたところに生じるエラーも見つけられるようになります。ある、2 年間にわたる ThoughtWorks 社のプロジェクトでは、53 回もの、さまざまなリファクタリングが技術リーダーによって行われ、それからようやくプロジェクトが公開されました。そのプロジェクトのコード・カバレッジは完全であったため、彼は確かな自信を持ってリファクタリングを行うことができたのです。

大がかりなリファクタリングを行えるようにするためには、コード・ベースをどのように用意すればよいのでしょう。1 つの選択肢は、プロジェクト全体に対してテストを作成し終わるまで、何もコードを作成しないことです。しかしそれを提案した途端、皆さんは解雇され、そしてユニット・テストの価値を認める会社で働くことになるでしょう。このやり方は最適とは言えません。次善の選択肢は、チーム内の他の人達にテストの価値を認識させ、コードの中で最も重要な部分の前後に少しずつテストを追加していく方法です。近い将来の適当な日を選び、例えば「来週の木曜日から、私達のコード・カバレッジは増加し続ける」と宣言するのです。新しいコードを作成する際には必ずテストを追加し、バグを修正する際には必ずテストを作成します。最も重要な部分 (新機能の部分やバグが発生しやすい部分) の前後に徐々にテストを追加することによって、テストの効果が最も高い場所にテストを追加するのです。

ユニット・テストでは、アトミックな動作を検証することができます。しかし、コード・ベースが Composed Method の理想に従っていない場合にはどうすればよいのでしょう。つまり、すべてのメソッドには何十行あるいは何百行ものコードがあり、それぞれのメソッドが無数のタスクを実行する場合にはどうすればよいのでしょう。ユニット・テストのフレームワークを使用すると、これらのメソッドに対して大まかな機能テストを作成することができ、開発者はメソッドの入力状態と出力状態を変換することに集中できるようになります。これらの機能テストは細かな動作をすべて検証するわけではないため、ユニット・テストほど適切ではありませんが、何もテストしないのに比べれば適切です。コードの中で非常に重要な部分に関しては、リファクタリングを開始する前のセーフティー・ネットとして、何らかの機能テストの追加を考慮する必要があります。

リファクタリングのメカニズムは単純であり、現在の主な IDE はどれも、優れたリファクタリング機能を備えています。困難な部分は、何をリファクタリングすべきなのかを見つける点にあります。この記事のこれから先では、この点について説明します。

プロジェクトのベースとの結合

Java の世界では、誰もがフレームワークを使って開発を進めます。フレームワークは、重要かつ最高のベース (つまりこれまで開発者が作成する必要のなかったベース部分) を提供してくれますが、商用のフレームワークであれ、オープンソースのフレームワークであれ、フレームワークの中には危険が潜んでいます。その危険とは、フレームワークを利用すると常に、あまりにもフレームワークに頼りすぎてしまうため、コードの中に隠れている設計が見えにくくなります。

フレームワークやアプリケーション・サーバーにはヘルパー・クラスがあり、これらのクラスを利用すると、その先の開発が容易になります。これらのクラスのいくつかを単純にインポートして使用すれば、ある特定の作業を容易に完了することができます。典型的な例が、非常によく利用されているオープンソースの Web フレームワークである Struts です。Struts には、お決まりの一般的な作業を処理してくれる一連のヘルパー・クラスが含まれています。例えばドメイン・クラスが Struts の ActionForm クラスを継承するようにすると、リクエストのフォーム・フィールドへのデータの追加や、検証イベントとライフサイクル・イベントの処理、その他の手軽な動作を Struts が自動的に行ってくれます。つまり Struts には、Struts のクラスを使えば開発作業が大幅に容易になる、という誘惑があります。Struts を利用すると、構造は図 1 のようになりがちです。

図 1. Struts の ActionForm クラスを使う
Model class extending ActionForm
Model class extending ActionForm

黄色っぽいボックスにはドメイン・クラスが含まれています。ところが Struts フレームワークは ActionForm を継承するように奨励します。そうした方が便利だからです。しかし ActionForm を継承すると、作成されるコードは必然的に Struts フレームワークに結合されます。もはやそのドメイン・クラスは Struts アプリケーション以外では使えません。また ActionForm を継承すると、ドメイン・クラスの設計にも悪影響を与えます。今やこのユーティリティー・クラスはオブジェクト階層構造の上にあり、継承を利用して一般的な動作を統合することはできないからです。

図 2 の方法は図 1 よりもはるかに適切です。

図 2. 合成を使って Struts から分離することで改善された設計
合成を使って Struts から分離することで改善された設計
合成を使って Struts から分離することで改善された設計

このバージョンでは、ドメイン・クラスは Struts の ActionForm に依存していません。あるインターフェースによって、ドメイン・クラスと (ドメインとフレームワークとのブリッジとして動作する) ScheduleItemForm クラスの両方のセマンティクスを定義しています。このインターフェースを ScheduleItemImplScheduleItemForm の両方が実装しており、ScheduleItemForm クラスは継承ではなく合成により、ドメイン・クラスへの参照を保持しています。Struts のヘルパーがドメイン・クラスへの依存性を保持することはできますが、その逆は許容されません。つまりドメイン・クラスをフレームワークに依存させてはいけません。このようにしておくと、他のタイプのアプリケーション (Swing アプリケーションやサービス・レイヤーなど) でも自由に ScheduleItem を使用することができます。

プロジェクトのベースとの結合は多くのアプリケーションで容易に行われ、一般的であり、至る所に見られます。フレームワークを利用する場合、フレームワークが提供する便利な機能をインポートすると、フレームワークのサービスを利用しやすくなります。しかしその誘惑に負けてはいけません。フレームワークによってすべてが覆われてしまうと、イディオムのようなパターン (これまでの記事で、アプリケーションの中で発生する簡単なパターンとして定義しました) がコードの中にあっても発見しにくくなります。

DRY 原則に対する違反

Andy Huntと Dave Thomas は、彼らの著書『達人プログラマー ― システム開発の職人から名匠への道』の中で、Don't Repeat Yourself (同じ処理のコードを繰り返し作成しないこと) という DRY 原則を定義しています (「参考文献」を参照)。コードの中で DRY 原則に違反する 2 つの側面は、コピー・アンド・ペーストによるコーディングと構造的な重複であり、これが設計に影響します。

コピー・アンド・ペーストによるコード

コードの中に重複があると、イディオムのようなパターンを発見できないため、設計がわかりにくくなります。コピー・アンド・ペーストによるコードには場所によって微妙な違いがあるため、あるメソッド、またはメソッドの集合の実際の使い方を判断しにくくなります。そしてもちろん、コピー・アンド・ペーストによるコーディングは最終的に害になること誰もが知っています。結局は動作の変更が必要になり、コードをコピー・アンド・ペーストした場所をすべて追跡することは困難だからです。

では、コード・ベースの中に入り込んだ重複部分をどのようにして見つければよいのでしょう。IDE には、(IntelliJ の場合のように) 重複検出機能があるか、あるいは (Eclipse の場合のように) プラグインが用意されています。スタンドアロンのツールもあり、CPD (Copy/Paste Detector) のようなオープンソースのツールと Simian のような商用ツールの両方があります (「参考文献」を参照)。

CPD プロジェクトは PMD ソース分析ツールの一部です。CPD は Swing ベースのアプリケーションであり、個々のファイル内および複数ファイル全体の両方で、指定された数のトークンを分析します。ここでは説明用の例として、あまり単純ではないコード・ベースが必要なので、先ほど触れた Struts プロジェクトを選びました。Struts 2 コード・ベースに対して CPD を実行すると、図 3 に示す結果が得られます。

図 3. Struts 2 コード・ベースに CPD を実行した結果
Struts 2 コード・ベースに CPD を実行した結果
Struts 2 コード・ベースに CPD を実行した結果

CPD によって、Struts コード・ベースの中にとてもたくさんの重複したコードが見つかります。これらの重複コードの大部分は、Struts にポートレットのサポートを追加する部分の前後にあります。実際、複数ファイルにまたがる重複の大部分は、PortletXXXXXX との間 (例えば PortletApplicationMapApplicationMap との間) にあります。これは、ポートレットをサポートするための設計が適切でなかったことを示す重大なコード・スメル (訳注: コード・スメル (code smell) とは、問題が潜在している可能性のあるコードのこと) であり、既存のフレームワークにさらに動作を追加するために大量の重複があるコードを使っったことによるものです。継承または合成を使った方が簡潔な方法でフレームワークを拡張することができ、そのどちらも不可能な場合には、さらに大きな問題があるということです。

ApplicationMap.java ファイルと Sorter.java ファイルを見ると、このコード・ベースの中に重複に関するもう 1 つの一般的な問題があることがわかります。ApplicationMap.java には重複した 27 行のコード・チャンクが含まれています (リスト 1)。

リスト 1. ApplicationMap.java の中にある重複したコード
entries.add(new Map.Entry() {
    public boolean equals(Object obj) {
        Map.Entry entry = (Map.Entry) obj;

        return ((key == null) ? 
            (entry.getKey() == null) : 
            key.equals(entry.getKey())) && ((value == null) ? 
                (entry.getValue() == null) : 
                value.equals(entry.getValue()));
    }

    public int hashCode() {
        return ((key == null) ? 
            0 : 
            key.hashCode()) ^ ((value == null) ? 
                0 : 
                value.hashCode());
    }

    public Object getKey() {
        return key;
    }

    public Object getValue() {
        return value;
    }

    public Object setValue(Object obj) {
        context.setAttribute(key.toString(), obj);

        return value;
    }
});

この重複したコードの中では、ネストされた三項演算子が何度も使用されています (他の人は誰もそのコードを理解できないため、自分の仕事を確保するという意味では適切なコーディングです)。しかしそれとは別に興味深いところは、このコードの前段に現れる 2 つのメソッドの中で重複が起きているところです。その 2 つのメソッドのうちの最初のメソッドをリスト 2 に示します。

リスト 2. コードの重複がある最初の前段部分
while (enumeration.hasMoreElements()) {
    final String key = enumeration.nextElement().toString();
    final Object value = context.getAttribute(key);
    entries.add(new Map.Entry() {
    // remaining code elided, shown in Listing 1

リスト 3 は、コードの重複がある 2 番目の前段部分を示しています。

リスト 3. 重複のあるコードの 2 番目の前段部分
while (enumeration.hasMoreElements()) {
    final String key = enumeration.nextElement().toString();
    final Object value = context.getInitParameter(key);
    entries.add(new Map.Entry() {
    // remaining code elided, shown in Listing 1

この while ループ全体の中での唯一の違いは、リスト 2 では context.getAttribute(key) を呼び出しているのに対し、リスト 3 では context.getInitParameter(key) を呼び出していることです。当然ながら、これはパラメーター化することができ、そうすれば重複したコードを独自のメソッドに縮小することができます。この Struts の例はいたずらにコピー・アンド・ペーストしたコードの好例ですが、このコピー・アンド・ペーストしたコードは不必要な上、修正も簡単です。

実際、リスト 2 やリスト 3 のようにエントリーを利用して一連の属性に追加できるということは、これが Struts コード・ベースの中にあるイディオムのようなパターンであることを示しています。ほとんど同じコードが複数の場所に存在するようにしてしまうと、Struts によって必ずそのコードをよりわかりやすい 1 つの場所に集約する必要があるという事実が隠されてしまい、そうした集約が行われなくなってしまいます。Struts コード・ベースの複数の場所に現れるクラスがある場合、そのクラスの設計を整理するための 1 つの方法は、そうしたイディオムのようなパターンが存在することを認識し、そのパターンによる動作を統合することです。

構造的な重複

もう 1 つの形式の重複は検出するのが難しく、従って気付かないうちに広がっている可能性があります。それが構造的な重複です。扱ったことがある言語が限定されている (特に Java や C# など、メタプログラミングのサポートが不十分な言語しか扱ったことがない) 開発者の場合は特に、この問題をなかなか見つけることができません。構造的な重複を最も適切に要約した表現として、私の同僚である Pat Farley は「同じ空白でありながら値が異なる」という表現を使っています。つまり、実質的に同じ (つまり空白としては同じ) コードをコピーしたものの、変数の値は異なってしまうという場合です。この形の重複は CPD のようなツールでは検出されません。繰り返し使われるベース部分の各インスタンスで実際に値は固有だからです。しかしこの形の重複は、やはりコードにとって害になります。

下記はその一例です。単純な Employee クラスがあり、このクラスにいくつかのフィールドがあるとしましょう (リスト 4)。

リスト 4. 単純な Employee クラス
public class Employee {
    private String name;
    private int salary;
    private int hireYear;

    public Employee(String name, int salary, int hireYear) {
        this.name = name;
        this.salary = salary;
        this.hireYear = hireYear;
    }

    public String getName() { return name; }
    public int getSalary() { return salary;}
    public int getHireYear() { return hireYear; }
}

この単純なクラスの任意のフィールドに対してソートできるようにしたい、とします。Java 言語の場合には、Comparator インターフェースを実装するコンパレーター・クラスを作成することで、ソート順を変えることができます。namesalary に対するコンパレーターをリスト 5 に示します。

リスト 5. namesalary に対するコンパレーター
public class EmployeeNameComparator implements Comparator<Employee> {
    public int compare(Employee emp1, Employee emp2) {
        return emp1.getName().compareTo(emp2.getName());
    }
}

public class EmployeeSalaryComparator implements Comparator<Employee> {
    public int compare(Employee emp1, Employee emp2) {
        return emp1.getSalary() - emp2.getSalary();                
    }
}

Java 開発者にとって、このコードはまったく自然に思えます。しかし、2 つのコンパレーターのコードを重ねて表示した図 4 をよく見てください。

図 4. 重ねて表示したコンパレーター
重ねて表示したコンパレーター
重ねて表示したコンパレーター

これを見るとわかるように、「同じ空白でありながら値が異なる」という表現にぴったりと当てはまります。2 つのコンパレーターのコードの大部分は重複しており、唯一異なるのは戻り値のみです。ここでは比較を実行するベース部分を「自然な」方法 (つまり Java 言語の設計者が意図した方法) で使っているため、重複があることは自然にはわかりません。しかしここには明らかに重複があります。プロパティーが 3 つのみの場合には大きな問題ではないかもしれませんが、プロパティーの数が次第に増え、大量になったとしたらどうでしょう。どの時点でこの重複に取り組み、どのように解決すればよいのでしょう。

ここではリフレクションを使って汎用的なソート用のベース部分を作成します。このソート用のベース部分には重複したボイラープレート・コードがそれほどありません。そのために、各フィールドに対するソートとコンパレーター生成の両方を自動的に処理するクラスを作成します。リスト 6EmployeeSorter クラスを示しています。

リスト 6. EmployeeSorter クラス
public class EmployeeSorter {

    public void sort(List<DryEmployee> employees, String criteria) {
        Collections.sort(employees, getComparatorFor(criteria));
    }

    private Method getSelectionCriteriaMethod(String methodName) {
        Method m;
        methodName = "get" + methodName.substring(0, 1).toUpperCase() +
                methodName.substring(1);
        try {
            m = DryEmployee.class.getMethod(methodName);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e.getMessage());
        }
        return m;
    }

    public Comparator<DryEmployee> getComparatorFor(final String field) {
        return new Comparator<DryEmployee>() {
            public int compare(DryEmployee o1, DryEmployee o2) {
                Object field1, field2;
                Method method = getSelectionCriteriaMethod(field);
                try {
                    field1 = method.invoke(o1);
                    field2 = method.invoke(o2);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                return ((Comparable) field1).compareTo(field2);
            }
        };
    }
}

sort() メソッドは Collecions.sort() メソッドを使用して、employee のリストと生成されたコンパレーターを渡し、このクラスの 3 番目のメソッドを呼び出します。getComparatorFor() メソッドは、渡された基準に基づいて匿名のコンパレーター・クラスをその場で生成するファクトリーとして動作します。getComparatorFor() メソッドはリフレクションを使用します。つまり getSelectionCriteriaMethod() を使って employee クラスから適切な get メソッドを取得し、比較対象の 2 つのインスタンスそれぞれで get メソッドを呼び出し、その結果を返します。リスト 7 のユニット・テストを見ると、このクラスが 2、3 のフィールドに対してどのように動作しているかがわかります。

リスト 7. 汎用のコンパレーターに対するテスト
public class TestEmployeeSorter {
    private EmployeeSorter _sorter;
    private ArrayList<DryEmployee> _list;
 
    @Before public void setup() {
        _sorter = new EmployeeSorter();
        _list = new ArrayList<DryEmployee>();
        _list.add(new DryEmployee("Homer", 20000, 1975));
        _list.add(new DryEmployee("Smithers", 150000, 1980));
        _list.add(new DryEmployee("Lenny", 100000, 1982));
    }

    @Test public void name_comparisons() {
        _sorter.sort(_list, "name");
        assertThat(_list.get(0).getName(), is("Homer"));
        assertThat(_list.get(1).getName(), is("Lenny"));
        assertThat(_list.get(2).getName(), is("Smithers"));
    }

    @Test public void salary_comparisons() {
        _sorter.sort(_list, "salary");
        assertThat(_list.get(0).getSalary(), is(20000));
        assertThat(_list.get(1).getSalary(), is(100000));
        assertThat(_list.get(2).getSalary(), is(150000));
    }
}

リフレクションをこのように使用すると、複雑さと簡潔さのどちらを取るか、という問題であることがわかります。リフレクションをベースにする方法は、最初は理解しにくいのですが、いくつかの利点があります。第 1 に、この方法は Employee クラスのすべてのプロパティーを、現在と将来にわたって自動的に処理します。このコードが用意できると、新しいプロパティーを Employee に追加しても安全であり、追加したプロパティーをソートするためのコンパレーターを苦労して作成する必要がありません。第 2 に、この方法では大量のプロパティーを効率的に処理することができます。過度の重複にならない限り、構造的な重複を許容した方が楽です。しかし開発者は、「この問題の解決にリフレクションを使うのが妥当なのはプロパティーの数がいくつの場合なのか」と自問する必要があります。プロパティーの数が 10 の場合でしょうか、20 の場合でしょうか、それとも 50 の場合でしょうか。この数字は開発者とチームによって異なるかもしれません。しかし、ある程度客観的に検討するのであれば、リフレクションを使用する方法と個々にコンパレーターを作成する方法のどちらが複雑かを検討してみてはいかがでしょう。

私は「Test-driven design, Part 2」の中で、1 つのメソッドの相対的な複雑さを判断する単純な尺度として、循環的複雑度というメトリックを導入しました。Java 言語での循環的複雑度を測定する便利なオープンソース・ツールとして、JavaNCSS ツールがあります (「参考文献」を参照)。1 つのコンパレーター・クラスに対して JavaNCSS を実行すると、1 が返されます。これは驚くに当たりません。そのクラスの中にある唯一のメソッドは 1 行のコードしかなく、ブロックはありません。EmployeeSorter クラス全体に対して JavaNCSS を実行すると、すべてのメソッドの循環的複雑度の合計は 8 になります。このことから、プロパティーの数が 9 に達したらリフレクションを使う方法に移行するのが妥当であることがわかります。つまりプロパティーの数が 9 に達すると、構造的な重複による複雑さの方がリフレクションをベースとする方法の複雑さを上回るということです。リフレクションを使うのが不安であれば、プロパティーの数をもう少し大きくしてもよいかもしれません。

いずれにせよ、それぞれのソリューションには、それなりの代償も払いますがそれなりの利点もあるため、それをどう判断するかは皆さん次第です。私は Java 言語や他の言語でリフレクションに慣れているため、リフレクションによるソリューションを積極的に選ぶ傾向がありますが、それは私がソフトウェアでのあらゆる繰り返しを嫌うからでもあります。

まとめ

今回の記事では、新方式の設計を理解し、また識別するためのツールとして、リファクタリングの使い方を最初に説明しました。次にプロジェクトのベースとの結合について、またベース部分との結合によって設計にどんな問題が生じるかを説明しました。そしてこの記事の大部分では、いくつか異なる形式で現れる重複について説明しました。リファクタリングと設計に関する話題は豊富にあります。次回の記事は今回の続きとして、最もリファクタリングが必要なコード部分、つまり発見が待たれるイディオムのようなパターンを含んでいる可能性が高いコードの部分を見つける上で、メトリックがいかに役立つかを説明します。


ダウンロード可能なリソース


関連トピック

  • プロダクティブ・プログラマ ― プログラマのための生産性向上術』(Neal Ford 著、2009年、オライリー・ジャパン刊) は、このシリーズで取り上げる話題のいくつかを詳細に解説しています。
  • リファクタリング ― プログラムの体質改善テクニック』(Martin Fowler らの共著、2000年、ピアソンエデュケーション刊) はリファクタリングについて解説した著名な本です。
  • 達人プログラマー ― システム開発の職人から名匠への道』(Andy Hunt と Dave Thomas の共著、2001年、ピアソンエデュケーション刊) によって DRY の原則が有名になりました。
  • PMDでバグを退治する」(Elliotte Rusty Harold 著、developerWorks、2005年1月) は、PMD に組み込まれたルールと独自のカスタム・ルール・セットを使って Java コードの品質を改善する方法を説明しています。
  • Automation for the people: Continual refactoring」(Paul Duvall 著、developerWorks、2008年7月) を読み、リファクタリングに適したコード・スメルを静的な分析ツールを使って特定する方法を学んでください。
  • developerWorks の Java technology ゾーンには Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
  • (CPD を含めて) PMD をダウンロードしてください。
  • Simian (Similarity Analyser) を利用すると、Java、C#、C、C++、COBOL、Ruby、JSP、ASP、HTML、XML、Visual Basic、そして Groovy のソースコードの中にある重複を特定することができます。
  • JavaNCSS は Java プログラミング言語用にソースコードの標準的なメトリクスを 2 種類測定できるコマンドライン・ユーティリティーです。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=398862
ArticleTitle=進化するアーキテクチャーと新方式の設計: 設計のためのリファクタリング
publish-date=05262009