4

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

另外,我知道我的示例代码不是使用装饰器模式,而只是我为该原型提供上下文的代码示例。

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

我在这里使用控制台应用程序只是为了展示如果我以链式方式“新建”它们会是什么样子。

我的问题是:

(1) 如何使用 Simple Injector(在引导类中)将其连接起来并且仍然保持顺序相同?

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

}
}

控制器调用的装饰器链的顺序应该是控制器(检查)> 安全(如果可以继续,则允许调用)> 回购(然后更新持久层)> 日志(到某个设施)> 并返回到控制器。

新的控制器类:

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 方法吗?

另外,我现在是否只将 IGenericRepository 的安全实现传递给控制器​​?

此外,记录器已更改为如下所示:

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:

似乎现在应该像装饰器模式一样工作;感谢 Steven 提供的所有出色答案。

原型主要功能:

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>请求 an 时,GenericRepository<T>都会返回LoggingRepository<T>由 an 包装的 an 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),而无需注入两个不同的版本?

这取决于您的需求,但复合模式代理模式可能在这里有所帮助。复合模式允许您在该事物的接口后面隐藏“事物”的集合。例如:

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 类中,但是有没有比硬连接记录器并仍然使用 Generic Repositories 更好的方法呢?

绝对:不要将记录器注入存储库,因为这是一个横切关注点。您可能会比更改通用存储库代码的其余部分更早地更改日志记录逻辑。所以你应该写一个装饰器。

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

更新:

私有只读 IGenericRepository 安全GenericRepository;

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

如果我想根据返回值在 Controller 中采取一些行动

如果这真的是你需要的,请好好想想。特别是为了安全。在那个级别,您通常应该只想检查并抛出异常。你不想在你的控制器中捕获这样的异常,更不用说你想返回一个值。

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

并且您仍然可以通过实现Application_Error事件并检查是否SecurityException引发了 a 并将用户重定向到一个页面来向用户显示友好的消息,该页面解释了他们不幸地试图访问系统不允许访问的页面。但是 IMO,如果用户看到该页面,他们要么是在“入侵”系统,要么是你犯了编程错误。

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

在我现在正在使用的系统中,我的 Windows 窗体类依赖于IPromptableCommandHandler<TCommand>而不是ICommandHandler<TCommand>. 那是因为我们想向用户显示一个对话框,解释他们输入的数据是无效的(某些数据只能由服务器验证),除了命令之外,我们还传递了一个允许“提示命令处理程序”的委托如果命令被成功处理,则回调。可提示的命令处理程序实现本身依赖于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());

当控制器调用存储库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>.

附言。您为存储库命名的方法真的很难看。以下是一些提示:

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