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

时间:2013-10-22 19:21:12

标签: design-patterns aop decorator simple-injector repository-design

我正在尝试使用装饰器构建一个将面向方面编程应用于我的项目的原型。我的项目的某些部分将使用通用存储库(对于简单的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;
    }
}

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

}
}

Controller调用的Decorator链的顺序应该是Controller(检查)&gt;安全性(如果可以继续,则允许呼叫)&gt; Repo(更新持久层然后)&gt;记录(到一些设施)&gt;然后返回Controller。

新控制器类:

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);
}
}

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

如果我想根据返回值在Controller中执行某些操作(例如从Security Decorator返回一个bool),那么我是否必须修改IGenericRepository接口(以及GenericRepository类)?在某种程度上,这意味着,由于Repo和Security Decorator类都实现了相同的接口,如果我想更改Security方法的返回值或参数,我还需要更改Repository方法吗? / p>

另外,我现在只将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;
    }
    // ..

1 个答案:

答案 0 :(得分: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)这是装饰模式的正确使用(因为我不是   使用基本抽象或接口类或装饰器基础)

我不确定你现在是怎么做的;我的代码中没有看到任何装饰器。但有一点是错的。您的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)是否有一种干净的方式来使用多个实现   ILogger服务(例如DatabaseLogger和ConsoleLogger)中的   相同的存储库没有注入两个不同的版本?

这取决于您的需求,但Composite PatternProxy pattern可能会有所帮助。复合模式允许您隐藏一系列事物&#39;在那个东西的界面后面。例如:

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类中,但是有一个   更好的方法是这样做,而不是硬连接记录器并仍然使用   通用存储库?

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

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

<强>更新

  

private readonly IGenericRepository securityGenericRepository;

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

  

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

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

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

您仍然可以通过实施Application_Error事件并检查是否已抛出SecurityException并将用户重定向到一个页面来向用户显示友好消息,该页面说明他们不幸尝试访问某个页面该系统不允许访问。但IMO,如果用户看到该页面,他们要么是“黑客攻击”。系统,或者你犯了编程错误。

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

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

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

  

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

使用两个装饰器进行注册可确保在请求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做了什么?什么是&#39; T&#39;在IRepository<Customer>的背景下?一个更好的名称是CreateCustomer,但这是不可能的,因为IRepository<Order>.CreateCustomer没有任何意义。通过命名IRepository<T>.Create,所有这些问题都会消失。