4

デコレーターを使用して、アスペクト指向プログラミングをプロジェクトに適用するプロトタイプを構築しようとしています。私のプロジェクトの一部では、(単純な CRUD のために) 一般的なリポジトリを使用しますが、最終的には、コマンドおよびクエリ ハンドラーも組み込む予定です (これらは、ProcessCustomerOrders などの特定のタスクを実行します)。また、ここで例として挙げたい分野横断的な懸念事項は、セキュリティとロギングです。

また、サンプル コードは Decorator パターンを使用したものではなく、コンテキストを提供するためにこのプロトタイプ用に配置したコードの例にすぎないこともわかっています。

Proxy パターンや Code Weaving パターンなど、AOP (またはクロスカッティング コンサーン) を実装する他の方法があることは理解していますが、これらのパターンに精通していないため、それらの間のトレードオフがわかりません。

ここでコンソール アプリを使用しているのは、連鎖的に「新しく」作成した場合にどのように見えるかを示すためだけです。

私の質問は次のとおりです。

(1) Simple Injector (ブートストラップ クラス) を使用してこれを接続し、順序を同じに保つにはどうすればよいですか?

(2) これはデコレータ パターンの適切な使用法ですか (基本抽象またはインターフェイス クラスまたはデコレータ ベースを使用していないため)?

(3) 2 つの異なるバージョンを注入せずに、同じリポジトリで ILogger サービスの複数の実装 (DatabaseLogger と ConsoleLogger など) を利用するクリーンな方法はありますか?

(4) 実際のログ記録は Repository メソッドに実装され、ILogger サービスは Repository クラスに挿入されますが、ロガーを配線して汎用リポジトリを引き続き使用するよりも、これを行うためのより良い方法はありますか?

(5) このプロトタイプでのリポジトリの使用方法に基づいて、プロキシ パターンまたはコード ウィービング パターンを使用する必要がありますか?

また、このデザインに関する一般的な批評も歓迎します。

プロトタイプコード:

public class Program
{
    public static void Main(string[] args)
    {
        var e = new Entity
        {
            Id = 1,
            Name = "Example Entity",
            Description = "Used by Decorators",
            RowGuild = Guid.NewGuid()
        };

        Controller controller = 
            new Controller(
                new GenericRepository<Entity>(
                    new ClientManagementContext(), 
                    new ConsoleLogger()
                ), 
                new WebUser()
            );

        controller.Create(e);
    }
}

public static class RepositoryBoostrapper
{
    public static void Bootstrap(Container container)
    {
        container.RegisterOpenGeneric(typeof(IGenericRepository<>), typeof(GenericRepository<>));
    }
}

public class Entity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Guid RowGuild { get; set; }
    public byte[] RowVersion { get; set; }
}

public class Controller
{
    private readonly IGenericRepository<Entity> _repository;
    private readonly IUserSecurity _userSecurity;

    public Controller(IGenericRepository<Entity> repository, IUserSecurity userSecurity)
    {
        _repository = repository;
        _userSecurity = userSecurity;
    }

    // Displays all Entities on a web page view
    public ActionResult Index() {
        IEnumerable<Entity> e = null;
        User user = User.Identity.Name;

        if (_userSecurity.ValidateUser(user))
        {
            e = _repository.ReadTs();
        }
        return View(e);
    }

    public ActionResult Create(Entity e) {
        User user = User.Identity.Name;

        if (_userSecurity.ValidateUser(user))
        {
            if (ModelState.IsValid)
            {
                _repository.CreateT(e);
                return RedirectToAction("Index");
            }
        }
        return View(e);
    }
}

public interface IGenericRepository<T>
{
    T ReadTById(object id);
    IEnumerable<T> ReadTs();
    void UpdateT(T entity);
    void CreateT(T entity);
    void DeleteT(T entity);
}

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    private readonly ClientManagementContext _context;
    private readonly ILogger _logger;

    public GenericRepository(ClientManagementContext context, ILogger logger)
    {
        _context = context;
        _logger = logger;
    }

    public T ReadTById(object id) {
        return _context.Set<T>().Find(id);
    }

    public IEnumerable<T> ReadTs() {
        return _context.Set<T>().AsNoTracking().AsEnumerable(); 
    }

    public void UpdateT(T entity) {
        var watch = Stopwatch.StartNew();

        _context.Entry(entity).State = EntityState.Modified;
        _context.SaveChanges();

        _logger.Log(typeof(T).Name +
        " executed in " +
        watch.ElapsedMilliseconds + " ms.");
    }

    public void CreateT(T entity) {
        var watch = Stopwatch.StartNew();

        _context.Entry(entity).State = EntityState.Added;
        _context.SaveChanges();

        _logger.Log(typeof(T).Name +
        " executed in " +
        watch.ElapsedMilliseconds + " ms.");
    }


    public void DeleteT(T entity) {
        _context.Entry(entity).State = EntityState.Deleted;
        _context.SaveChanges();
    }
}



public class Logger
{
    private readonly ILogger _logger;

    public Logger(ILogger logger)
    {
        _logger = logger;
    }

    public void Log(string message)
    {
        _logger.Log(message);
    }
}

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class DatabaseLogger : ILogger
{
    public void Log(string message)
    {
        // database logging
    }
}

public interface IUserSecurity
{
    bool ValidateUser(User user);
}

public class UserSecurity
{
    private readonly IUserSecurity _userSecurity;

    public UserSecurity(IUserSecurity userSecurity)
    {
        _userSecurity = userSecurity;
    }

    public bool ValidateUser(User user)
    {
        return _userSecurity.ValidateUser(user);
    }
}

public class WebUser : IUserSecurity
{
    public bool ValidateUser(User user)
    {
        // validate MVC user

        return true;
    }
}

UPDATE @Stevenの回答に基づく:

デコレーターとリポジトリーのシンプルなインジェクター DI:

public static class RepositoryBoostrapper
{
public static void Bootstrap(Container container)
{
    container.RegisterOpenGeneric(
        typeof(IGenericRepository<>),
        typeof(GenericRepository<>));

    container.RegisterDecorator(
        typeof(IGenericRepository<>),
        typeof(LoggingRepositoryDecorator<>));

    container.RegisterDecorator(
        typeof(IGenericRepository<>),
        typeof(SecurityRepositoryDecorator<>));

}
}

コントローラーによって呼び出されるデコレーター チェーンの順序は、コントローラー (チェック) > セキュリティ (OK の場合は続行して呼び出しを許可する) > レポ (永続化レイヤーを更新してから) > ログ (何らかの機能へ) > そして戻る必要があります。コントローラーへ。

新しいコントローラ クラス:

public class Controller
{
private readonly IGenericRepository<Entity> securityGenericRepository;

public Controller(
    IGenericRepository<Entity> securityGenericRepository)
{
    this.securityGenericRepository = securityGenericRepository;
}

// Displays all Entities on a web page view
public bool Index() {
    var e = new Entity
    {
        Id = 1,
        Name = "Example Entity",
        Description = "Used by Decorators",
        RowGuild = Guid.NewGuid()
    };
    this.securityGenericRepository.CreateT(e);
    return false;
}

public ActionResult Create(Entity e) {
    if (ModelState.IsValid)
    {
        this.securityGenericRepository.CreateT(e);
        return RedirectToAction("Index");
    }
    return View(e);
}
}

上記のコードの抜粋に関する質問:

戻り値に基づいてコントローラで何らかのアクションを実行したい場合 (たとえば、Security Decorator から bool を返す)、IGenericRepository インターフェイス (したがって GenericRepository クラス) を変更する必要がありますか? つまり、Repo クラスと Security Decorator クラスはどちらも同じインターフェイスを実装しているため、Security メソッドの戻り値やパラメーターを変更したい場合は、Repository メソッドも変更する必要があるのでしょうか?

また、IGenericRepository の Security 実装を Controller に渡すだけですか?

また、ロガーは次のように変更されました。

public class LoggingRepositoryDecorator<T> : IGenericRepository<T>
{
private readonly IGenericRepository<T> decoratee;
private readonly ILogger logger;

public LoggingRepositoryDecorator(IGenericRepository<T> decoratee, ILogger logger)
{
    this.decoratee = decoratee;
    this.logger = logger;
}

// ...

public void CreateT(T entity)
{
    var watch = Stopwatch.StartNew();

    this.decoratee.CreateT(entity);

    this.logger.Log(typeof(T).Name + " executed in " +
        watch.ElapsedMilliseconds + " ms.");
}
// ...
}

上記では、Decoratee を呼び出して、Decorator の機能を上に追加するだけです。

最後に、セキュリティ デコレータ:

public class SecurityRepositoryDecorator<T> : IGenericRepository<T>
{
  private readonly IGenericRepository<T> decoratee;
  private readonly IUserSecurity userSecurity;
  private User user;

  public SecurityRepositoryDecorator(
  IGenericRepository<T> decoratee,
  IUserSecurity userSecurity)
  {
    this.decoratee = decoratee;
    this.userSecurity = userSecurity;
    this.user = User.Identity.Name;
  }

  // ...

  public void CreateT(T entity)
  {
    if (userSecurity.ValidateUser(user))
      this.decoratee.CreateT(entity);
  }
  // ...
  }

上記で理解できないのは、ロガーがどこで/いつ呼び出されるのかということです。

更新 2:

Decorator パターンが正常に動作するようです。すべての素晴らしい答えをくれたスティーブンに感謝します。

プロトタイプの主な機能:

public static void Main(string[] args)
{
    var container = new Container();
    PrototypeBoostrapper.Bootstrap(container);

    IRepository<Entity> repository = 
        new ValidateUserDecorator<Entity>(
            new LoggingDecorator<Entity>(
                new Repository<Entity>(
                    new PrototypeContext()), 
                new ConsoleLogger()), 
            new ClaimsPrincipal());

    var controller = new Controller(repository);

    var e = new Entity
    {
        Id = 1,
        Name = "Example Entity",
        Description = "Used by Decorators",
        RowGuild = Guid.NewGuid()
    };

    controller.Create(e);
}

検証 (セキュリティ) デコレーター:

public class ValidateUserDecorator<T> : IRepository<T>
{
    private readonly IRepository<T> decoratee;
    //private readonly IUserSecurity userSecurity;
    private IPrincipal User { get; set; }

    public ValidateUserDecorator(
        IRepository<T> decoratee,
        IPrincipal principal)
    {
        this.decoratee = decoratee;
        User = principal;
    }

    //..
    public void CreateT(T entity)
    {
        if (!User.IsInRole("ValidRoleToExecute"))
            throw new ValidationException();
        this.decoratee.CreateT(entity);
    }
    //..

ロギング デコレータ:

public class LoggingDecorator<T> : IRepository<T>
{
    private readonly IRepository<T> decoratee;
    private readonly ILogger logger;

    public LoggingDecorator(IRepository<T> decoratee, ILogger logger)
    {
        this.decoratee = decoratee;
        this.logger = logger;
    }

    // ..
    public void CreateT(T entity)
    {
        var watch = Stopwatch.StartNew();

        this.decoratee.CreateT(entity);

        this.logger.Log(typeof(T).Name + " executed in " +
                        watch.ElapsedMilliseconds + " ms.");
    }
    // ..

汎用リポジトリ:

public class Repository<T> : IRepository<T> where T : class
{
    private readonly PrototypeContext _context;

    public Repository(PrototypeContext context)
    {
        _context = context;
    }
    //..
    public void CreateT(T entity) {
        _context.Entry(entity).State = EntityState.Added;
        _context.SaveChanges();
    }
    //..

コントローラー:

public class Controller
{
    private readonly IRepository<Entity> repository;

    public Controller(
        IRepository<Entity> repository) {
            this.repository = repository;
    }
    // ..
    public bool Create(Entity e) {
        this.repository.CreateT(e);
        return true;
    }
    // ..
4

1 に答える 1

6

(1) Simple Injector (ブートストラップ クラス) を使用してこれを接続し、順序を同じに保つにはどうすればよいですか?

Simple Injector には、デコレータの登録に使用できるRegisterDecoratorメソッドが含まれています。登録されたデコレーターは、登録された順に適用されます (保証されます)。例:

container.RegisterOpenGeneric(
    typeof(IGenericRepository<>), 
    typeof(GenericRepository<>));

container.RegisterDecorator(
    typeof(IGenericRepository<>), 
    typeof(LoggingRepositoryDecorator<>));

container.RegisterDecorator(
    typeof(IGenericRepository<>), 
    typeof(SecurityRepositoryDecorator<>));

IGenericRepository<T>この構成により、が要求されるたびに、でラップされた でラップされたGenericRepository<T>が返されます。最後に登録されたデコレータが最も外側のデコレータになります。LoggingRepository<T>SecurityRepository<T>

(2) これはデコレーター パターンの適切な使用法ですか (基本抽象またはインターフェイス クラスまたはデコレーター ベースを使用していないため)

あなたが現在どのようにやっているかはわかりません。コードにデコレータがありません。しかし、1 つのことが間違っています。あなたGenericRepository<T>は を使用していますがILogger、ロギングは横断的な懸念事項です。デコレータに配置する必要があります。そのデコレータは次のようになります。

public LoggingRepositoryDecorator<T> : IGenericRepository<T> {
    private IGenericRepository<T> decoratee;
    private ILogger _logger;

    public LoggingRepositoryDecorator(IGenericRepository<T> decoratee,
        ILogger logger) {
        this.decoratee = decoratee;
        this._logger = logger;
    }

    public T ReadTById(object id) { return this.decoratee.ReadTById(id); }
    public IEnumerable<T> ReadTs() { return this.decoratee.ReadTs(); }

    public void UpdateT(T entity) {
        var watch = Stopwatch.StartNew();

        this.decoratee.UpdateT(entity);

        _logger.Log(typeof(T).Name + " executed in " + 
            watch.ElapsedMilliseconds + " ms.");    
    }

    public void CreateT(T entity)  {
        var watch = Stopwatch.StartNew();

        this.decoratee.CreateT(entity); 

        _logger.Log(typeof(T).Name + " executed in " + 
            watch.ElapsedMilliseconds + " ms.");    
    }

    public void DeleteT(T entity) { this.decoratee.DeleteT(entity); }
}

(3) 2 つの異なるバージョンを注入せずに、同じリポジトリで ILogger サービスの複数の実装 (DatabaseLogger と ConsoleLogger など) を利用するクリーンな方法はありますか?

ニーズによって異なりますが、ここでは複合パターンまたはプロキシ パターンのいずれかが役立つ場合があります。Composite パターンを使用すると、「モノ」のコレクションをそのモノのインターフェースの背後に隠すことができます。例えば:

public class CompositeLogger : ILogger {
    private readonly IEnumerable<ILogger> loggers;

    public CompositeLogger(IEnumerable<ILogger> loggers) {
        this.loggers = loggers;
    }

    public void Log(string message) {
        foreach (var logger in this.loggers) {
            logger.Log(message);
        }        
    }    
}

これは次のように登録できます。

// Register an IEnumerable<ILogger>
container.RegisterCollection<ILogger>(new[] {
    typeof(DatabaseLogger), 
    typeof(ConsoleLogger)
});

// Register an ILogger (the CompositeLogger) that depends on IEnumerable<ILogger>
container.Register<ILogger, CompositeLogger>(Lifestyle.Singleton);

一方、プロキシ パターンを使用すると、プロキシ内でメッセージをルート化する方法に関する決定を隠すことができます。例:

public class LoggerSelector : ILogger {
    private readonly ILogger left;
    private readonly ILogger right;

    public LoggerSelector(ILogger left, ILogger right) {
        this.left = left;
        this.right = right;
    }

    public void Log(string message) {
        var logger = this.SelectLogger(message);
        logger.Log(message);
    }

    private ILogger SelectLogger(string message) {
        return message.Contains("fatal") ? this.left : this.right;
    }
}

これは次のように登録できます。

container.Register<ConsoleLogger>();
container.Register<DatabaseLogger>();

container.Register<ILogger>(() => new LoggerSelector(
    left: container.GetInstance<ConsoleLogger>(),
    right: container.GetInstance<DatabaseLogger>());

(4) 実際のログ記録は Repository メソッドに実装され、ILogger サービスは Repository クラスに挿入されますが、ロガーを配線して汎用リポジトリを引き続き使用するよりも、これを行うためのより良い方法はありますか?

絶対に: ロガーをレポジトリに挿入しないでください。これは分野横断的な問題だからです。汎用リポジトリ コードの残りの部分を変更するよりもはるかに早く、ロギング ロジックを変更する可能性があります。したがって、代わりにデコレータを作成する必要があります。

幸いなことに、リポジトリ用の汎用インターフェースを作成したので、ロギング動作をリポジトリに追加するための汎用デコレータを 1 つ作成するだけで済みます。残念ながら、リポジトリ インターフェイスには 5 つのメンバーがあるため、デコレーターはそれらすべてを実装する必要があります。しかし、これをデコレータのせいにすることはできません。Interface Segregation Principleに違反しているのはリポジトリ パターンそのものです。

アップデート:

プライベート読み取り専用 IGenericRepository securityGenericRepository;

このようにリポジトリに名前を付けるべきではありません。セキュリティとロギングは分野横断的な問題であり、消費者はそれらの存在について知る必要はありません。セキュリティが解除される前にトリガーする必要がある追加の分野横断的な懸念事項が必要であると判断した場合はどうしますか? すべてのsecurityGenericRepository依存関係の名前を に変更しますfooGenericRepositoryか? それは、デコレーターを持つことの目的全体を台無しにしてしまいます: アプリケーションのコードを 1 行も変更することなく、新しい分野横断的な問題を動的にプラグインすることができます。

戻り値に基づいてコントローラーで何らかのアクションを実行したい場合

それが本当に必要なものかどうかよく考えてください。特にセキュリティの場合。そのレベルでは、通常、チェックして例外をスローするだけでよいはずです。コントローラーでそのような例外をキャッチしたくはありません。値を返したいということは言うまでもありません。

このようなセキュリティ デコレータは通常、悪意のある者がシステムで悪いことをするのを防ぐための安全メカニズムを意味します。投げるSecurityExceptionことは正しいことです。このような例外はログに記録され、チームまたはサポートによって取り上げられます。おそらくあなたがしようとしているのは、ユーザーが現在の役割で許可されていないボタンをクリックしたときにわかりやすいメッセージを表示することですが、代わりに、このボタンをユーザーに表示しないようにする必要があります。

Application_Errorまた、イベントを実装し、SecurityExceptionがスローされたかどうかを確認し、システムがアクセスを許可していないページに残念ながらアクセスしようとしたことを説明するページにユーザーをリダイレクトすることにより、ユーザーにわかりやすいメッセージを表示することもできます。しかし、IMO、ユーザーがそのページを見た場合、彼らはシステムを「ハッキング」しているか、プログラミングミスを犯しています。

デコレータはラップと同じ抽象化を実装することに注意してください。これは、デコレーターを使用して抽象化を変更できない (そして別のものを返すことができない) ことを意味します。これが必要な場合、消費者は別のものに依存する必要があります。ただし、これはあまり一般的なシナリオではないことに注意してください。そのため、これが本当に必要かどうかをよく考える必要があります。

私が現在取り組んでいるシステムでは、私の Windows フォーム クラスはIPromptableCommandHandler<TCommand>ではなく に依存していICommandHandler<TCommand>ます。これは、ユーザーが入力したデータが無効であることを説明するダイアログをユーザーに表示し (一部のデータはサーバーによってのみ検証できる)、コマンドに加えて、「プロンプト可能なコマンド ハンドラー」を許可するデリゲートを渡すためです。コマンドが正常に処理された場合にコールバックします。プロンプト可能なコマンド ハンドラーの実装自体は、 に依存し、作業を委任して、サービスから返されたものをICommandHandler<TCommand>すべてキャッチします。これにより、各フォームが醜い try-catch ブロックを持つことを防ぎます。それでも解決策はあまり良くありません。より良い解決策が得られたら変更します。ValidationExceptionWCF

しかし、そのようなソリューションを使用しても、セキュリティを実行し、catch ステートメントを含むプロキシ (私の場合はプロンプト可能なコマンド ハンドラー) を持つデコレータを作成する必要があるでしょう。デコレータとは異なるものを返そうとしないでください。

上記で理解できないのは、ロガーがどこで/いつ呼び出されるのかということです。

2 つのデコレーターへの登録により、IGenericRepositotory<Customer>が要求されたときに、次のオブジェクト グラフが構築されることが保証されます。

IGenericRepository<Customer> repository =
    new SecurityRepositoryDecorator<Customer>(
        new LoggingRepositoryDecorator<Customer>(
            new GenericRepository<Customer>(
                new ClientManagementContext()),
            DatabaseLogger(),
        new AspNetUserSecurity());

コントローラーがリポジトリCreateメソッドを呼び出すと、次の呼び出しチェーンが実行されます。

Begin SecurityRepositoryDecorator<Customer>.Create (calls `userSecurity.ValidateUser`)
    Begin LoggingRepositoryDecorator.Create (calls `Stopwatch.StartNew()`)
        Begin GenericRepository<Customer>.Create
        End GenericRepository<Customer>.Create
    End LoggingRepositoryDecorator.Create (calls ` this.logger.Log`)
End SecurityRepositoryDecorator<Customer>.Create

したがって、セキュリティ デコレータはログ デコレータを呼び出します。これは、セキュリティ デコレータがログ デコレータをラップする (そしてログ デコレータが をラップするGenericRepository<T>) ためです。

ps。リポジトリのメソッドの命名は本当に醜いです。いくつかのヒントを次に示します。

  • IRepository<T>の代わりにインターフェイスを呼び出しますIGenericRepository<T>(T実際にはジェネリックであることを意味するため)。
  • Tメソッドからすべての接尾辞を削除します。クローズド リポジトリを定義する場合は意味がありません。たとえば、何をしIRepository<Customer>.CreateTますか?のコンテキストで「T」とは何IRepository<Customer>ですか? より適切な名前は ですがCreateCustomer、意味がないため、それは不可能IRepository<Order>.CreateCustomerです。名前を付けることでIRepository<T>.Create、これらの問題はすべて解消されます。
于 2013-10-22T20:24:04.250 に答える