繁体   English   中英

属性可以用来修改方法的代码/行为吗?

[英]Can attributes be used to modify a method's code/behavior?

假设您有以下形式的几种方法(出于说明目的,此处已大大简化):

片段 1

public async Task<T> DoSomething<T>()
{
    return await MyApi.DoSomeOperation<T>();
}

您希望为所有此类方法添加异常处理功能,但无需编写大量重复代码。 最后,您的方法应该看起来(或执行)如下所示:

片段 2

public async Task<T> DoSomething<T>()
{
    try
    {    
        return await MyApi.DoSomeOperation<T>();
    }
    catch (Exception ex)
    {
        var apiEx = new MyApiException(ex);        
        MyLogger.Log(apiEx);
        throw apiEx;  
    }
}

有没有办法用属性做到这一点? 例如,我可以做这样的事情吗?

片段 3

public class TryCatchLogThrowAttribute : Attribute
{
    public Type ExceptionType { get; set; }
    public Type LoggerType { get; set; }
    // logic goes here:
}

然后按如下方式装饰我的方法(期望实际执行的代码看起来或行为类似于上面的代码段 2)?

片段 4

[TryCatchLogThrow(typeof(MyApiException), typeof(MyLogger))]
public async Task<T> DoSomething<T>()
{
    return await MyApi.DoSomeOperation<T>();
}

这有多可行?

简单的答案是:不。C# 和 .NET 都不支持开箱即用。 属性是附加到成员或类型的元数据,但必须由代码拾取和使用它们才能执行任何操作。

一些属性直接影响编译器,一些实际上并不作为属性存储,而是影响元数据标志,其余的只是作为元数据与类型的每个成员一起存储。

在几乎所有情况下,开箱即用,它们只能通过反射获得,因此您必须编写额外的代码来检查它们的存在并采取行动。 然而,成员本身并没有注意到这些属性的存在,并且内部的代码不会因此而改变。

您所描述的是 AOP,面向方面的编程,您将方面(通常是横切行为)附加到类型或成员,例如日志记录、性能监控、错误处理、访问控制等,而无需实际将行为写入成员作为可读代码。

开箱即用不支持此功能,但有一些产品和库可让您执行此操作。

我可以列出一些,但这个列表并不详尽,因为我已经有几年没有研究 AOP了:

这两种方法都允许您(无论是否为您自动化)加载已编译的程序集,解码并拆开它,在某些地方注入额外的代码,然后将重写的程序集写入磁盘。

在这两者中,我强烈推荐 PostSharp,因为它是迄今为止两者中最容易使用的,但我再次关注 AOP 已经很长时间了,所以可能会有更多的竞争者来争夺这个宝座。

您正在寻找一种实现面向方面编程的方法。 一个有价值的目标。

有没有办法用属性做到这一点?

如果您想使用属性来实现这一点,则只能使用特殊工具来完成。 有两种类型可供选择:

  1. 拦截库
  2. 执行 IL 编织的后编译器

这两种选择都有各自的后果:

  • 拦截库要求您定义接口。 该库将基于该接口生成一个代理类,它允许包装原始类并添加此行为。
  • 拦截库限制将行为添加到方法边界。 你不能完全重写整个方法,尽管我必须承认这是我从未要求过的。
  • 这个城堡动态代理中非常流行的拦截库。 它是一个免费的开源工具
  • 后编译器功能更强大,并且允许更改任何代码,通常以复杂的方式进行。 这可以使用静态方法完成,不需要接口。
  • 然而,后编译器的后果是它们将您的代码与添加的行为紧密结合在一起。 这会使测试复杂化,并使您的代码不太灵活。
  • 一个著名的后编译器是 PostSharp。 它是一种商业产品。 没有免费版本。
  • 另一个后编译器库是 Fody。 它可以免费使用,但要获得支持,您必须是(付费)“客户”。

然而,还有第三种选择,它不需要工具,即使用装饰器设计模式 不过,它确实需要使用接口。

将您的DoSomething<T>()方法视为接口的一部分:

public interface IDoStuff
{
    Task<T> DoSomething<T>();
}

您的默认实现具有您问题的原始代码:

public class DefaultDoStuff : IDoStuff
{
    public async Task<T> DoSomething<T>()
    {
        return await MyApi.DoSomeOperation<T>();
    }
}

现在,您可以通过创建一个允许包装原始IDoStuff的新IDoStuff实现来扩展DefaultDoStuff的行为,如下所示:

public class ExceptionLoggingDoStuff : IDoStuff
{
    private readonly ILogger logger;
    private readonly IDoStuff original;

    public ExceptionLoggingDoStuff(ILogger logger, IDoStuff original)
    {
        this.logger = logger;
        this.original = original;
    }

    public async Task<T> DoSomething<T>()
    {
        try
        {    
            return await this.original.DoSomething<T>();
        }
        catch (Exception ex)
        {
            var apiEx = new MyApiException(ex);        
            this.logger.Log(apiEx);
            throw apiEx;  
        }
    }
}

现在,使用第二个ExceptionLoggingDoStuff实现,您可以构造以下对象图:

// Construct objects
IDoStuff stuff =
    new ExceptionLoggingDoStuff(
        new MyLogger(),
        new DefaultDoStuff());

// Use it
stuff.DoSomething<object>();

ExceptionLoggingDoStuff是一个装饰器,因为它允许装饰(或包装)另一个IDoStuff 这允许您扩展DefaultDoStuff的行为而无需更改它。

装饰器可以非常强大,要以防止代码重复的方式使用它们,它需要一个非常特别设计的应用程序,即遵循SOLID 原则的设计; 这并不总是容易实现的。 我们的书的第 10 章中,Mark Seemann 和我将这种技术称为“面向方面的设计编程”。 因为它试图仅使用与使用工具相反的软件设计原则和模式来实现 AOP。 我们书的第 11 章描述了这种基于工具的 AOP 方法并讨论了它的缺点(在 DI 和松散耦合的背景下)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM