次の方法で共有


Cutting Edge

クラスにソフトウェア コントラクトを導入する

Dino Esposito

image: Dino Espositoソフトウェア開発の古くからの良き慣習として、重要な動作を行う前に、各メソッドの先頭に条件付きステートメントという防壁を設置することが推奨されてきました。各条件付きステートメントでは、入力値が確かめなければならないさまざまな条件をチェックします。条件が満たされなければ、コードで例外をスローします。このパターンは、多くの場合、"If-Then-Throw" と呼ばれます。

しかし、効果的で正しいコードを記述するのに必要なのは If-Then-Throw だけでしょうか。これであらゆる場合に事足りるのでしょうか。

If-Then-Throw があらゆる場合に十分では "ない" かもしれないという意見は、新しいものではありません。Design by Contract (DbC: コントラクトによる設計) は、数年前、Bertrand Meyer 氏によって取り入れられた手法で、ソフトウェアを構成する各要素がコントラクトを保持し、各要素が入力として何を想定し、結果として何を提供するかを、形式に従ってそのコントラクトに記述するという考え方に基づいています。If-Then-Throw パターンは、コントラクトの最初の入力を想定する部分にはほぼ対応していますが、2 つ目の何を提供するかという部分は完全に欠落しています。DbC は、すべての主流プログラミング言語でネイティブにサポートされているわけではありません。ただし、フレームワークは存在しているため、一般に使用される言語 (Java、Perl、Ruby、JavaScript、そしてもちろん Microsoft .NET Framework 言語) で DbC の特色を感じることができます。.NET では、mscorlib アセンブリにある Code Contracts ライブラリ (.NET Framework 4 で追加) を通じて DbC を実行します。このライブラリは、Silverlight 4 アプリケーションでは使用できますが、Windows Phone アプリケーションでは使用できないので注意してください。

ほぼすべての開発者は、原理上、コントラクトファーストの開発アプローチがすばらしいものであることには同意するでしょう。しかし、マイクロソフトによってソフトウェアのコントラクトが使用可能になり、Visual Studio に統合された今でも、それほど多くの開発者が .NET 4 アプリケーションでコード コントラクトを積極的に使用しているとは思えません。今月のコラムでは、コントラクトファーストのアプローチによって生み出される、コードの保守と開発の容易さについてのメリットに重点を置いて説明します。うまくいけば、このコラムの主旨を利用して、次のプロジェクトでコード コントラクトを上司に売り込むことができます。今後のコラムでは、構成、ランタイム ツール、継承といったプログラミング機能などの側面を詳しく説明していく予定です。

シンプルな Calculator クラスについて考える

コード コントラクトを利用するかどうかは気持ちの問題です。巨大なアーキテクチャと多数の最先端テクノロジを使用する必要がある大規模なアプリケーションの設計を要請されるまで、コード コントラクトには手を触れないということがないようにしてください。適切に管理しなければ、最も強力なテクノロジでも問題が発生する可能性があることを覚えておいてください。コード コントラクトは、十分理解すれば、ほとんどすべての種類のアプリケーションの役に立ちます。では、まず、シンプルなクラスから始めましょう。次のように典型的な Calculator クラスを使用します。

public class Calculator
{
    public Int32 Sum(Int32 x, Int32 y)
    {
        return x + y;
    }

    public Int32 Divide(Int32 x, Int32 y)
    {
        return x / y;
    }
}

少なくとも 1 つの重要な点が欠落しているため、おそらくこのコードが現実的でないことには同意されるでしょう。つまり、ゼロ除算を行おうとしているかどうかのチェックが欠落しています。このコードの改良版を記述する前に、この電卓には他にも負の値をサポートしないという、対処すべき問題があることにしましょう。図 1 は、If-Then-Throw ステートメントをいくつか追加した更新版のコードです。

図 1 If-Then-Throw のパターンを実装する Calculator クラス

public class Calculator
{
    public Int32 Sum(Int32 x, Int32 y)
    {
        // Check input values
        if (x <0 || y <0)
            throw new ArgumentException();


        // Perform the operation
        return x + y;
    }

    public Int32 Divide(Int32 x, Int32 y)
    {
        // Check input values
        if (x < 0 || y < 0)
            throw new ArgumentException();
        if (y == 0)
            throw new ArgumentException();

        // Perform the operation  
        return x / y;
    }
}

これで、入力データの処理を開始するか、無効な入力の場合には処理開始の前に例外をスローするクラスを記述できました。では、クラスが生成する結果についてはどうでしょう。クラスの結果について、どのようなことがわかっているでしょう。仕様を見ると、どちらのメソッドもゼロ以上の値を返すことが想定されています。この仕様を適用し、仕様を満たさない場合にエラーにするにはどうすればよいでしょう。これには、3 つ目のバージョンのコードが必要です (図 2 参照)。

図 2 事前条件と事後条件をチェックする Calculator クラス

public class Calculator
{
    public Int32 Sum(Int32 x, Int32 y)
    {
        // Check input values
        if (x <0 || y <0)
            throw new ArgumentException();

        // Perform the operation
        Int32 result = x + y;

        // Check output
        if (result <0)
            throw new ArgumentException();

        return result;
    }

    public Int32 Divide(Int32 x, Int32 y)
    {
        // Check input values
        if (x < 0 || y < 0)
            throw new ArgumentException();
        if (y == 0)
            throw new ArgumentException();

        // Perform the operation
        Int32 result = x / y;

        // Check output
        if (result < 0)
            throw new ArgumentException();

        return result;
    }
}

このバージョンでは、どちらのメソッドも入力値のチェック、演算の実行、出力のチェックという、3 つの異なるフェーズから構成されるようになります。入力と出力のチェックは、それぞれ目的が異なります。入力チェックは、呼び出し元のコードのバグを警告します。出力チェックは、自身のコードのバグを見つけます。出力チェックは本当に必要でしょうか。一部の単体テストでは、チェック条件をアサーションを通じて検証できることはわかっています。その場合は、このようなチェックをランタイム コードに埋め込む必要は厳密にはありません。ただし、コードでチェックを行っておくと、クラスが自己記述的になり、クラスで実行できる処理と実行できない処理が明確になります。これは、コントラクトを使用するサービスの条件にとてもよく似ています。

図 2 のソース コードと最初のシンプルなクラスを比較してみると、ソースではかなり多くの行が増加していることがわかります。これでも、わずかな要件しか満たしていないシンプルなクラスです。もう 1 歩先に進めましょう。

図 2 では、先ほど確認した 3 つの手順 (入力チェック、演算、および出力チェック) がシーケンシャルに実行されます。演算の実行が複雑で、メソッドの出口を追加するとしたらどうなるでしょう。さらに、追加する出口の一部で、結果が期待通りでないというエラー状況を参照するとしたらどうなるでしょう。事態は実に複雑になる可能性があります。この点を示すには、メソッドの 1 つに出口の近道を追加すれば十分です (図 3 参照)。

図 3 出口の近道を設けると事後条件をチェックするコードが重複する

public Int32 Sum(Int32 x, Int32 y)
{
    // Check input values
    if (x <0 || y <0)
        throw new ArgumentException();
            
    // Shortcut exit
    if (x == y)
    {
        // Perform the operation
        var temp = x <<1; // Optimization for 2*x

        // Check output
        if (temp <0)
            throw new ArgumentException();

        return temp;
    }

    // Perform the operation
    var result = x + y;

    // Check output
    if (result <0)
        throw new ArgumentException();

    return result;
}

サンプル コード (これは単なる例です) では、Sum メソッドは 2 つの値が等しい場合に加算ではなく乗算するという近道を試みます。ただし、出力値をチェックするコードは、コードの出口でそれぞれ複製しなければなりません。

結局のところ、本格的なツールや、少なくとも特定のヘルパー フレームワークがなければ、ソフトウェア開発にコントラクトファーストのアプローチを採用することは合理的だと考えられる人はいません。事前条件のチェックは比較的簡単でコストがかかりませんが、事後条件を手動で操作すると、コード ベース全体が扱いにくくなり、エラーが発生しやすくなります。入力パラメーターがコレクションの場合に条件をチェックして、メソッドやプロパティが呼び出されたときに、クラスが常に既知の有効な状態であるようにするなど、開発者にとってクラスのソース コードが完全に台無しになるようなコントラクトの付帯的な側面のいくつかについては言うまでもありません。

Code Contracts を導入する

.NET Framework 4 の Code Contracts は、クラスのコントラクトを表現するために、非常に便利な構文を提供するフレームワークです。特に、Code Contracts は、事前条件、事後条件、およびインバリアントの 3 種類のコントラクトをサポートします。事前条件とは、メソッドを安全に実行するために検証すべき前提条件を表します。事後条件とは、メソッドの実行を正常に終了するか、例外をスローするかを判断するために検証すべき条件を表します。最後に、インバリアントは、クラス インスタンスの有効期限内は常に true であるべき条件を表します。より正確に言うと、インバリアントは、クラスとクライアント間の考えられるすべての対話の後 (つまり、コンストラクタなどのパブリック メンバーの実行後) に保持しておかなければならない条件を示します。インバリアントとして表される条件は、プライベート メンバーの呼び出しの後はチェックされず、その後、一時的に違反状態になることがあります。

Code Contracts API は、Contract クラスで定義される静的メソッドの一覧から構成されます。Requires メソッドは事前条件を、Ensures メソッドは事後条件を表すために使用します。図 4 に、Code Contracts を使用して Calculator クラスを書き直す方法を示します。

図 4 Code Contracts を使用して記述した Calculator クラス

using System.Diagnostics.Contracts;
public class Calculator
{
    public Int32 Sum(Int32 x, Int32 y)
    {
        Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
        Contract.Ensures(Contract.Result<Int32>() >= 0);

        if (x == y)
            return 2 * x;

        return x + y;
    }

    public Int32 Divide(Int32 x, Int32 y)
    {
        Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
        Contract.Requires<ArgumentOutOfRangeException>(y > 0);

        Contract.Ensures(Contract.Result<Int32>() >= 0);

        return x / y;
    }
}

図 3図 4 を簡単に比較すれば、DbC の実装に API の効果があることがわかります。メソッドのコードは、かなり読みやすい形に戻り、事前条件と事後条件の両方を含むコントラクト情報と、実際の動作の 2 つのレベルだけで区別されるようになります。図 3 のように、条件と動作を混在させる必要はありません。その結果、読みやすさがかなり向上し、チームはこのコードをはるかに簡単にメンテナンスできるようになります。たとえば、必要に応じて、新しい事前条件の追加や事後条件の編集を迅速かつ安全に実行できます。1 か所にだけを操作するため、変更を明確に追跡できます。

コントラクト情報は、シンプルな C# コードまたは Visual Basic コードで表現します。コントラクトの命令は、従来の宣言型の属性とは似ていませんが、宣言型の特色を強く残しています。属性ではなくシンプルなコードを使用すると、頭の中で考える条件をより自然に表現できるので、開発者のプログラミング能力が向上します。加えて、コード コントラクトを使用すると、コードのリファクタリングを行う際に、提供されるガイダンスが多くなります。実際に、Code Contracts は、メソッドに期待する動作を示します。Code Contracts は、メソッドを記述する際にコーディングの規範を維持するのに役立ち、事前条件と事後条件が多数になる場合でもコードを読みやすくしておくのに役立ちます。図 4 のような高度な構文を使用してコントラクトを表現できたとしても、コードが実際にコンパイルされたときに、結果のフローが図 3 で示したコードと非常に異なる場合があります。では、どこに秘訣があるのでしょう。

Visual Studio のビルド プロセスに統合された追加ツールの Code Contracts rewriter (コード コントラクト リライター) は、コードを作り直して、表現されている事前条件と事後条件の意図を把握し、これらを適切なコード ブロックに拡張して、論理的に属する場所に配置します。開発者は、事後条件の配置場所に関してや、ある時点で、コードを編集して別の出口を追加する際に事後条件を複製する場所について考慮する必要はありません。

条件を表現する

Code Contracts のドキュメントにより事前条件と事後条件の正確な構文がわかります。最新の PDF は、DevLabs のサイト (bit.ly/f4LxHi、英語) から入手できます。これを簡単に要約します。次のメソッドを使用して必要な条件を示し、それ以外の場合は、指定された例外をスローします。

Contract.Requires<TException> (Boolean condition)

このメソッドには、考慮しておきたいオーバーロードがいくつか存在します。Ensures メソッドは、事後条件を表します。

Contract.Ensures(Boolean condition)

事前条件を記述する際は、一般に、条件の表現に入力パラメーターと、おそらく、同じクラスの他のメソッドまたはプロパティのみを含めることになるでしょう。このような場合は、このメソッドを Pure 属性で修飾し、メソッドの実行によってオブジェクトの状態が変更されないことを示す必要があります。Code Contracts のツールでは、プロパティの get アクセス操作子は pure であると想定されるため注意してください。

事後条件を記述する際は、返す値やローカル変数の初期値といった、他の情報にアクセスしなければならない場合があります。これは、Contract.Result<T> などのアドホック メソッドを通じて、メソッドから返される (型 T の) 値を取得し、Contract.OldValue<T> を使用して、メソッドの実行の先頭で指定されたローカル変数に格納されている値を取得して実行します。最後に、メソッドの実行中に例外がスローされるときに、条件を検証する機会もあります。この場合は、Contract.EnsuresOnThrow<TException> メソッドを使用します。

短縮形

実際、コントラクトの構文はプレーンなコードを使用するよりもコンパクトですが、同じようにサイズが大きくなる可能性があります。サイズが大きくなれば、プレーンなコードと同様、コードが読みにくくなります。自然療法として、コントラクトの複数の命令をサブルーチンにグループ化します (図 5 参照)。

図 5 ContractAbbreviator を使用する

public class Calculator
{
    public Int32 Sum(Int32 x, Int32 y)
    {
        // Check input values
        ValidateOperands(x, y);
        ValidateResult();

        // Perform the operation
        if (x == y)
            return x<<1; 
        return x + y;
    }

    public Int32 Divide(Int32 x, Int32 y)
    {
        // Check input values   
        ValidateOperandsForDivision(x, y);
        ValidateResult();

        // Perform the operation
        return x / y;
    }

    [ContractAbbreviator]

    private void ValidateOperands(Int32 x, Int32 y)
    {
        Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
    }

    [ContractAbbreviator]
    private void ValidateOperandsForDivision(Int32 x, Int32 y)
    {
        Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
        Contract.Requires<ArgumentOutOfRangeException>(y > 0);
    }

    [ContractAbbreviator]

    private void ValidateResult()
    {
        Contract.Ensures(Contract.Result<Int32>() >= 0);
    }
}

ContractAbbreviator 属性は、修飾されたメソッドを正しく解釈する方法を、リライターに指示します。実際に、修飾されたメソッドを、展開する一種のマクロとして見なすこの属性がなければ、ValidateResult メソッド (および図 5 のその他の ValidateXxx メソッド) にはやや込み入ったコードが含まれることになります。たとえば、void メソッドで使用された場合、Contract.Result<T> は何を参照するでしょう。現時点では、ContractAbbreviator 属性は mscorlib アセンブリに含まれていないので、プロジェクト内で開発者が明示的に定義する必要があります。このクラスはとても単純です。

namespace System.Diagnostics.Contracts
{
    [AttributeUsage(AttributeTargets.Method, 
      AllowMultiple = false)]
    [Conditional("CONTRACTS_FULL")]
    internal sealed class 
      ContractAbbreviatorAttribute : 
      System.Attribute
    {
    }
}

わかりやすく改良されたコード

まとめると、Code Contracts API (基本的には Contract クラス) は、mscorlib アセンブリに属しているため、.NET Framework 4 にネイティブに含まれます。Visual Studio 2010 のプロジェクト プロパティには、Code Contracts の構成固有の構成ページが付属しています。プロジェクトごとに、そのページに移動して、コントラクトの実行時チェックを明示的に有効にする必要があります。また、DevLabs の Web サイトからランタイム ツールをダウンロードすることも必要です。サイトにアクセスしたら、使用している Visual Studio のバージョンに合ったインストーラーを選択します。ランタイム ツールには、コード コントラクト リライター、インターフェイスのジェネレーター、および静的チェッカーが含まれます。

Code Contracts では、メソッドごとに想定する動作と結果を設定しなければならないので、明確なコードを記述するのに役立ちます。少なくとも、リファクタリングしてコードを改善する際にガイダンスが提供されます。Code Contracts について説明することはもっとたくさんあります。特に、このコラムでは、インバリアントについては簡単に述べただけですし、コントラクトの継承という強力な機能についてはまったく触れていません。今後のコラムでは、これらのすべてのトピックと、他のトピックも取り上げる予定です。お見逃しなく。

Dino Esposito は、『Programming Microsoft ASP.NET MVC』(Microsoft Press、2010 年) の著者であり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者です。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。Twitter (twitter.com/despos、英語) で彼をフォローしてください。

この記事のレビューに協力してくれた技術スタッフの Brian Grunkemeyer に心より感謝いたします。