アーキテクチャ内でビジネスオブジェクトを作成する必要がある、適切に構成されたCMSタイプのシステムでは、

これはアーキテクチャに関する質問です。

私は、データアクセスのためにリポジトリパターンを使用する優れたドメイン層を持つASP.NET MVCでレイヤードシステムを作成したとしましょう。これらのドメインオブジェクトの1つはProductです。 CMS側では、製品の作成と編集のためのビューがあります。そして、私はその製品を見せるべきフロントエンドを持っています。これらのビューはかなり異なるため、異なるビューモデルが適切です。

1)ユーザーがデータ入力ビューで新製品のデータを入力したときに、新しいProductオブジェクトを作成する場所はどこですか?コントローラーで?しかし、オブジェクト作成の責任をコントローラに持たせると、Single Responsibilityの原則が損なわれる可能性があります。または、工場パターンを使用する必要がありますか?これは、入力されたデータが工場に「そのまま」渡されるため、工場が非常に具体的であることを意味します。したがって、IProductFactoryに対するコーディングは不可能です。なぜなら、入力データはそのデータ入力ビューに固有のものであるからです。それで、このコントローラと工場の間に緊密な結合があるのは間違いありませんか?

2)フロントエンドに表示されるProductビューモデルはどこから来るべきですか?その答えは、私にはViewModelFactoryのように見えます。ViewModelFactoryは、ドメインオブジェクトProductを取得し、そこからビューを作成します。しかし、このビューモデールは、このビューに特有のものです。なぜなら、私たちが求めているビューモデルは特定的なものだからです。それでは、コントローラーとビューモデリングが密接に結合されるのは正しいですか?

3)検証:入力データはフロントエンドで検証する必要がありますが、ドメイン層でも製品を検証する必要があります(ドメイン層はUIについて何も知らず、UIが検証を行うかどうかわからないため、検証に依存しないそこ)。検証はどこで行なわれるべきですか? ProductFactoryは良い選択と思われます。製品工場のタスクが「有効な製品オブジェクトの作成」と記述されている場合、SRPを守っているように思えます。 しかし、おそらくProductビジネス・オブジェクトには検証が含まれているはずです。これは、製品の検証が作成時だけでなく他の場所でも必要になるため、より適切と思われます。しかし、まだ作成されていない製品をどのように検証できますか? ProductビジネスオブジェクトはIsNameValid、IsPriceValidなどのメソッドを持つべきですか?

1

1 答え

私はあなたの2番目の質問にまず答えるつもりです。

はい、viewmodelsはコントローラと緊密に結合する必要があります。しかし、ViewModelFactoryは必要ありません。 AutoMapperやValueInjecterのようなものは、ドメインProductをProductViewModelに変換するのに十分なはずです。

あなたの最初の質問については、あなたのドメインのProduct Factoryをあなたのコントローラと分離しておくべきです。あなたが使用できるアプローチはいくつかあります。メソッドの引数としてスカラー値(例えば、string、boolなど)、その他のプリミティブ、純粋な.NETタイプのみをとるファクトリメソッドを作成する方法があります。

その後、コントローラがviewmodelからファクトリメソッドにスカラーを渡すことができます。これは疎結合しており、非常に粘着的です。

例えば:

[HttpPost]
public ActionResult CreateProduct(ProductViewModel model)
{
    if (ModelState.IsValid)
    {
       //assuming product factory is constructor-injected
        var domainProduct = _productFactory.BuildProduct(
            model.Name, model.Price, model.Description);
       //... eventually return a result
    }
    return View(model);
}

別のアプローチは、ドメインオブジェクトにviewmodelプロパティを直接渡すためのメソッドを配置することですが、この方法では、プロパティセッターを非公開にすることをお勧めします。

[HttpPost]
public ActionResult CreateProduct(ProductViewModel model)
{
    if (ModelState.IsValid)
    {
       //assuming no product factory
        var domainProduct = new Domain.Product();
        domainProduct.SetName(model.Name);
        domainProduct.SetPrice(model.Price);
        domainProduct.SetDescription(model.Description);
       //... eventually return a result
    }
    return View(model);
}

あまり冗長ではなく、ドメイン層にオブジェクトを作成するので、最初のオプションを優先します。ただし、MVCレイヤとドメインレイヤの間でビューモデルタイプを共有していないため、両方が緩やかに結合されています。代わりに上位レイヤー(MVC)がドメインレイヤーに依存していますが、ドメインレイヤーにはすべてのMVCに関する懸念がありません。

最初の2つのコメントへの返信

第2のコメントの最初の再検証:それは必ずしも検証を強制する製品工場の責任である必要はありませんが、ドメインでビジネスルールを実施する場合は、工場で以下 。例えば、製品ファクトリは、上記のSetXyzPropertyメソッドと同様に、プロダクトをインスタンス化し、次にビルド操作をエンティティ上のメソッドに委譲することができます(違いは、これらのメソッドは、内部 code> public )。この場合、それ自体の検証を強制するのは製品エンティティの責任です。

バリデーションを強制するために例外をスローすると、それらはファクトリとコントローラにバブルアップします。これは私が一般的にやろうとしていることです。ビジネスルールがコントローラーにバブルすると、MVCに検証ルールがないことを意味し、ModelState.IsValidはfalseでなければなりません。また、ファクトリからメッセージを戻すことを心配する必要もなくなり、ビジネスルール違反は例外の形で発生します。

あなたの最初のコメントについては、はい:MVCはドメインに依存しますが、逆もありません。あなたがファクトモードにビューモデルを渡したければ、あなたのドメインはviewmodelクラスがあるlib(MVCでなければならない)に依存しているでしょう。ファクトリメソッドの引数やファクトリメソッドのオーバーロードが爆発する可能性があります。これが起きた場合は、工場に頼るよりも、より細かい方法をエンティティ自体に公開するほうが良いかもしれません。

たとえば、フォーム全体を通過することなく、ユーザーがすばやくクリックして製品の名前または価格だけを変更できるフォームがあるとします。そのアクションは、フルブラウザのPOSTではなく、JSONを使用してajaxで実行することさえできます。コントローラがそれを処理するとき、 productFactory.RePriceOrRename(int productId、object priceOrName)の代わりに myProduct.SetPriceOrName(object priceOrName)を呼び出す方が簡単かもしれません。

質問の回答への返信

他の人は意見が異なるかもしれませんが、私の場合、ビジネスドメインは検証APIを公開すべきではありません。これは、エンティティに対してIsValidPriceメソッドを持つことができないと言っているわけではありません。しかし、私は公開APIの一部として公開されるべきではないと思います。次の点を考慮してください。

namespace NinetyNineCentStore.Domain
{
    public class Product
    {
        public decimal Price { get; protected set; }
        public void SetPrice(decimal price)
        {
            ValidatePrice(price);
            Price = price;
        }
        internal static bool IsPriceValid(decimal price)
        {
            return IsPriceAtLeast99Cents(price) 
                && IsPriceAtMostNineteen99(price) 
                && DoesPriceEndIn99Cents(price);
        }
        private static bool IsPriceAtLeast99Cents(decimal price)
        {
            return (price >= 0.99m);
        }
        private static bool IsPriceAtMostNineteen99(decimal price)
        {
            return (price <= 19.99m);
        }
        private static bool DoesPriceEndIn99Cents(decimal price)
        {
            return (price % 1 == 99);
        }
        private static void ValidatePrice(decimal price)
        {
            if (!IsPriceAtLeast99Cents(price))
                throw new InvalidOperationException(
                    "Product price must be at least 99 cents.");
            if (!IsPriceAtMostNineteen99(price))
                throw new InvalidOperationException(
                    "Product price must be no greater than 19.99.");
            if (!DoesPriceEndIn99Cents(price))
                throw new InvalidOperationException(
                    "Product price must end with 99 cents.");
        }
    }
}

上記は、APIに公開することなく、エンティティに対する検証をカプセル化します。あなたの工場は依然として内部IsPriceValid を呼び出すことができますが、小さなビジネスルールのすべての順列に関心を持つ必要はありません。クライアント(内部またはパブリック)がルールに違反しようとすると、例外がスローされます。

このパターンは過度のように思えるかもしれませんが、エンティティに複数のプロパティを含むビジネスルールを検討してください。たとえば、 Product.IsOnSale == true のときに DoesPriceEndIn99Cents ルールを破ることができます。すでにValidatePriceがカプセル化されているため、新しい検証APIメソッドを公開することなくそのルールに対応できます。

3
追加された
明確で有用な答えをありがとう。私が推測するいくつかの理由は次のとおりです。1)すべてのデータが毎回利用可能であるとは限らないため、複数の過負荷が発生する可能性があります。2)主な過負荷にはかなりのパラメータが含まれている可能性があります。しかし、当然のことながら、ビューモデルをプロダクトファクトリに1つのパラメータとして渡すことはできません。なぜなら、これはより便利に思えるからです。3)ビューモデルファクタリは、UIレイヤの一部です
追加された 著者 staccata,
2番目のコメントを削除し、元の質問に3)
追加された 著者 staccata,