面向方面的编程:什么时候开始使用框架?


22

我只是看着这个演讲Greg Young的警告人们KISS:保持简单愚蠢。

其中一个建议,他的事情是做面向方面编程,一个不会需要一个框架

他首先提出了一个严格的约束条件:所有方法只能使用一个参数,并且只能使用一个参数(尽管他稍后通过使用部分应用程序来放松此参数)。

他给出的示例是定义一个接口:

public interface IConsumes<T>
{
    void Consume(T message);
}

如果我们要发出命令:

public class Command
{
    public string SomeInformation;
    public int ID;

    public override string ToString()
    {
       return ID + " : " + SomeInformation + Environment.NewLine;
    }
}

该命令实现为:

public class CommandService : IConsumes<Command>
{
    private IConsumes<Command> _next;

    public CommandService(IConsumes<Command> cmd = null)
    {
        _next = cmd;
    }
    public void Consume(Command message)
    {
       Console.WriteLine("Command complete!");
        if (_next != null)
            _next.Consume(message);
    }
}

要将日志记录到控制台,只需实施以下一项:

public class Logger<T> : IConsumes<T>
{
    private readonly IConsumes<T> _next;

    public Logger(IConsumes<T> next)
    {
        _next = next;
    }
    public void Consume(T message)
    {
        Log(message);
        if (_next != null)
            _next.Consume(message);
    }

    private void Log(T message)
    {
        Console.WriteLine(message);
    }
}

然后,命令前日志记录,命令服务和命令后日志记录就是:

var log1 = new Logger<Command>(null);
var svr  = new CommandService(log);
var startOfChain = new Logger<Command>(svr);

该命令由以下命令执行:

var cmd = new Command();
startOfChain.Consume(cmd);

为此,例如在PostSharp中,可以这样注释CommandService

public class CommandService : IConsumes<Command>
{
    [Trace]
    public void Consume(Command message)
    {
       Console.WriteLine("Command complete!");
    }
}

然后必须在属性类中实现日志记录,例如:

[Serializable]
public class TraceAttribute : OnMethodBoundaryAspect
{
    public override void OnEntry( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : Entered!" );   
    }

    public override void OnSuccess( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : Exited!" );
    }

    public override void OnException( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : EX : " + args.Exception.Message );
    }
}

Greg使用的参数是,从属性到属性的实现之间的联系“太神奇了”,无法解释初级开发人员的情况。最初的示例全部是“正义代码”,并且易于解释。

因此,在经历了漫长的积累之后,问题是:您何时从Greg的非框架方法转向使用PostSharp这样的工具进行AOP?


3
+1:绝对是一个好问题。一个人可能会简单地说:“ ...如果您已经了解了没有解决方案的解决方案”。
史蒂文·埃弗斯

1
也许我只是不习惯这种风格,但是编写这样的整个应用程序的想法让我感到非常疯狂。我宁愿使用方法拦截器。
亚罗诺(Aaronaught)2011年

@Aaronaught:是的,这就是为什么我要在这里发布的部分原因。格雷格(Greg)的解释是,系统配置随后仅以正常代码连接了所有不同IConsumes部分。不必使用外部XML或某些Fluent接口---还有另一件事需要学习。有人可能会认为这种方法论也是“另一件事”。
Peter K.

我仍然不确定我是否了解动机。诸如AOP之类的概念的本质是要能够声明性地表达关注点,即通过配置。对我来说,这只是在重新发明方形齿轮。并不是对您或您的问题的批评,但我认为唯一明智的答案是“除非其他所有选择都失败,否则我永远不会使用格雷格的方法。”
亚罗诺(Aaronaught)2011年

但这一点都不困扰我,但是这难道不是一个堆栈溢出问题吗?
宫阪丽

Answers:


17

他是否在尝试编写“直接面向TDWTF”的AOP框架?我仍然很不了解他的意思。一旦您说出“所有方法必须都带有一个参数”,您就失败了吧?在那个阶段,您说,好的,这对我编写软件的能力施加了一些严重的人为约束,让我们现在就删除它,三个月后我们就可以使用一个完整的噩梦代码库。

你知道吗?您可以使用Mono.Cecil轻松编写一个简单的基于属性驱动的基于IL的日志记录框架。(测试稍微复杂一点,但是...)

哦,还有IMO,如果您不使用属性,那么它不是AOP。在后处理器阶段进行方法进入/退出记录代码的重点是,它不会与您的代码文件混淆,因此您在重构代码时无需考虑它。这就是它的力量。

格雷格的一切都证明了保持愚蠢的愚蠢范式。


6
+1使其保持愚蠢状态。让我想起了爱因斯坦的名言:“使一切尽可能简单,但不要简单”。
宫阪丽

FWIW,F#具有相同的限制,每个方法最多接受一个参数。
R0MANARMY 2011年

1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;看起来需要两个参数,我想念什么?

2
@The Mouth -实际发生的事情是,concat "Hello, "您实际上正在创建一个函数,该函数采用just y,并且已x预定义为本地绑定为“ Hello”。如果可以看到此中间函数,则其外观类似于let concat_x y = "Hello, " + y。然后,您要致电concat_x "World!"。语法使它变得不那么明显,但这使您可以“烘焙”新功能-例如,let printstrln = print "%s\n" ;; printstrln "woof"。同样,即使您执行诸如之类的操作let f(x,y) = x + y,实际上也只是一个元组参数。
宫阪丽

1
我第一次在米兰达大学读书的时候就做过任何函数式编程,我不得不看看F#,这听起来很有趣。

8

天哪,那家伙真是令人难以忍受。我希望我只是阅读您问题中的代码,而不是看那个演讲。

如果仅出于使用AOP的考虑,我认为我永远不会使用这种方法。格雷格说这对简单的情况很好。这是我在简单情况下的处理方式:

public void DeactivateInventoryItem(CommandServices cs, Guid item, string reason)
{
    cs.Log.Write("Deactivated: {0} ({1})", item, reason);
    repo.Deactivate(item, reason);
}

是的,我做到了,我完全摆脱了AOP!为什么?因为在简单的情况下您不需要AOP

从函数编程的角度来看,每个函数只允许一个参数并没有真正吓到我。尽管如此,这确实不是一种可以与C#完美配合的设计-违反您的语言要求并不能满足您的任何要求。

仅在有必要建立命令模型的情况下才使用这种方法,例如,如果需要撤消堆栈或使用WPF Commands

否则,我将只使用框架或一些反思。PostSharp即使工作在Silverlight和Compact Framework的-所以他所谓的“魔法”还真是没那么神奇可言

我也不同意避免使用框架,以便能够向初级人员解释事物。这对他们没有任何好处。如果格雷格按照粗略的白痴一样对待他的下辈,那我怀疑他的高级开发者也不是很好,因为他们可能没有太多的机会在他们学习期间学习任何东西。大三


5

我在大学里对AOP做过独立研究。我实际上写了一篇关于使用Eclipse插件对AOP建模的方法的论文。我想那实际上是无关紧要的。关键点是:1)我还年轻且没有经验,以及2)我正在与AspectJ合作。我可以告诉你,大多数AOP框架的“魔力”并不那么复杂。实际上,我实际上是在尝试使用哈希表执行单参数方法的同时从事一个项目。IMO,单参数方法确实是一个框架并且具有侵入性。即使在这篇文章中,我也花了更多的时间来理解单参数方法,而不是回顾声明式方法。我要补充一点,我还没有看过电影,所以这种方法的“魔力”可能在于使用部分应用程序。

我想格雷格回答了你的问题。如果您认为自己处于花费过多时间向初级开发人员解释AOP框架的情况下,应该切换到这种方法。IMO,如果您在这条船上,则可能是在雇用错误的初级开发人员。我不认为AOP要求采用声明式方法,但是对我来说,从设计的角度来看,它更加清晰且无创。


+1表示“我比理解声明式方法花了更多时间来理解单参数方法。” 我发现该IConsume<T>示例对于完成的工作过于复杂。
Scott Whitlock

4

除非我遗漏了某些东西,否则您显示的代码是“责任链”设计模式,如果您需要在对象上对对象执行一系列操作(例如通过一系列命令处理程序的命令),该模式非常有用运行。

如果您在编译时知道要添加的行为,则使用PostSharp的AOP很好。PostSharp的代码编织几乎意味着运行时开销为零,并且确实使代码非常整洁(尤其是当您开始使用多播方面的东西时)。我认为PostSharp的基本用法并不特别复杂。PostSharp的缺点是它确实会显着增加编译时间。

我在生产代码中同时使用了这两种技术,尽管它们可以应用的地方有些重叠,但我认为大多数情况下,它们实际上是针对不同的场景。



4

格雷格描述的是绝对合理的。里面也有美丽。该概念适用于与纯面向对象不同的范例。它更多是一种过程方法或面向流程的设计方法。因此,如果您使用的是旧代码,则应用此概念将非常困难,因为可能需要进行大量重构。

我将尝试举另一个例子。也许不是很完美,但我希望它可以使观点更清楚。

因此,我们有一个使用存储库的产品服务(在这种情况下,我们将使用存根)。该服务将获取产品列表。

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public override string ToString() { return String.Format("{0}, {1}", Name, Price); }
}

public static class ProductService
{
    public static IEnumerable<Product> GetAllProducts(ProductRepositoryStub repository)
    {
        return repository.GetAll();
    }
}

public class ProductRepositoryStub
{
    public ProductRepositoryStub(string connStr) {}

    public IEnumerable<Product> GetAll()
    {
        return new List<Product>
        {
            new Product {Name = "Cd Player", Price = 49.99m},
            new Product {Name = "Yacht", Price = 2999999m }
        };
    }
}

当然,您也可以将接口传递给服务。

接下来,我们要在视图中显示产品列表。因此,我们需要一个接口

public interface Handles<T>
{
    void Handle(T message);
}

和一个保存产品列表的命令

public class ShowProductsCommand
{
    public IEnumerable<Product> Products { get; set; }
}

和视图

public class View : Handles<ShowProductsCommand>
{
    public void Handle(ShowProductsCommand cmd)
    {
        cmd.Products.ToList().ForEach(x => Console.WriteLine(x.ToString()));
    }
}

现在,我们需要一些执行所有这些操作的代码。我们将在名为Application的类中进行此操作。Run()方法是不包含或至少不包含业务逻辑的集成方法。依赖项作为方法注入到构造函数中。

public class Application
{
    private readonly Func<IEnumerable<Product>> _getAllProducts;
    private readonly Action<ShowProductsCommand> _showProducts;

    public Application(Func<IEnumerable<Product>> getAllProducts, Action<ShowProductsCommand> showProducts)
    {
        _getAllProducts = getAllProducts;
        _showProducts = showProducts;
    }

    public void Run()
    {
        var products = _getAllProducts();
        var cmd = new ShowProductsCommand { Products = products };
        _showProducts(cmd);
    }
}

最后,我们在main方法中编写应用程序。

static void Main(string[] args)
{
    // composition
    Func<IEnumerable<Product>> getAllProducts = () => ProductService.GetAllProducts(new ProductRepositoryStub(""));
    Action<ShowProductsCommand> showProducts = (x) => new View().Handle(x);
    var app = new Application(getAllProducts, showProducts);

    app.Run();
}

现在很酷的事情是,我们可以添加诸如日志记录或异常处理之类的内容,而无需接触现有代码,也无需框架或注释。对于异常处理,例如,我们只添加一个新类:

public class ExceptionHandler<T> : Handles<T>
{
    private readonly Handles<T> _next;

    public ExceptionHandler(Handles<T> next) { _next = next; }

    public void Handle(T message)
    {
        try
        {
            _next.Handle(message);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

然后,我们在应用程序的入口点将其插入到一起。我们甚至不必触摸Application类中的代码。我们只替换一行:

Action<ShowProductsCommand> showProducts = (x) => new ExceptionHandler<ShowProductsCommand>(new View()).Handle(x);

因此继续:当我们进行面向流程的设计时,可以通过在新类中添加功能来添加方面。然后,我们必须在合成方法中更改一行,仅此而已。

因此,我认为您的问题的答案是,您不能轻易地从一种方法转换为另一种方法,但必须决定在项目中将采用哪种架构方法。

编辑: 实际上,我刚刚意识到与产品服务一起使用的部分应用程序模式使事情变得更加复杂。我们需要在产品服务方法周围包装另一个类,以便也可以在此处添加方面。可能是这样的:

public class ProductQueries : Queries<IEnumerable<Product>>
{
    private readonly Func<IEnumerable<Product>> _query;

    public ProductQueries(Func<IEnumerable<Product>> query)
    {
        _query = query;
    }

    public IEnumerable<Product> Query()
    {
        return _query();
    }
}

public interface Queries<TResult>
{
    TResult Query();
}

然后必须像这样更改组成:

Func<IEnumerable<Product>> getAllProducts = () => ProductService.GetAllProducts(new ProductRepositoryStub(""));
Func<IEnumerable<Product>> queryAllProducts = new ProductQueries(getAllProducts).Query;
Action<ShowProductsCommand> showProducts = (x) => new ExceptionHandler<ShowProductsCommand>(new View()).Handle(x);
var app = new Application(queryAllProducts, showProducts);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.