階層化アーキテクチャでの検証と承認


13

「検証が階層化アーキテクチャのどこに属しているのかを尋ねる別の質問ではありませんか?!?」ええ、はい、しかし、これがこの問題に対する少し異なる見解になることを願っています。

私は、検証は多くの形式を取り、コンテキストベースであり、アーキテクチャの各レベルで異なると固く信じています。これが投稿の基礎です。各レイヤーで実行される検証のタイプを識別するのに役立ちます。また、よくある質問は、承認チェックの場所です。

サンプルシナリオは、ケータリングビジネスのアプリケーションからのものです。日中定期的に、運転手は、トラックを現場から現場へ移動する間に蓄積した余分な現金をオフィスに引き入れることができます。このアプリケーションでは、ユーザーはドライバーのIDと金額を収集することにより、「キャッシュドロップ」を記録できます。関連するレイヤーを説明するスケルトンコードを次に示します。

public class CashDropApi  // This is in the Service Facade Layer
{
    [WebInvoke(Method = "POST")]
    public void AddCashDrop(NewCashDropContract contract)
    {
        // 1
        Service.AddCashDrop(contract.Amount, contract.DriverId);
    }
}

public class CashDropService  // This is the Application Service in the Domain Layer
{
    public void AddCashDrop(Decimal amount, Int32 driverId)
    {
        // 2
        CommandBus.Send(new AddCashDropCommand(amount, driverId));
    }
}

internal class AddCashDropCommand  // This is a command object in Domain Layer
{
    public AddCashDropCommand(Decimal amount, Int32 driverId)
    {
        // 3
        Amount = amount;
        DriverId = driverId;
    }

    public Decimal Amount { get; private set; }
    public Int32 DriverId { get; private set; }
}

internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
    internal ICashDropFactory Factory { get; set; }       // Set by IoC container
    internal ICashDropRepository CashDrops { get; set; }  // Set by IoC container
    internal IEmployeeRepository Employees { get; set; }  // Set by IoC container

    public void Handle(AddCashDropCommand command)
    {
        // 4
        var driver = Employees.GetById(command.DriverId);
        // 5
        var authorizedBy = CurrentUser as Employee;
        // 6
        var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
        // 7
        CashDrops.Add(cashDrop);
    }
}

public class CashDropFactory
{
    public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
    {
        // 8
        return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
    }
}

public class CashDrop  // The domain object (entity)
{
    public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
    {
        // 9
        ...
    }
}

public class CashDropRepository // The implementation is in the Data Access Layer
{
    public void Add(CashDrop item)
    {
        // 10
        ...
    }
}

コードに配置された検証チェックを見た10の場所を示しました。私の質問は、次のビジネスルール(長さ、範囲、形式、タイプなどの標準的なチェックと一緒に)を指定して、それぞれで実行するチェックがある場合、それを実行することです。

  1. キャッシュドロップの金額はゼロより大きくなければなりません。
  2. キャッシュドロップには有効なドライバーが必要です。
  3. 現在のユーザーには、キャッシュドロップを追加する権限が必要です(現在のユーザーはドライバーではありません)。

あなたの考え、あなたがこのシナリオをどのように持っているか、どのようにアプローチするか、そしてあなたの選択の理由を共有してください。


SEは、「理論的および主観的な議論を促進する」ための正しいプラットフォームではありません。終了する投票。
tdammers

言葉遣いが不十分。私は本当にベストプラクティスを探しています。
SonOfPirate

2
@tdammers-はい、それは正しい場所です。少なくともそうなりたい。FAQから:「主観的な質問は許可されています。」それが彼らがStack Overflowの代わりにこのサイトを作った理由です。ナチスに近づかないでください。質問がひどい場合は、あいまいになります。
FastAl

@FastAI:「主観的」な部分ではなく、「議論」が気になります。
tdammers

CashDropAmount使用するのではなく、値オブジェクトを持つことで、ここで値オブジェクトを活用できると思いますDecimal。ドライバーが存在するかどうかの確認はコマンドハンドラーで行われ、同じことが承認規則にも当てはまります。Approver approver = approverService.findById(employeeId)従業員が承認者ロールにない場合にスローするようなことを行うことで、無料で認証を取得できます。Approver単なる値オブジェクトであり、エンティティではありません。ファクトリを削除するか、代わりにARのファクトリメソッドを使用することもできますcashDrop = driver.dropCash(...)
plalx

回答:


2

検証対象がアプリケーションの各層で異なることに同意します。通常、現在のメソッドでコードを実行するために必要なものだけを検証します。基本的なコンポーネントをブラックボックスとして扱い、それらのコンポーネントの実装方法に基づいて検証しません。

したがって、例として、CashDropApiクラスでは、「契約」がnullではないことを確認するだけです。これによりNullReferenceExceptionsが防止され、このメソッドが適切に実行されることを保証するために必要なすべてのことです。

サービスクラスまたはコマンドクラスで何かを検証したかどうかはわかりませんが、ハンドラーはCashDropApiクラスと同じ理由で「コマンド」がnullでないことを確認するだけです。ファクトリクラスとエンティティクラスの両方の方法で検証を確認しました。どちらかが「量」の値を検証したい場所であり、他のパラメーターがnullではない(ビジネスルール)。

リポジトリは、オブジェクトに含まれるデータがデータベースで定義されたスキーマと一致しており、daa操作が成功することのみを検証する必要があります。たとえば、nullにできない列や最大長の列がある場合など。

セキュリティチェックに関しては、本当に意図の問題だと思います。ルールは不正アクセスを防ぐことを目的としているため、このチェックをプロセスのできるだけ早い段階で行い、ユーザーが承認されていない場合に実行する不要な手順の数を減らしたいと思います。多分それをCashDropApiに入れたでしょう。


1

最初のビジネスルール

キャッシュドロップの金額はゼロより大きくなければなりません。

CashDropエンティティとAddCashDropCommandクラスの不変式のように見えます。このような不変条件を強制する方法はいくつかあります。

  1. Design By Contractルートを利用して使用する ごとに進み、場合に応じて、前提条件、事後条件、および[ContractInvariantMethod]の組み合わせでコード契約をします。
  2. 0未満の量を渡すと、ArgumentExceptionをスローする明示的なコードをコンストラクター/セッターに記述します。

あなたの2番目のルールは本質的に広範です(質問の詳細に照らして):有効であることは、運転者エンティティが運転できることを示すフラグを持っていることを意味します(つまり、運転免許証が停止されていません)、それは運転者が実際にその日に動作するか、CashDropApiに渡されたdriverIdが永続ストアで有効であることを意味します。

これらのいずれの場合でも、ドメインモデルをナビゲートし、Driverからインスタンスを取得する必要がありますIEmployeeRepositorylocation 4、コード例でます。そのため、ここでは、リポジトリへの呼び出しがnullを返さないことを確認する必要があります。この場合、driverIdは無効であり、これ以上処理を進めることはできません。

他の2つの(仮想の)チェック(ドライバーに有効な運転免許証があり、今日はドライバーが働いていた)については、ビジネスルールを実行しています。

ここで行う傾向があるのは、エンティティを操作するバリデータクラスのコレクションを使用することです(Eric Evansの書籍-Domain Driven Design の仕様パターンのように)。FluentValidationを使用して、これらのルールとバリデーターを作成しました。その後、より単純なルールから、より複雑で完全なルールを作成(および再利用)できます。また、アーキテクチャ内のどのレイヤーを実行するかを決定できます。しかし、システム全体に散らばることなく、すべてを1か所でエンコードします。

3番目のルールは、横断的な関心事である承認に関連しています。既にIoCコンテナーを使用しているため(IoCコンテナーがメソッドのインターセプトをサポートしていると仮定)、AOPを実行できます。許可を行うapsectを作成すると、IoCコンテナーを使用して、必要な場所にこの許可動作を注入できます。ここでの大きなメリットは、一度ロジックを記述したことですが、システム全体で再利用できます。

動的プロキシ(Castle Windsor、Spring.NET、Ninject 3.0など)を介したインターセプトを使用するには、ターゲットクラスでインターフェイスを実装するか、基本クラスから継承する必要があります。ターゲットメソッドへの呼び出しの前にインターセプトし、ユーザーの承認を確認し、ユーザーが持っていない場合、呼び出しが実際のメソッドに進むのを防ぎます(excption、log、失敗を示す値を返すなど)。操作を実行する適切な役割。

あなたの場合は、どちらかの呼び出しを傍受することができます

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

ここでの問題CashDropServiceは、インターフェイス/基本クラスがないため、傍受できない可能性があります。またはAddCashDropCommandHandler、IoCによって作成されていないため、IoCは呼び出しをインターセプトする動的プロキシを作成できません。Spring.NETには、正規表現を介してアセンブリ内のクラスのメソッドをターゲットにできる便利な機能があるため、これが機能する可能性があります。

これがあなたにいくつかのアイデアを与えることを願っています。


「IoCコンテナーを使用して、この許可動作を必要な場所に挿入する」方法を説明できますか?これは魅力的なように思えますが、AOPとIoCを連携させることはこれまでのところ私を逃れます。
-SonOfPirate

残りについては、コンストラクターやセッターに検証を配置して、オブジェクトが無効な状態にならないようにします(不変式の処理)。ただし、IEmployeeRepositoryに移動してドライバーを見つけた後のnullチェックへの参照以外は、検証の残りの部分を実行する詳細は提供しません。FluentValidationの使用と再利用などを考慮して、指定されたモデルのどこにルールを適用しますか?
-SonOfPirate

回答を編集しました-これが役立つかどうかを確認してください。「特定のモデルのどこにルールを適用しますか?」おそらく、コマンドハンドラーで約4、5、6、7です。ビジネスレベルの検証を実行するために必要な情報を提供できるリポジトリにアクセスできます。しかし、ここには私と意見が合わない人もいると思います。
RobertMS

明確にするために、すべての依存関係が注入されています。参照コードを簡潔にするために、それを中断しました。アスペクトはコンテナを介して注入されないため、私の問い合わせはアスペクト内で依存関係を持つことに関係しています。では、たとえば、AuthorizationAspectはどのようにAuthorizationServiceへの参照を取得しますか?
SonOfPirate

1

ルールの場合:

1-キャッシュドロップの金額はゼロより大きくなければなりません。

2-キャッシュドロップには有効なドライバーが必要です。

3-現在のユーザーには、キャッシュドロップを追加する権限が必要です(現在のユーザーはドライバーではありません)。

ビジネスルール(1)の場所(1)で検証を行い、ルール(2)の事前チェックとしてIdがnullまたは負(ゼロが有効であると仮定)でないことを確認します。その理由は、「利用可能な情報で確認できる間違ったデータでレイヤーの境界を越えないでください」という私のルールです。これの例外は、サービスが他の呼び出し元に対する義務の一部として検証を行う場合です。その場合、そこでのみ検証を行えば十分です。

ルール(2)および(3)の場合、これはデータベースアクセスレイヤ(またはデータベースレイヤ自体)でのみ実行する必要があります。これは、データベースアクセスに関係するためです。意図的にレイヤー間を移動する必要はありません。

特に、許可されていないユーザーがこのシナリオを有効にするボタンを押すことをGUIに許可させれば、ルール(3)は回避できます。これはコーディングが困難ですが、優れています。

良い質問!


承認のための+1-UIに配置することは、回答で言及しなかった代替手段です。
-RobertMS

UIで承認チェックを行うと、ユーザーによりインタラクティブなエクスペリエンスが提供されますが、サービスベースのAPIを開発しているため、呼び出し元が実装しているルールと実装していないルールを推測できません。これらのチェックの多くはUIに簡単に委任できるため、投稿の基盤としてAPIプロジェクトを使用することにしました。教科書をすばやく簡単に探すのではなく、ベストプラクティスを探しています。
-SonOfPirate

@ SonOfPirate、INMO、UIは検証を行う必要があります。UIは高速であり、サービスよりも多くのデータを持っているためです(場合によっては)。これで、サービスがクライアントを信頼しないようにする限り、サービスはその責任の一部であるため、独自の検証を行わずに境界外にデータを送信しないでください。したがって、さらに処理するためにデータベースにデータを送信する前に、サービスで(再度)非dbチェックを行うことをお勧めします。
-NoChance
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.