防御的プログラミングについて

Code Craftを読んでいたら防御的プログラミングについての記述があったので、自分が普段やっている防御的プログラミングについて書いてみる。

まずCode Craftで述べられている防御的プログラミングとは

Code Craft P7 より

注意深く慎重を期したプログラミングのことです。防御的プログラミングでは、信頼性の高いソフトウェアを構築するために、システム内のすべてのコンポーネントを、各コンポーネントがそれ自体を最大限に保護できるような方法で設計します。暗黙の仮定をコード内で明示的に検証することによって、それらを粉砕します。これは、不正な動作を発生させるような方法でコードが呼び出された時点で、それを阻止するか、少なくともその状況を把握しようとする試みです。

とある。

防御的なプログラミングと聞いてまっさきに思い浮かべるのは、メソッドや関数呼び出しで事前条件、事後条件などのチェック(Assert)する事だろう。これは「契約による設計」と呼ばれる手法で比較的知られていると思う。

C#でこれを行う場合は、DebugクラスのAssertメソッドを使う。

public void Do(object obj) {
    // obj引数はnot nullでなければならない
    Debug.Assert(obj != null)
}

Assertメソッドは引数に渡された条件がfalseの場合、コードの実行を中止してAssertメッセージを表示する。

しかし、このAssertメソッドはいささか汎用的過ぎるので、普段は以下のようなクラスを作って特定の用途に特化させた形で使っている。

ArgumentValidation.cs

[DebuggerStepThrough]
public static class ArgumentValidation {
    /// <summary>
    /// 指定したオブジェクトがnull参照かどうかをチェックします。
    /// </summary>
    /// <param name="obj">オブジェクト</param>
    /// <param name="name">引数名</param>
    /// <exception cref="ArgumentNullException">オブジェクトがnull参照の場合</exception>
    public static void CheckForNullReference(object obj, string name) {
        if(obj == null) {
            throw new ArgumentNullException(string.Format("[{0}]引数にnullは指定できません", name));
        }
    }
    /// <summary>
    /// 指定した文字列がnull参照か空文字列でないかどうかをチェックします。
    /// </summary>
    /// <param name="obj">文字列</param>
    /// <param name="name">引数名</param>
    /// <exception cref="ArgumentNullException">文字列がnull参照の場合</exception>
    /// <exception cref="ArgumentException">文字列が空文字列の場合</exception>
    public static void CheckForNullOrEmpty(string obj, string name) {
        CheckForNullReference(obj, name);

        if(obj.Length == 0) {
            throw new ArgumentException(string.Format("[{0}]引数に空文字列は指定できません", name));
        }
    }
}

null参照かどうかをチェックするものと、null参照でなく空文字列でないかどうかをチェックするメソッドを用意している。

内部ではAssertするのではなく、例外を投げている。これはクラス名からもわかるようにメソッドの引数をチェックするものであり、こういったチェックをAssertで行ってしまうと、Releaseビルド時はチェックされない事になってしまう。

まぁReleaseビルドにするのは、こういったチェックが全て通ってからというのが建前だが、実際のところそんな事は保証できないわけで、たいがいの場合、どこかで不正な値が渡されてプログラムがクラッシュする事になってしまう。そして、そういった場合はNullReferenceExceptionやIndexOutOfRangeExceptionなどの汎用的過ぎる例外が飛んでくる為、原因の追跡が困難になってしまう。

そもそもこういったメソッドの引数チェックというのは、そのメソッドの仕様の一部なわけでReleaseビルド時に取っ払ってしまっていいような類の物ではないと考えている。

ということで、Assertではなく例外を投げるようにしている。

使い方
public void Do(object obj, string value) {
    ArgumentValidation.CheckForNullReference(obj, "obj");
    ArgumentValidation.CheckForNullOrEmpty(value, "value");
}

ついでに以下のようなコードスニペットを用意して、記述する手間を減らしている。

argv_nore.snippet

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>argv_nore</Title>
            <Shortcut>argv_nore</Shortcut>
            <Description>引数の空文字列チェックに対するコード スニペット</Description>
            <Author>coma2n</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>arg</ID>
                    <ToolTip>変数名</ToolTip>
                    <Default>var</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp">
<![CDATA[ArgumentValidation.CheckForNullOrEmpty($arg$, "$arg$");$end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

argv_null.snippet

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>argv_null</Title>
            <Shortcut>argv_null</Shortcut>
            <Description>引数のNULL参照チェックに対するコード スニペット</Description>
            <Author>coma2n</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>arg</ID>
                    <ToolTip>変数名</ToolTip>
                    <Default>var</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp">
<![CDATA[ArgumentValidation.CheckForNullReference($arg$, "$arg$");$end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

本当は戻り値に対してもチェックをしなければいけないけど、めんどくさいのでやっていない。というかそこは単体テストでカバーできるので問題は無いだろう。

こういったチェックは地味ながら効果的なのでお試しあれ。