拦截器对我的C#类有什么作用?

19

我被要求在我的asp.net Web应用程序中实现Castle Dynamic Proxy,我正在查阅从Castle ProjectCode Project获取的几篇文章有关在asp.net Web应用程序中使用Castle Dynamic Proxy的文章......

这两篇文章都涉及创建拦截器,但我无法理解为什么要在类中使用拦截器...为什么要拦截本来表现正常的类呢?


那么,你首先为什么被要求使用DP呢? - Krzysztof Kozmic
2个回答

65

假设你的类在某种操作中需要实现三个任务:

  1. 进行安全检查;
  2. 记录方法调用;
  3. 缓存结果。

假设你的类不知道有关安全性、日志记录或缓存的具体配置方式,那么你需要依赖这些事物的抽象。

有几种方法可以实现这一点。其中一种方法是设置一堆接口并使用构造函数注入:

public class OrderService : IOrderService
{
    private readonly IAuthorizationService auth;
    private readonly ILogger logger;
    private readonly ICache cache;

    public OrderService(IAuthorizationService auth, ILogger logger,
        ICache cache)
    {
        if (auth == null)
            throw new ArgumentNullException("auth");
        if (logger == null)
            throw new ArgumentNullException("logger");
        if (cache == null)
            throw new ArgumentNullException("cache");
        this.auth = auth;
        this.logger = logger;
        this.cache = cache;
    }

    public Order GetOrder(int orderID)
    {
        auth.AssertPermission("GetOrder");
        logger.LogInfo("GetOrder:{0}", orderID);
        string cacheKey = string.Format("GetOrder-{0}", orderID);
        if (cache.Contains(cacheKey))
            return (Order)cache[cacheKey];
        Order order = LookupOrderInDatabase(orderID);
        cache[cacheKey] = order;
        return order;
    }
}

这并不是糟糕的代码,但是请考虑我们引入的问题:

  • OrderService类没有三个依赖项中的任何一个都无法工作。如果我们想要使其能够正常工作,就需要在代码中到处添加空值检查。

  • 为了执行相对简单的操作(查找订单),我们编写了大量的额外代码。

  • 所有这些样板代码必须在每个方法中重复,导致非常庞大、丑陋和容易出错的实现。

下面是一个易于维护得多的类:

public class OrderService : IOrderService
{
    [Authorize]
    [Log]
    [Cache("GetOrder-{0}")]
    public virtual Order GetOrder(int orderID)
    {
        return LookupOrderInDatabase(orderID);
    }
}

面向切面编程中,这些属性被称为连接点,它们的完整集合被称为切入点

我们可以留下"提示"来说明某些附加操作应该对该方法执行,而不是一遍又一遍地编写依赖代码。

当然,这些属性必须在某个时候转化为代码,但您可以通过为OrderService创建一个代理(请注意,GetOrder方法已被设置为virtual,因为需要重写该服务),并拦截GetOrder方法,将其延迟到主应用程序代码。

编写拦截器可能只需要像这样简单:

public class LoggingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        if (Attribute.IsDefined(invocation.Method, typeof(LogAttribute))
        {
            Console.Writeline("Method called: "+ invocation.Method.Name);
        }
        invocation.Proceed();
    }
}

创建代理的方法如下:

var generator = new ProxyGenerator();
var orderService = (IOrderService)generator.CreateClassProxy(typeof(OrderService),
    new LoggingInterceptor());

这不仅是代码更少重复,而且完全消除了实际的依赖关系,因为看看我们所做的-我们甚至还没有授权或缓存系统,但系统仍在运行。 我们只需通过注册另一个拦截器并检查AuthorizeAttributeCacheAttribute,稍后插入授权和缓存逻辑。

希望这解释了“为什么”。

侧边栏:正如Krzysztof Koźmic所评论的那样,使用动态拦截器不是DP的“最佳实践”。 在生产代码中,您不希望拦截器在不必要的方法中运行,因此请改用IInterceptorSelector


2
你基本上已经明白了。从技术角度来看,我会将“我们是否在正确的方法中”这个检查移动到拦截器选择器(一个IInterceptorSelector实现)中。 - Krzysztof Kozmic
2
@Krzysztof Koźmic:正是你的博客让我开始接触DP,所以我很高兴你能赞同!我也倾向于支持IInterceptorSelector,但在这里我主要是为了让那些不熟悉DP的人更容易理解这个基本示例而将其排除在外。 - Aaronaught
关于属性使用读取的缓存问题,什么才被认为是捕获足够保证唯一性的最佳实践? - Chris Marisic
@Chris:当你说“缓存属性使用读取”时,你是指缓存系统/拦截器的具体实现方式吗?这实际上是一个相当长的故事,起源于这里:https://dev59.com/13E95IYBdhLWcg3wY8_6。最终结果是一个包含20多个类的整个库。我正在使用的方式是,`CacheAttribute` 要么接受格式字符串(应用于参数),要么接受方法名(用于更复杂的键生成)。对于每个被缓存的方法,定义足够唯一的键是由类本身来决定的。 - Aaronaught
哦,忘了提一下,缓存拦截器使用完全限定类型名称作为键的一部分,因此键只需要在每个类的范围内唯一即可。这可能是一个很好的“最佳实践”,它类似于ASP.NET处理脚本注册的方式。 - Aaronaught

4
您会使用Castle-DynamicProxy的原因是为了所谓的面向切面编程。它可以使您在不需要依赖代码本身的情况下将代码插入到代码的标准操作流中。
一个简单的例子就是日志记录。您可以创建一个DynamicProxy,围绕一个您从中获得错误的类,以记录进入方法的数据并捕获任何异常,然后记录异常。
使用拦截器时,您的当前代码不知道它的存在(假设您的软件以正确的接口分离方式构建),并且您可以更改类的注册方式来使用代理类,而无需在代码的其他地方更改一行代码。然后,在解决错误后,您可以关闭代理。
代理的更高级用法可以在NHibernate中看到,其中所有惰性加载都通过代理处理。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接