在通用存储库上使用Decorator模式实现AOP

Aar*_*ron 4 aop design-patterns decorator repository-design simple-injector

我正在尝试使用装饰器构建一个将面向方面编程应用于我的项目的原型.我的项目的某些部分将使用通用存储库(对于简单的CRUD),但最终我还将包含命令和查询处理程序(这些将执行特定任务,如ProcessCustomerOrders等).另外,我想在这里举例说明的横切关注点是安全和日志记录.

另外,我知道我的示例代码不是使用Decorator模式,而只是我为此原型提供上下文的代码示例.

我知道还有其他方法可以实现AOP(或横切关注点),例如代理或代码编织模式,但我不熟悉这些模式,因此不知道它们之间的权衡.

我在这里使用一个控制台应用程序,只是为了展示如果我以链式方式"新建"它们将会是什么样子.

我的问题是:

(1)如何使用Simple Injector(在bootstrap类中)连接它并保持顺序相同?

(2)这是装饰器模式的正确使用(因为我没有使用基本抽象或接口类或装饰器基础)?

(3)在没有注入两个不同版本的情况下,是否有一种干净的方法可以在同一个存储库中使用ILogger服务的多个实现(例如DatabaseLogger和ConsoleLogger)?

(4)实际的日志记录是在Repository方法中实现的,并且ILogger服务被注入到Repository类中,但有没有比硬连接记录器更好的方法呢?仍然使用Generic Repositories?

(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;
    }
}
Run Code Online (Sandbox Code Playgroud)

更新根据@ 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<>));

}
}
Run Code Online (Sandbox Code Playgroud)

Controller调用的Decorator链的顺序应该是Controller(检查)> Security(如果OK继续,然后允许调用)> Repo(更新持久层然后)> Log(到某个设施)>并返回到控制器.

新控制器类:

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);
}
}
Run Code Online (Sandbox Code Playgroud)

关于以上代码的问题摘录:

如果我想根据返回值在Controller中执行某些操作(例如从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.");
}
// ...
}
Run Code Online (Sandbox Code Playgroud)

在上面,我只是调用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);
  }
  // ...
  }
Run Code Online (Sandbox Code Playgroud)

上面我不明白的是,记录器何时/何时被调用?

更新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);
}
Run Code Online (Sandbox Code Playgroud)

验证(安全)装饰器:

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);
    }
    //..
Run Code Online (Sandbox Code Playgroud)

记录装饰器:

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.");
    }
    // ..
Run Code Online (Sandbox Code Playgroud)

通用存储库:

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();
    }
    //..
Run Code Online (Sandbox Code Playgroud)

控制者:

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;
    }
    // ..
Run Code Online (Sandbox Code Playgroud)

Ste*_*ven 6

(1)如何使用Simple Injector(在bootstrap类中)连接它并仍保持相同的顺序,

Simple Injector包含一个RegisterDecorator方法,可用于注册装饰器.已注册的装饰者(保证)按其注册的顺序进行申请.例:

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

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

container.RegisterDecorator(
    typeof(IGenericRepository<>), 
    typeof(SecurityRepositoryDecorator<>));
Run Code Online (Sandbox Code Playgroud)

此配置确保每次IGenericRepository<T>请求时,GenericRepository<T>都会返回一个包装LoggingRepository<T>为a的包装SecurityRepository<T>.最后注册的装饰者将是最外面的装饰者.

(2)这是装饰器模式的正确使用(因为我没有使用基本抽象或接口类或装饰器基础)

我不确定你目前是怎么做的; 我的代码中没有看到任何装饰器.但有一点是错的.您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); }
}
Run Code Online (Sandbox Code Playgroud)

(3)在没有注入两个不同版本的情况下,是否有一种干净的方法可以在同一个存储库中使用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);
        }        
    }    
}
Run Code Online (Sandbox Code Playgroud)

您可以按如下方式注册:

// 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);
Run Code Online (Sandbox Code Playgroud)

另一方面,使用代理模式,您可以隐藏有关如何在代理中生根消息的决定.例:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以按如下方式注册:

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

container.Register<ILogger>(() => new LoggerSelector(
    left: container.GetInstance<ConsoleLogger>(),
    right: container.GetInstance<DatabaseLogger>());
Run Code Online (Sandbox Code Playgroud)

(4)实际的日志记录是在Repository方法中实现的,并且ILogger服务被注入到Repository类中,但有没有比硬连接记录器更好的方法呢?仍然使用Generic Repositories?

绝对:不要将记录器注入存储库,因为这是一个贯穿各领域的问题.您可能会比更改通用存储库代码的其余部分更快地更改日志记录逻辑.所以你应该写一个装饰器.

令人高兴的是,由于您为存储库创建了一个通用接口,因此您只需编写一个通用装饰器即可将存储行为添加到存储库.不幸的是,由于存储库接口有5个成员,因此装饰器需要实现所有这些成员.但你不能因此而责怪装饰者; 它是Repository模式本身违反了Interface Segregation Principle.

更新:

private readonly IGenericRepository securityGenericRepository;

您不应该像这样命名您的存储库.安全和日志记录是跨领域的问题,消费者不应该知道它们的存在.如果您确定需要在安全措施发生之前触发额外的跨领域问题,该怎么办?您要将所有securityGenericRepository依赖项重命名为fooGenericRepository?这将破坏装饰器的整个目的:它们允许您动态地插入新的横切关注点,而无需在应用程序中更改单行代码.

如果我想根据返回值在Controller中执行某些操作

如果真的是你需要的话,请认真思考.特别是对于安全.在那个级别,你通常只想检查并抛出异常.您不希望在控制器中捕获此类异常,更不用说要返回值.

这样的安全装饰器通常意味着安全机制,以防止邪恶的行为者使用您的系统做坏事.投掷SecurityException是正确的事情.此类异常将被记录,并由您的团队或支持人员接收.您可能尝试做的是在用户单击当前角色不允许的按钮时向用户显示友好消息,但您应该阻止向用户显示此按钮.

并且您可能仍然通过实现Application_Error事件并检查是否SecurityException已抛出并将用户重定向到一个页面来向用户显示友好消息,该页面解释了他们不幸地试图访问系统不允许访问的页面.但IMO,如果用户看到该页面,他们要么"黑客"系统,要么你犯了编程错误.

请记住,装饰器实现与包装相同的抽象.这意味着你不能用装饰器改变抽象(并且不能返回不同的东西).如果这是您所需要的,您的消费者将不得不依赖于不同的东西.但请注意,这不是一个非常常见的场景,所以如果这真的是你需要的话,你必须要认真思考.

在我正在进行的系统中,我的Windows窗体类依赖于IPromptableCommandHandler<TCommand>而不是ICommandHandler<TCommand>.那是因为我们想向用户显示一个对话框,说明他们输入的数据无效(某些数据只能由服务器验证)除了命令之外,我们传入一个委托,允许'promptable命令处理程序'到如果命令处理成功,则回调.提示命令处理程序实现本身依赖于ICommandHandler<TCommand>并委派工作并捕获ValidationExceptionWCF服务返回的任何内容.这可以防止每个表单都有一个丑陋的try-catch块.解决方案仍然不是很好,当我得到更好的解决方案时,我会改变.

但是,即使使用这样的解决方案,您可能仍然希望创建一个执行安全性的装饰器并且具有包含catch语句的代理(在我的情况下是可提示的命令处理程序).不要试图返回与装饰者不同的东西.

上面我不明白的是,记录器何时/何时被调用?

使用两个装饰器进行注册可确保在IGenericRepositotory<Customer>请求a时,构造以下对象图:

IGenericRepository<Customer> repository =
    new SecurityRepositoryDecorator<Customer>(
        new LoggingRepositoryDecorator<Customer>(
            new GenericRepository<Customer>(
                new ClientManagementContext()),
            DatabaseLogger(),
        new AspNetUserSecurity());
Run Code Online (Sandbox Code Playgroud)

当控制器调用存储库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
Run Code Online (Sandbox Code Playgroud)

因此,安全装饰器调用日志装饰器,因为安全装饰器包装日志装饰器(并且日志装饰器包装GenericRepository<T>).

PS.您为存储库命名的方法非常难看.以下是一些提示:

  • 调用接口IRepository<T>而不是IGenericRepository<T>(因为T暗示它实际上是通用的).
  • T从方法中删除所有后缀; 当您定义已关闭的存储库时,它们没有任何意义.例如,做IRepository<Customer>.CreateT什么?什么是'T'在一个IRepository<Customer>?一个更好的名字CreateCustomer,但这是不可能的,因为IRepository<Order>.CreateCustomer没有任何意义.通过命名它,IRepository<T>.Create所有这些问题都消失了.