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

2012年6月26日火曜日

[L2S]EntitySet.Removeでレコードを削除する

Linq to SQL - Using EntitySet.Remove to delete records | Joe Stevens' Blog

LINQ to SQL のデザイナでそれらしい設定が全くなかったので、「もしかしてレコード削除したいときは、DeleteOnSubmit をちまちま呼ばないといけないのか、ウゲー。」とか思っていたら、ちゃんとあった。

.dbml を右クリックでXMLエディタから開いて、XMLを直接編集することで設定できた。

うーん、機能自体はあるのにデザイナに無いなんて。

2012年6月12日火曜日

Windows Live Writer

Windows Live にはこういうのもあったんですね。試しに使って投稿してみる。

無料ブログの編集はWriter - Windows Live on MSN

この記事を読んで知りました。他にも便利そうなツールがたくさん。

Scott Hanselmanの開発者とパワーユーザーのための究極のWindowsツールリスト2009

2012年5月14日月曜日

[NHibernate]インターフェイスのマッピング

この間ふと、「NHibernateってインターフェイスをマッピングできるのかな。」と気になったので、実験をしてみました。

どういうこと?

例えば以下のようなエンティティがあったとします。

public class Employee : IPerson
{
 public virtual int Id { get; set; }

 public virtual string Name { get; set; }

 public string FullName
 {
  get { return Name; }
 }
}
public class Customer : IPerson
{
 public virtual int Id { get; set; }

 public virtual string FirstName { get; set; }

 public virtual string LastName { get; set; }

 public string FullName
 {
  get { return LastName + " " + FirstName; }
 }
}
いずれも見ての通り IPerson というインターフェイスを実装したクラスです。

public interface IPerson
{
 string FullName { get; }
}
マッピングは以下のようにします。

<class name="Employee" table="Employees" lazy="false">
  
 <id name="Id">
  <generator class="identity"></generator>
 </id>

 <property name="Name"></property>

</class>

<class name="Customer" table="Customers" lazy="false">
  
 <id name="Id">
  <generator class="identity"></generator>
 </id>

 <property name="FirstName"></property>
 <property name="LastName"></property>

</class>
この場合に、IPerson のエンティティ全体に対してクエリを投げたりできるのか。ということです。
以下のような問い合わせを想定します。

var persons = session.Query<IPerson>();
こんなことをしたい場合、どうすればいいんだろう?という疑問です。

ちなみに、適当に以下のようなデータを入れておきます。

Employees
Id Name
1 さいとー
2 ぱず
3 ぼーま
4 いしかわ
Customers
Id FirstName LastName
1 かずんど ごうだ
2 ひでお くぜ

モノは試し

せっかくなので上記のコードを試してみます。今は IPerson なんてマッピングファイルには一切記述していないので、失敗するだろうと思って試すと...


何事も無く動いてしまいました。しかも期待通りの結果です。

NHibernateが出したログから実行されたSQLを見てみます。

select employee0_.Id as Id0_, employee0_.Name as Name0_ from Employees employee0_
select customer0_.Id as Id1_, customer0_.FirstName as FirstName1_, customer0_.LastName as LastName1_ from Customers customer0_

どうやら IPerson に対するクエリを書いたので、実装エンティティを順番に問い合わせて、結果を合わせて返してくれたようですね。たまげたなぁ。


公式サイトのドキュメントに載ってた

ここに載ってました。暗黙的ポリモーフィズムと呼ばれるタイプのマッピングアプローチのようです。

インターフェイスのマッピングに関しては、他にも数種類のアプローチがあり、また今度試して見ることにします。(ドキュメントを眺めているだけだとなんだかよくわからないので...)


気になる

ただ、この方法だと実装エンティティの数だけ問い合わせが行われてしまいますね。
また、条件や並び順を指定したときどうなるのかも少し試してみたのですが、投稿をわけてまとめたいと思います。

2012年4月18日水曜日

DisplayNameを表示するHtmlHelperの拡張メソッド

たぶん誰もが思いつくであろう、DisplayName属性の値を表示するHtmlHelperの拡張メソッドです。

どうして標準でないんだろう。あれ、知らないだけなのか...?


2012年4月14日土曜日

ASP.NET MVC ひとり反省会

最近、ちょこちょこASP.NET MVC(主に2)の開発を経験して、失敗とそれに対する自分なりの正解を覚え書きします。

今まで試行錯誤とWebで先輩方の記事を読んだり、ソースコードを読んだりして、頭の中だけでやっていたんですが、やっぱり少しづつでも文章にして、整理していかないといけないと思ったので。



反省と対策


まずURL、URLをしっかり考える

ちゃんとURLを設計してから、組み立てる。


なぜ

しっかりしたURL設計がないと、ルーティングやコントローラの構成が破綻してしまいがち。
あとでこれを調整するのは辛い作業だった。


どうする

それぞれの機能をちゃんと考えて、相応しいURLを設計する。全てはそれから。


ビューのモデル(ビューモデル)は、ビュー毎に作成する

似た構成のビューのモデルは、つい共用してしまいがちだけど...

なぜ

片方に仕様変更が入った時、もう片方にも影響が出て大変。
この歪を下手に吸収しようとすると、どんどんワケのわからない状態になって、分けておいたほうがよほど楽だったということになってしまった。


どうする

最初からビューごとにビューモデルを分けて作っていく。


追記(2012/04/17)

@onosさんにご指摘頂いたので追記です。
単にビューごとにビューモデルを分けて作るのではなく、どこを切り離し、どこを共通化するのか、しっかり設計する必要がある。

例えば、検証周りの設定は単に分割して作ってしまうと、同じ設定を多方でバラバラに設定することになってしまう可能性がある。
そういった共通化と分離のバランスを取っていく。



クライアント側の検証と、サーバ側(ドメインモデル)の検証は別に考える

検証のやり方はともかく、これは混同しないほうがいいと思った。


なぜ

そもそも、クライアント側とサーバ側とでは、検証の目的が違うはず。
クライアント側は、ユーザビリティの向上が主な目的。
サーバ側は、データを守ることが主な目的。


どうする

検証の設定を無理に共有させようとしない。
それぞれのコンテキストに必要十分な検証を別々に設定する。また、各検証の目的を曖昧にしない。


ドメインのエンティティは、コントローラやビューに露出させない


なぜ

エンティティの制御はモデル内で完結しないと、何が起こるのかわからなくなってしまう。
また、ビューでエンティティを使っていると、エンティティがビューに依存していってしまう。
そうして、ビジネスロジックとは関係ないモノがモデル内で増殖してしまった。


どうする

モデルのアウトラインとなる、サービス層のインターフェイスにエンティティを使用しない。
DTO的なものを、それぞれのコンテキスト別に用意し、それにエンティティのデータを移して使う。
AutoMapperはとても便利。


まとめ

ふと書いておこうと思いついて、パッとでてくるものだけ殴り書きです。
こうやってベストプラクティス的なものを構築していくのは楽しい。

2011年12月3日土曜日

[C#]CompareToの実装を楽にする


最近ちまちまとIComparableを実装していた。例えばこんなクラスがあったら、

public class Version
{
    public Version()
    {
    }
    
    public Version(int major, int minor, int build, int revision)
    {
        Major = major;
        Minor = minor;
        Build = build;
        Revision = revision;
    }
 
    public int Major { get; set; }
    public int Minor { get; set; }
    public int Build { get; set; }
    public int Revision { get; set; }
}

こんな風にIComparableを実装していく。

public class Version : IComparable, IComparable<Version>
{
    public Version()
    {
    }
 
    public Version(int major, int minor, int build, int revision)
    {
        Major = major;
        Minor = minor;
        Build = build;
        Revision = revision;
    }
 
    public int Major { get; set; }
    public int Minor { get; set; }
    public int Build { get; set; }
    public int Revision { get; set; }
 
    public int CompareTo(object obj)
    {
        return CompareTo(obj as Version);
    }
 
    public int CompareTo(T other)
    {
        if (other == null)
            return 1;

        int majorCompared = Major.CompareTo(other.Major);
        if (majorCompared != 0)
            return majorCompared;

        int minorCompared = Minor.CompareTo(other.Minor);
        if (minorCompared != 0)
            return minorCompared;

        int buildCompared = Build.CompareTo(other.Build);
        if (buildCompared != 0)
            return buildCompared;

        return Revision.CompareTo(other.Revison);
    }
}

3つぐらい実装したところで、面倒すぎて死ぬかと思った。しかも、比較対象が参照型だと、nullの判定まで必要でさらに面倒くさい。

そこで、ちょっと考えることにした。

どうしよう


どうせほとんどのクラスが、内部フィールドの単純な比較の合成なんだから、宣言的にそういうことをやってくれる汎用クラスを一つ作れば良い。

具体的には以下のような使い方をしたい。


[TestFixture]
public class ComplexComparerTest
{
    [Test]
    public void Comparer_Test()
    {
        var comparer = new ComplexComparer<Version>();
        comparer
            .Member(v => v.Major)
            .Member(v => v.Minor)
            .Member(v => v.Build)
            .Member(v => v.Revision)
            ;
  
        var ver_1_0_0_0 = new Version(1, 0, 0, 0);
        var ver_2_0_0_0 = new Version(2, 0, 0, 0);

        Assert.IsTrue(0 < comparer.Compare(ver_1_0_0_0, ver_2_0_0_0));
        Assert.IsTrue(0 > comparer.Compare(ver_2_0_0_0, ver_1_0_0_0));
    }
}

ラムダ式で比較に利用するメンバを設定するメソッド(Memberメソッド)を用意し、設定した順に比較が行われるようにしたい。


実装しよう

というわけで実装する。適当にメンバを用意する。


public class ComplexComparer<T> : IComparer, IComparer<T>
{
    public ComplexComparer<T> Member<TMember>(Expression<Func<T, TMember>> expression)
        where TMember : IComparable
    {
        // ここ実装
        return this;
    }

    public int Compare(object x, object y)
    {
        return Compare((T)x, (T)y);
    }

    public int Compare(T x, T y)
    {
        // ここ実装
        return 0;
    }
}

比較処理を順番に実行していけばいいんだから、比較デリゲートのリストを持つようにして、CompareToの中身を実装してしまおう。


public class ComplexComparer<T> : IComparer, IComparer<T>
{
    private readonly IList<Comparison<T>> _comparisons = new List<Comparison<T>>();

    public ComplexComparer<T> Member<TMember>(Expression<Func<T, TMember>> expression)
        where TMember : IComparable
    {
        // あとで実装
        return this;
    }

    public int Compare(object x, object y)
    {
        return Compare((T)x, (T)y);
    }

    public int Compare(T x, T y)
    {
        foreach (var comparison in _comparisons)
        {
            var compared = comparison(x, y);
            if (compared != 0)
                return compared;
        }

        return 0;
    }
}

あとはMemberメソッドで、比較デリゲートが追加されるようにするだけだ。


public ComplexComparer<T> Member<TMember>(Expression<Func<T, TMember>> expression)
        where TMember : IComparable
    {
        var compiled = expression.Compile();

        _comparisons.Add((x, y) => 
        {
            T xMember = compiled(x);
            T yMember = compiled(y);
   
            if (xMember != null)
                return xMember.CompareTo(yMember);
   
            if (yMember != null)
                return yMember.CompareTo(xMember) * -1;
   
            return 0;
        });

        return this;
    }

Compareメソッドにnullの場合の判定も入れておこう。


public int Compare(T x, T y)
    {
        if (x == null || y == null)
        {
            if (x != null) return 1;
            if (y != null) return -1;
            return 0;
        }
  
        foreach (var comparison in _comparisons)
        {
            var compared = comparison(x, y);
            if (compared != 0)
                return compared;
        }

        return 0;
    }



かんせい


public class ComplexComparer<T> : IComparer, IComparer<T>
{
    private readonly IList<Comparison<T>> _comparisons = new List<Comparison<T>>();

    public ComplexComparer<T> Member<TMember>(Expression<Func<T, TMember>> expression)
        where TMember : IComparable
    {
        var compiled = expression.Compile();

        _comparisons.Add((x, y) => 
        {
            T xMember = compiled(x);
            T yMember = compiled(y);
   
            if (xMember != null)
                return xMember.CompareTo(yMember);
   
            if (yMember != null)
                return yMember.CompareTo(xMember) * -1;
   
            return 0;
        });
        
        return this;
    }

    public int Compare(object x, object y)
    {
        return Compare((T)x, (T)y);
    }

    public int Compare(T x, T y)
    {
        if (x == null || y == null)
        {
            if (x != null) return 1;
            if (y != null) return -1;
            return 0;
        }
  
        foreach (var comparison in _comparisons)
        {
            var compared = comparison(x, y);
            if (compared != 0)
                return compared;
        }

        return 0;
    }
}

使ってみる

Versionクラスに実装してみる。

public class Version : IComparable, IComparable<Version>
{
    public Version()
    {
    }
 
    public Version(int major, int minor, int build, int revision)
    {
        Major = major;
        Minor = minor;
        Build = build;
        Revision = revision;
    }
 
    public int Major { get; set; }
    public int Minor { get; set; }
    public int Build { get; set; }
    public int Revision { get; set; }
 
    private static readonly ComplexComparer<Version> _comparer = 
        new ComplexComparer<Version>()
            .Member(v => v.Major)
            .Member(v => v.Minor)
            .Member(v => v.Build)
            .Member(v => v.Revision);
 
    public int CompareTo(object obj)
    {
        return CompareTo(obj as Version);
    }
 
    public int CompareTo(T other)
    {
        return _comparer.Compare(this, other);
    }
}

とてもすっきりしたし楽になった。これなら間違いも少なくなるだろう。めでたしめでたし。

まとめ


  • 内部の比較デリゲートに詰める処理をアレンジすればいろいろバリエーションが広がる。
  • Expressionは便利
  • 宣言的な書き方は楽だし間違いが少ない。

Factorio: Space Exploration クリア記録

 工場建設クラフトゲーム、Factorio の MOD である Space Exploration のクリア記録です。 はじめに プレイ時間は約 350 時間、2023年10月から2025年2月にかけて15ヶ月間に及びました。この期間中3人の友人と毎週末、工場勤務に明け暮れました...