Advent LINQ (12): リファクタリング可能なメンバ名

デバッグやフレームワークのサポートメソッドとして、動的にプロパティ名を取得したい場合がある。例えば、エラーが発生した場合に、そのエラーが発生したプロパティ名をログに記録したいとする。

private ILog log_ = ...;
private string description_;
// 詳細内容を取得・設定する(スペースは含まれてはならない)
public string Description
{
    get
    {
        return description_;
    }
    set
    {
        // スペースが含まれていれば
        if (value.Contains(" ") == true)
        {
            // ログにエラーを記憶する(Log4Net風)
            log_.Error("Description");        // <-- プロパティ名
            return;
        }
        description_ = value;
    }
}

ログ出力時に、プロパティ名を文字列として渡している。このコードを実装後、「Description」というプロパティ名が不適切であり、「Title」に変更したいとしよう。Visual Studioにはテキスト一括置換が存在するので、DescriptionからTitleに一括置換する方法が考えられる。しかし、この方法は他のコードでたまたまDescriptionという名前を持つプロパティやメソッド、更にはDescriptionという文字列なども含め、あらゆるDescriptionをTitleに変更してしまう。これでは、正しく置換されたかどうかを検証しなければならない。

そのようなわけで、Visual Studioには「リファクタ」機能があり、安全にシンボル名を変更する事が出来る。変更しようとしているシンボル名を意味的に解析し、単に同じ名前というだけでなく、実際にコードとして同じ対象を参照するシンボル名だけを、正しく置換してくれる。この機能を使って、上記のDescriptionをTitleに置き換えると、以下のようになる。

public sealed class PersonEntity
{
    private ILog log_ = ...;
    private string title_;      // <-- title_に変更

    // Titleに変更
    public string Title
    {
        get
        {
            return title_;
        }
        set
        {
            // スペースが含まれていれば
            if (value.Contains(" ") == true)
            {
                // ログにエラーを記憶する(Log4Net風)
                log_.Error("Description");        // <-- プロパティ名
                return;
            }
            title_ = value;
        }
    }
}

最初にプライベートフィールドの「description_」を「title_」にリファクタする。すると、ソースコード上のすべてのdescriotion_がtitle_に置き換わる。一括置換でも同じ結果が得られるが、こちらの方がより安全に修正出来ることは、上で説明したとおりだ。
次に、プロパティの名前「Description」を「Title」にリファクタする。ここで問題が発生する。Descriptionプロパティを参照している箇所(上記には存在しないが、他のメソッドやクラスなどから参照している場合は、それらのシンボル名)は、リファクタ操作で正しく修正される。しかし、ログ出力を行っているErrorメソッドの引数の”Description”という文字列は修正されない。

リファクタ操作では、コードの意味が解析される。プロパティやメソッドへの参照コードであれば、自動的に追跡されて修正されるが、文字列である場合は意味的に同一なのかどうかを機械的に判定する事が出来ない。そのため、リファクタでは文字列は置換対象とならないのだ。

このようなコードが実装の中に散見するようになると、リファクタ機能を使って積極的に安全に修正を行うことが困難となる。そこで、コードに意味を与え、リファクタ対象のシンボル名として認識させつつも、動的にシンボル名を取得する方法を考える必要がある。
まず、ログ出力のコードを独立したメソッドにする。

// ログを出力する
private void WriteErrorLog(string propertyName)
{
    log_.Error(propertyName);
}
public string Title
{
    get
    {
        return title_;
    }
    set
    {
        // スペースが含まれていれば
        if (value.Contains(" ") == true)
        {
            // ログにエラーを記憶する
            this.WriteErrorLog("Description");        // <-- プロパティ名
            return;
        }
        title_ = value;
    }
}

やっていることは変わらない。次が肝心だ。WriteErrorLogの引数を、以下のように変更する。

// ログを出力する(タイプセーフ版)
private void WriteErrorLog<T>(Expression<Func<PersonEntity, T>> lambdaExpression)
{
    log_.Error(/* TODO: propertyName */);
}

Expressionクラスを受け取るようになっている。Expressionクラスとは、「式」の構造を表したインスタンスだ。例えば、このメソッドは以下のように使用する。

public string Title
{
    get
    {
        return title_;
    }
    set
    {
        // スペースが含まれていれば
        if (value.Contains(" ") == true)
        {
            // ログにエラーを記憶する
            this.WriteErrorLog(this_ => this_.Title);      // <-- プロパティを参照するラムダ式
            return;
        }
        title_ = value;
    }
}

WriteErrorLogにラムダ式を記述している。ラムダ式は暗黙の型変換が成立する場合は、デリゲートに置き換えることができる。従って、通常は以下のようなデリゲート型(Func<T, U>)の変数で受ける。

// 匿名デリゲート(C# 2.0)
Func<PersonEntity, string> propertyNameFunc = delegate() { return this_.Title; }

// ラムダ式(C# 3.0)
Func<PersonEntity, string> propertyNameFunc = this_ => this_.Title;

しかし、ラムダ式で記述している場合に限り、Expressionクラスでも受けることができる。

// ラムダ式ツリー(C# 3.0)
Expression<Func<PersonEntity, string>> lambdaExpression = this_ => this_.Title;

表面的には、Expressionクラスで受けていること以外に違いはない。しかし、Expressionクラスは以下のようにデリゲートを実行する事が出来ない(そもそもExpressionクラスはデリゲートではない)。

// 匿名デリゲート(C# 2.0)
var title = propertyNameFunc();

// ラムダ式(C# 3.0)
var title = propertyNameFunc();

// ラムダ式ツリー(C# 3.0)
var title = lambdaExpression(); // <-- 構文エラー(デリゲートではない)

Expressionクラスが何者なのかはさておき、このクラスを以下のようにすると、ラムダ式に記述したプロパティ名を取得する事が出来る。

// ログを出力する(タイプセーフ版)
private void WriteErrorLog<T>(Expression<Func<PersonEntity, T>> lambdaExpression)
{
    // ラムダ式の式部分を、メンバ照会式として取得する
    var memberExpression = (MemberExpression)lambdaExpression.Body;

    // メンバ照会式が参照するメンバ情報を、プロパティ情報として取得する
    var propertyInfo = (PropertyInfo)memberExpression.Member;

    // プロパティ名を取得する
    log_.Error(propertyInfo.Name);
}

これで、WriteErrorLogメソッドの引数をラムダ式で記述可能となった。ラムダ式でプロパティ参照式を書けば、それはコードとして意味のある記述となる。コードの意味が解釈できれば、リファクタ機能の対象として、自動的に修正が可能となる。文字列による緩すぎるシンボル名の参照を排除する事で、コードのメンテナンス性が向上する。