4

大規模な ASP.NET MVC アプリケーションのビジネス レイヤーとデータ レイヤーを作成しようとしています。この規模のプロジェクトを試みるのはこれが初めてなので、本を何冊か読んで、物事を適切に分離することに細心の注意を払っています。通常、私のアプリケーションではビジネス ロジックとデータ アクセス レイヤーが混在し、複数のビジネス エンティティが 1 つのクラス内で絡み合っています (追加する場所を見つけようとしているときに、何度か混乱しました)。

私が読んできた内容のほとんどは、ビジネス レイヤーとデータ レイヤーを分離することです。これはすべて問題ないように思えますが、いくつかのシナリオでこれを行う方法を正確に視覚化するのに苦労しています. たとえば、管理者が新しい製品をシステムに追加できるようにするシステムを作成しているとします。

public class Product
{ 
   public int Id { get; private set; }
   public string Name { get; set; }
   public decimal Price { get; set; }
}

次に、リポジトリを作成してデータ アクセスを分離します。

public class ProductRepository
{
   public bool Add(Product product);
}

製品の名前に少なくとも 4 文字を要求したいとしましょう。これをきれいに行う方法がわかりません。

私が持っていた 1 つのアイデアは、Name の set プロパティを拡張し、4 文字の長さの場合にのみ設定することでした。ただし、 Product.Name != 渡されたものを除いて、製品を作成しているメソッドが名前が設定されなかったことを知る方法はありません。

私が持っていた別のアイデアは、それをリポジトリの Add() メソッドに入れることですが、その後、ビジネス ロジックとデータ ロジックがすぐそこにあります。つまり、Add 呼び出しが失敗した場合、ビジネス ロジックまたは DAL が失敗したためです (また、モック フレームワークを使用してテストできないことも意味します)。

私が考えることができる唯一のことは、レポジトリの Add() メソッドから呼び出される第 3 層に私の DAL を配置することですが、私の本やweb (少なくとも私は見たことがある)。また、それが必要かどうか確信が持てない場合、ドメイン モデルの複雑さが増します。

もう 1 つの例は、名前が 1 つの製品だけで使用されるようにすることです。これは、Product クラス、ProductRepository の Add() メソッド、またはどこに入れますか?

補足として、私は ORM として NHibernate を使用する予定ですが、(理論的には) 必要なことを達成するために、TDD はすべてを分離できるはずなので、使用している ORM は問題ではありません。

前もって感謝します!

4

8 に答える 8

4

私は通常、階層化されたアーキテクチャを使用してこれに取り組みます。これを行う方法?基本的に、次の (理想的には) VS プロジェクトがあります。

  • プレゼンテーション層 (UI 要素が存在する場所)
  • ビジネス層 (実際のビジネス ロジックが存在する場所)
  • データ アクセス レイヤー (基盤となる DBMS と通信する場所)

それらすべてを分離するために、私は最終的にいわゆるインターフェースレイヤーstを使用します

  • プレゼンテーション層 (UI 要素が存在する場所)
  • IBusiness 層 (ビジネス層のインターフェースを含む)
  • ビジネス層 (実際のビジネス ロジックが存在する場所)
  • IDataAccess レイヤー (DAO レイヤーのインターフェイスを含む)
  • データ アクセス レイヤー (基盤となる DBMS と通信する場所)

これは非常に便利で、適切に分離されたアーキテクチャを作成します。基本的に、プレゼンテーション レイヤーはインターフェイスにアクセスするだけで、実装自体にはアクセスしません。対応するインスタンスを作成するには、Factory またはできれば依存性注入ライブラリを使用する必要があります ( Unityは .Net アプリまたは Spring.Net に適しています)。

これは、ビジネス ロジックやアプリのテスト容易性にどのような影響を与えますか?
すべてを詳細に記述するにはおそらく長すぎますが、十分にテスト可能な設計にすることに関心がある場合は、依存性注入ライブラリを絶対に検討する必要があります。

NHibernate を使用すると、どのような ORM でも
、DAO レイヤーが他のレイヤーからインターフェイスを介して完全に分離されるため、基盤となる DB にアクセスするための背後にあるテクノロジーを使用できます。必要に応じて、SQL クエリを直接発行するか、NHibernate を使用することができます。良い点は、アプリの他の部分から完全に独立していることです。SQL を手動で記述して今日から開始し、明日は BL またはプレゼンテーション レイヤーを変更することなく、DAO dll を NHibernate を使用するものと交換できます。
さらに、BL ロジックのテストは簡単です。次のようなクラスがあるかもしれません:

public class ProductsBl : IProductsBL
{

   //this gets injected by some framework
   public IProductsDao ProductsDao { get; set; }

   public void SaveProduct(Product product)
   {
      //do validation against the product object and react appropriately
      ...

      //persist it down if valid
      ProductsDao.PersistProduct(product);
   }

   ...
}

SaveProduct(...)これで、テスト ケースで ProductDao をモック化することにより、メソッドの検証ロジックを簡単にテストできます。

于 2010-02-02T20:42:00.373 に答える
2

Product一部のシナリオで 4 文字未満の製品を許可する場合を除き、製品名の制限などをドメイン オブジェクトに入れます (この場合、コントローラのレベルで 4 文字のルールを適用します)。クライアント側)。ライブラリを共有すると、ドメイン オブジェクトが他のコントローラ、アクション、内部メソッド、さらには他のアプリケーションによって再利用される可能性があることに注意してください。検証は、アプリケーションやユース ケースに関係なく、モデリングしている抽象化に適している必要があります。

ASP .NET MVC を使用しているため、フレームワークに含まれている機能豊富で拡張性の高い検証 API を利用する必要があります (詳細については、キーワードで検索しIDataErrorInfo MVC Validation Application Block DataAnnotationsてください)。呼び出し元のメソッドがドメイン オブジェクトが引数を拒否したことを知る方法はたくさんあります。たとえば、ArgumentOutOfRangeException.

製品名が一意であることを保証する例については、他のすべての の知識が必要になるため、絶対にクラスに入れません。これは論理的に永続層に属し、オプションでリポジトリに属します。ユースケースによっては、名前がまだ存在しないことを確認する別のサービスメソッドが必要になる場合がありますが、後で永続化しようとしたときにまだ一意であると想定しないでください (再度確認する必要があります。一意性を検証し、永続化する前にそれをしばらく保持すると、他の誰かが同じ名前のレコードを永続化する可能性があります)。ProductProduct

于 2010-02-02T20:58:09.807 に答える
1

これは私がそれを行う方法です:

いくつかの一般的なアイテム インターフェイスを継承するエンティティ クラスに検証コードを保持します。

Interface Item {
    bool Validate();
}

次に、リポジトリのCRUD関数で、適切な Validate 関数を呼び出します。

このようにして、すべてのロジック パスが値を検証しますが、その検証が実際に何であるかを確認するには、1 か所だけを調べる必要があります。

さらに、たとえばビューなど、リポジトリの範囲外でエンティティを使用することもあります。そのため、検証が分離されている場合、各アクション パスはリポジトリに問い合わせることなく検証をテストできます。

于 2010-02-02T20:43:48.573 に答える
0

SRP (単一責任の原則)に従って、検証が製品のドメインロジックから分離されていると、より適切に機能する可能性があります。データの整合性に必要なため、おそらくリポジトリに近いはずです。検証を考慮せずに常に実行するようにしたいだけです。

この場合IValidationProvider<T>、IoCコンテナまたは好みに応じて具体的な実装に接続された汎用インターフェイス(例)を使用している可能性があります。

public abstract Repository<T> {

  IValidationProvider<T> _validationProvider;    

  public ValidationResult Validate( T entity ) {

     return _validationProvider.Validate( entity );
  }

}  

このようにして、検証を個別にテストできます。

リポジトリは次のようになります。

public ProductRepository : Repository<Product> {
   // ...
   public RepositoryActionResult Add( Product p ) {

      var result = RepositoryResult.Success;
      if( Validate( p ) == ValidationResult.Success ) {
         // Do add..
         return RepositoryActionResult.Success;
      }
      return RepositoryActionResult.Failure;
   }
}

外部APIを介してこの機能を公開する場合は、さらに一歩進んで、ドメインオブジェクトとデータアクセスの間を仲介するサービスレイヤーを追加できます。この場合、検証をサービスレイヤーに移動し、データアクセスをリポジトリに委任します。あなたが持っているかもしれません、IProductService.Add( p )。しかし、これはすべての薄い層のために維持するのが面倒になる可能性があります。

私の0.02ドル。

于 2010-02-02T21:31:24.880 に答える
0

さて、これが私の3番目の答えです。この猫の皮を剥ぐには非常に多くの方法があるからです。

public class Product
{
    ... // normal Product stuff

    IList<Action<string, Predicate<StaffInfoViewModel>>> _validations;

    IList<string> _errors; // make sure to initialize
    IEnumerable<string> Errors { get; }

    public void AddValidation(Predicate<Product> test, string message)
    {
        _validations.Add(
            (message,test) => { if(!test(this)) _errors.Add(message); };
    }

    public bool IsValid()
    {
        foreach(var validation in _validations)
        {
            validation();
        }

        return _errors.Count() == 0;
    }
}

この実装により、ロジックをドメイン エンティティにハードコーディングすることなく、任意の数のバリデータをオブジェクトに追加できます。ただし、これを理解するには、IoC または少なくとも基本的なファクトリを使用する必要があります。

使用法は次のようになります。

var product = new Product();
product.AddValidation(p => p.Name.Length >= 4 && p.Name.Length <=20, "Name must be between 4 and 20 characters.");
product.AddValidation(p => !p.Name.Contains("widget"), "Name must not include the word 'widget'.");
product.AddValidation(p => p.Price < 0, "Price must be nonnegative.");
product.AddValidation(p => p.Price > 1, "This is a dollar store, for crying out loud!");
于 2010-02-02T22:05:10.697 に答える
0

U は他の検証システムを使用できます。次のようなサービス層の IService にメソッドを追加できます。

IEnumerable<IIssue> Validate(T entity)
{
    if(entity.Id == null)
      yield return new Issue("error message");
}
于 2011-08-28T20:00:34.203 に答える
0

疎結合でこれを実現する別の方法は、次のように、エンティティ タイプのバリデータ クラスを作成し、IoC に登録することです。

public interface ValidatorFor<EntityType>
{
    IEnumerable<IDataErrorInfo> errors { get; }
    bool IsValid(EntityType entity);
}

public class ProductValidator : ValidatorFor<Product>
{
    List<IDataErrorInfo> _errors;
    public IEnumerable<IDataErrorInfo> errors 
    { 
        get
        {
            foreach(IDataErrorInfo error in _errors)
                yield return error;
        }
    }
    void AddError(IDataErrorInfo error)
    {
        _errors.Add(error);
    }

    public ProductValidator()
    {
        _errors = new List<IDataErrorInfo>();
    }

    public bool IsValid(Product entity)
    {
        // validate that the name is at least 4 characters;
        // if so, return true;
        // if not, add the error with AddError() and return false
    }
}

検証するときが来たら、IoC に aを要求し、ValidatorFor<Product>を呼び出しますIsValid()

では、検証ロジックを変更する必要がある場合はどうなるでしょうか? の新しい実装を作成しValidatorFor<Product>、古いものの代わりに IoC に登録できます。ただし、別の基準を追加する場合は、デコレータを使用できます。

public class ProductNameMaxLengthValidatorDecorator : ValidatorFor<Person>
{
    List<IDataErrorInfo> _errors;
    public IEnumerable<IDataErrorInfo> errors 
    { 
        get
        {
            foreach(IDataErrorInfo error in _errors)
                yield return error;
        }
    }
    void AddError(IDataErrorInfo error)
    {
        if(!_errors.Contains(error)) _errors.Add(error);
    }

    ValidatorFor<Person> _inner;

    public ProductNameMaxLengthValidatorDecorator(ValidatorFor<Person> validator)
    {
        _errors = new List<IDataErrorInfo>();
        _inner = validator;
    }

    bool ExceedsMaxLength()
    {
        // validate that the name doesn't exceed the max length;
        // if it does, return false 
    }

    public bool IsValid(Product entity)
    {
        var inner_is_valid = _inner.IsValid();
        var inner_errors = _inner.errors;
        if(inner_errors.Count() > 0)
        {
            foreach(var error in inner_errors) AddError(error);
        }

        bool this_is_valid = ExceedsMaxLength();
        if(!this_is_valid)
        {
            // add the appropriate error using AddError()
        }

        return inner_is_valid && this_is_valid;
    }
}

IoC 構成を更新すると、クラスを開いて変更することなく、最小長と最大長の検証ができるようになりました。この方法で、任意の数のデコレータをチェーンできます。

または、さまざまなプロパティに対して多くのValidatorFor<Product>実装を作成し、IoC にそのようなすべての実装を要求して、それらをループで実行することもできます。

于 2010-02-02T21:38:16.063 に答える
0

制限のために、DAL の部分クラスを利用し、データ注釈バリデーターを実装します。多くの場合、これにはカスタム バリデータの作成が含まれますが、完全に柔軟であるため、うまく機能します。有効性チェックの一部としてデータベースにヒットする、非常に複雑な依存検証を作成することができました。

http://www.asp.net/(S(ywiyuluxr3qb2dfva1z5lgeg))/learn/mvc/tutorial-39-cs.aspx

于 2010-02-02T20:56:14.867 に答える