2012年7月23日月曜日

[C#]エンティティのEqualsとGetHashCode

しばらくの間、曖昧な理解のまま Equals と GetHashCode を実装していたので、反省。

まず、Equals と GetHashCode の実装については、以下の記事が参考になります。

概ね理解しているつもりだったんですが、今日ハッシュのコレクションを扱うときに、コレクション内のある要素を削除(Remove)しようとしても、削除できない(Contains(item) が false になってしまう)、というバグが出たので調べていたら、こちらの記事に行き当たりました。

  1. 2 つのオブジェクトが等しい (== 演算子の定義によって等しい) 場合、それらのオブジェクトからは同じハッシュ値が生成されなければならない。同じハッシュ値が生成されない場合、コンテナからオブジェクトを探しだすためのキーとしてハッシュコードを使用することができない。
  2. 任意のオブジェクト A に対して、A.GetHashCode() は各インスタンス毎に異なる値でなければならない。A のどんなメソッドが呼ばれたとしても、A.GetHashCode() は常に同じ値を返さなければならない。これによって、オブジェクトが格納されている正しいバケツを常に見つけだすことができる。
  3. ハッシュ関数は任意に入力された複数の整数値に対し、ランダム分布となる返り値を返す必要がある。この性質により、ハッシュベースのコンテナへ効率的にオブジェクトを格納できる。

この実装ポリシーのうち、2番を思いっきり無視していました。つまり、ハッシュのコレクションを生成したあとに、コレクション内のある要素の GetHashCode の演算に利用してるプロパティが変化して、ハッシュ値が変わってしまったようです。

また、以下の様な記事もありました。

 

エンティティにおける、Equals と GetHashCode の実装

今回このハッシュのコレクションというのは、いわいるエンティティのコレクションだったわけですが、その実装が以下のようなもの。

public class User
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as User;
        return 
            other != null && 
            FirstName == other.FirstName && 
            LastName == other.LastName;
    }

    public override int GetHashCode()
    {
        return 
            (FirstName ?? string.Empty).GetHashCode() ^ 
            (LastName ?? string.Empty).GetHashCode();
    }
}

テーブルのほうはこんな感じです。

CREATE TABLE Users (
    Id INT IDENTITY PRIMARY KEY,
    FirstName NVARCHAR(50) NOT NULL,
    LastName NVARCHAR(50) NOT NULL,
    UNIQUE (FirstName, LastName)
)

Id が人工キーで、スキーマ上のメインの識別子になります。自然キーとして FirstName と LastName が利用できる(実際には名前は被るので使えないと思いますが…例です)ので、FirstName と LastName に一意キー制約を付けてあります。

この Equals 及び GetHashCode の実装では、以下のガイドラインの推奨に従って、人工キーではなく自然キーによる等価比較の実装を行なっています。(ちなみに ORM は NHibernate です

ビジネスキーの等価性 を使って、 equals() と hashCode() を実装することをお勧めします。ビジネスキーの等価性とは、 equals() メソッドが、ビジネスキー、つまり現実の世界においてインスタンスを特定するキー(自然 候補キー) を形成するプロパティだけを比較することを意味します。

このとき、FirstName と LastName が変化する場合、上記のような実装だとハッシュ値が変わってしまってポリシー違反を起こします。

なので、FirstName と LastName が変化する属性としての側面をもつ場合は、Equals と GetHashCode の実装に使うとマズイということになります。(FirstName と LastName が不変ならオッケーなんですが

このような場合は、素直に変化しない(はずの) Id を用いたほうがよさそうです。

public class User
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as User;
        return
            other != null &&
            Id == other.Id;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

本来なら自然キーでの実装のほうが、なにかと扱いやすいのですが仕方ないですね。

ちなみにこの例でも、Id のセッタが公開されてしまっているので、キチンと実装する場合は、Id が不変になるように工夫したほうがよさそうです。

2012年7月18日水曜日

[C#]Enumに振る舞いを

Enumにプロパティとかメソッドとか、豊富な振る舞いを持たせたい時どうすればいいんだろうという疑問です。

拡張メソッドを使う

public enum AircraftType
{
    Fighter,
    Attacker,
    Bomber
}

public static class AircraftTypeHelper
{
    public static string DisplayName(this AircraftType value)
    {
        return _displayNames[value];
    }

    private readonly static IDictionary<AircraftType, string> _displayNames = new Dictionary<AircraftType, string>()
    {
        { AircraftType.Fighter, "戦闘機" },
        { AircraftType.Attacker, "攻撃機" },
        { AircraftType.Bomber, "爆撃機" }
    };
}
 string displayName = AircraftType.Fighter.DisplayName();

こんな感じに拡張メソッドを使うと、プロパティは無理だけど、それなりに振る舞いをつけることができます。

素直にクラスで実装する

public class AircraftType
{
    private AircraftType(AircraftTypeValue value)
    {
        value = value;
    }

    private enum AircraftTypeValue
    {
        Fighter,
        Attacker,
        Bomber
    }

    public static readonly AircraftType Fighter = new AircraftType(AircraftTypeValue.Fighter);
    public static readonly AircraftType Attacker = new AircraftType(AircraftTypeValue.Attacker);
    public static readonly AircraftType Bomber = new AircraftType(AircraftTypeValue.Bomber);

     private readonly AircraftTypeValue _value;

    public override bool Equals(object obj)
    {
        var other = obj as AircraftType;
        return other != null && _value == other._value;
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

こんな感じでシングルトンで実装する方法です。Equals とか ToString なんかの基本的なメソッドの実装は内部に隠してある Enum に丸投げ。

これならプロパティでもなんでも追加できます。(当たり前だけど...)

    public string DisplayName
    {
        get { return _displayNames[_value]; }
    }

    private readonly static IDictionary<AircraftTypeValue, string> _displayNames = new Dictionary<AircraftTypeValue, string>()
    {
        { AircraftTypeValue.Fighter, "戦闘機" },
        { AircraftTypeValue.Attacker, "攻撃機" },
        { AircraftTypeValue.Bomber, "爆撃機" }
    };
    string displayName = AircraftType.Fighter.DisplayName;

どっちがいいんだろう

拡張メソッドを使う方はまんま Enum を使うので手軽ですね。でも拡張メソッド以上のことはできないので、複雑な振る舞いを持たせたりするのには向いていないですね。

シングルトンでの実装は、自由にプロパティでもメソッドでも追加できるので柔軟性はあります。でも、実装がちょっと面倒です。

また、Enum をそのまま使うと FlagsAttributes でビット演算できたりして重宝することもあります。

個人的には後者(シングルトン)を使うことが多いのですが、振る舞いなんかがあまりない純粋な列挙値だったら普通に Enum を使います。あと、ビット演算したいときとか…

ケース・バイ・ケースなのかな?

2012年7月17日火曜日

[ASP.NET MVC]検証周りの設計

ASP.NET MVC では、ModelState を利用してビューに検証結果を表示したり、コントローラ側からエラーを追加したりできます。

ModelBinder を使えば、DataAnnotation で面倒な検証作業を省略でき、エラーを ModelState に手動で追加する必要もなく、とても素晴らしいフレームワークです。

でも、実際に作ってみると躓く(というか躓いた)のが、モデル内部の検証結果をどのようにビューへ伝達するか、でした。

ASP.NET MVC の検証機構と、モデルでの検証

まず ModelBinder や ModelState はあくまでも、コントローラとビュー側の仕組みです。うまく設計されたMVCアーキテクチャであれば、モデルは単体で独立していて、コントローラやビューに依存していないはずです。

そうなると、モデルはしっかり内部に検証機構を備える必要があります。コントローラやビューが変わっても、ちゃんと動作する必要がありますから。

ASP.NET MVC は ビューとコントローラについてはフレームワークとしてサポートしていますが、モデルについては開発者が自身で設計していかないといけません。

どうしたか

検証自体の実装は省略します。DataAnnotation を使ったり、オリジナルの検証機構を実装したりしてみました。個人的には DataAnnotation がいいと思います。基本的な検証なら用意された検証属性を使うだけで実装できますし、カスタム検証属性を作れば、多様なケースに対応できます。

肝心なのは検証結果をいかにビューまで伝達するかです。私が最初に試したのは…

ModelState をモデルに渡す

最初はよくわからずにこうなりました。ModelState をビューとコントローラ、モデルまで一貫した検証結果のコンテナとして利用しました。

しかし ModelState は ASP.NET MVC の仕組みの一つなので、それをモデルに入れてしまうのはなんとも言えない気持ち悪さ、ビューやコントローラへの依存が生まれてしまいます。

検証結果のコンテナを自前で作る

次は自前で検証結果を扱うコンテナを用意しました。モデルが返してくるこの独自の検証結果のコンテナを、コントローラ側で ModelState にマップしてビューへ伝達します。

この方法は最初うまくいったように思いました。でも大きな問題が。すごく面倒です。

また、最終的には ModelState を利用するので、ModelState を意識せざるを得なくなり、最初の試みとだんだん大差なくなってしまいました。

モデルは特別なことをしない

最終的にどうなったかというと…

例外結果のコンテナの仕組みをモデルから全部排除しました。モデルで検証に失敗したら単に例外をスローするだけです。つまり、モデルでの検証はデータを守る最後の砦としてのみ機能させるということです。

その代わり、ModelBinder による検証を充実させ、モデルでの処理の際、極力検証に失敗しないように作ることにしました。ユーザの通常の操作の範囲なら、ModelBinder による検証で十分拾えると思います。

ModelBinder による検証が充実していれば、モデルでの検証が失敗するケースは、特殊なケースになります。例えば悪意ある操作などです。

そんな特殊なケースまで丁寧に検証結果を表示したりする必要はないということにしてしまいました:)

こうすることでモデルの作りがシンプルに保たれ、モデルは必要最小限の仕事に集中すればよくなり、随分すっきりしたように思います。

まとめ?

結局モデルの検証結果を伝達するのをやめるという、なんとも身も蓋もない結論になってしまいましたが、個人的にいろいろ試したところでは、これがいちばんしっくりきました。

ModelBinder は非常に優れた仕組みなので、カスタムバインダや検証属性を作って有効活用しましょう!

TFT 10.14 Peeba Comp

こちらのガイドの自分用まとめです。 https://www.reddit.com/r/CompetitiveTFT/comments/hraunp/tft_1014_break_the_meta_new_peeba_comp_set_35/ 難しいですが完成すると非常に強く、プレ...