拦截与注入:框架架构决策


28

我正在帮助设计这个框架。使用某些通用组件应完成一些通用任务:特别是记录,缓存和引发事件。

我不确定是否最好使用依赖注入并将所有这些组件引入每个服务(例如属性),还是应该在每种服务方法上放置某种元数据并使用拦截来完成这些常见任务?

这是两个示例:

注射:

public class MyService
{
    public ILoggingService Logger { get; set; }

    public IEventBroker EventBroker { get; set; }

    public ICacheService Cache { get; set; }

    public void DoSomething()
    {
        Logger.Log(myMessage);
        EventBroker.Publish<EventType>();
        Cache.Add(myObject);
    }
}

这是另一个版本:

拦截:

public class MyService
{
    [Log("My message")]
    [PublishEvent(typeof(EventType))]
    public void DoSomething()
    {

    }
}

这是我的问题:

  1. 哪种解决方案最适合复杂的框架?
  2. 如果拦截获胜,与方法的内部值进行交互(例如与缓存服务一起使用)有什么选择?我可以使用其他方式而不是属性来实现此行为吗?
  3. 也许还有其他解决方案可以解决该问题?

2
我对1和2没有意见,但是关于3:考虑研究AoP(面向方面的编程),尤其是Spring.NET

只是为了澄清一下:您正在寻找相依注入和面向方面编程之间的比较,对吗?
M.Babcock 2012年

@ M.Babcock还没有看到这样的说法,但我自己这是正确的

Answers:


38

诸如日志记录,缓存之类的横切关注点不是依赖项,因此不应将其注入服务中。但是,尽管大多数人似乎都可以使用一个完整的交错AOP框架,但是有一个不错的设计模式: Decorator

在上面的示例中,让MyService实现IMyService接口:

public interface IMyService
{
    void DoSomething();
}

public class MyService : IMyService
{
    public void DoSomething()
    {
        // Implementation goes here...
    }
}

这使MyService类完全摆脱了横切关注点,因此遵循 单一职责原则(SRP)。

要应用日志记录,可以添加日志记录装饰器:

public class MyLogger : IMyService
{
    private readonly IMyService myService;
    private readonly ILoggingService logger;

    public MyLogger(IMyService myService, ILoggingService logger)
    {
        this.myService = myService;
        this.logger = logger;
    }

    public void DoSomething()
    {
        this.myService.DoSomething();
        this.logger.Log("something");
    }
}

您可以以相同的方式实现缓存,计数,事件等。每个装饰器仅做一件事,因此它们也遵循SRP,您可以用任意复杂的方式来组合它们。例如

var service = new MyLogger(
    new LoggingService(),
    new CachingService(
        new Cache(),
        new MyService());

5
装饰器模式是使这些问题分开的一种好方法,但是,如果您有很多服务,那么我将在其中使用PostSharp或Castle.DynamicProxy之类的AOP工具,否则对于每个服务类接口,我都必须对该类进行编码和一个记录器装饰器,每个装饰器都可能是非常相似的样板代码(即,您获得了改进的模块化/封装,但您仍然在重复很多)。
马修·格罗夫斯

4
同意 我给了一个报告,去年,介绍如何从装修移到AOP:channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/...
马克·西曼

我基于此程序
Dave Mateer

我们如何通过依赖注入来注入服务和装饰器?
TIKSN '16

@TIKSN的简短答案是:如上所示。但是,既然您在问,那么您必须在寻找其他问题的答案,但是我无法猜测那是什么。您能在网站上详细说明还是提出一个新问题?
Mark Seemann

6

对于少数服务,我认为Mark的答案很好:您无需学习或引入任何新的第三方依赖关系,并且仍将遵循良好的SOLID原则。

对于大量服务,我建议您使用AOP工具,例如PostSharp或Castle DynamicProxy。PostSharp具有免费(如啤酒)版本,并且他们最近才发布了用于诊断的PostSharp工具包(如啤酒和语音免费),它将为您提供一些现成的日志记录功能。


2

我发现框架的设计在很大程度上与这个问题正交-您应该首先关注框架的界面,也许作为背景的心理过程来考虑某人可能是如何实际使用它的。您不想做任何事情来阻止它被巧妙地使用,但是它应该仅仅是框架设计的输入。其中之一。


1

我已经多次面对这个问题,并且我想出了一个简单的解决方案。

最初,我使用装饰器模式并手动实现每种方法,当您拥有数百种方法时,这将变得非常乏味。

然后,我决定使用PostSharp,但是我不喜欢包含整个库只是为了做一些我可以用(很多)简单代码完成的事情的想法。

然后,我采用了透明的代理路由,这很有趣,但是涉及在运行时动态地发出IL,而这并不是我在生产环境中要做的事情。

我最近决定使用T4模板在设计时自动实现装饰器模式,事实证明T4模板实际上很难使用,我需要快速完成此工作,因此创建了以下代码。它既快速又肮脏(并且不支持属性),但希望有人会发现它有用。

这是代码:

        var linesToUse = code.Split(Environment.NewLine.ToCharArray()).Where(l => !string.IsNullOrWhiteSpace(l));
        string classLine = linesToUse.First();

        // Remove the first line this is just the class declaration, also remove its closing brace
        linesToUse = linesToUse.Skip(1).Take(linesToUse.Count() - 2);
        code = string.Join(Environment.NewLine, linesToUse).Trim()
            .TrimStart("{".ToCharArray()); // Depending on the formatting this may be left over from removing the class

        code = Regex.Replace(
            code,
            @"public\s+?(?'Type'[\w<>]+?)\s(?'Name'\w+?)\s*\((?'Args'[^\)]*?)\)\s*?\{\s*?(throw new NotImplementedException\(\);)",
            new MatchEvaluator(
                match =>
                    {
                        string start = string.Format(
                            "public {0} {1}({2})\r\n{{",
                            match.Groups["Type"].Value,
                            match.Groups["Name"].Value,
                            match.Groups["Args"].Value);

                        var args =
                            match.Groups["Args"].Value.Split(",".ToCharArray())
                                .Select(s => s.Trim().Split(" ".ToCharArray()))
                                .ToDictionary(s => s.Last(), s => s.First());

                        string call = "_decorated." + match.Groups["Name"].Value + "(" + string.Join(",", args.Keys) + ");";
                        if (match.Groups["Type"].Value != "void")
                        {
                            call = "return " + call;
                        }

                        string argsStr = args.Keys.Any(s => s.Length > 0) ? ("," + string.Join(",", args.Keys)) : string.Empty;
                        string loggedCall = string.Format(
                            "using (BuildLogger(\"{0}\"{1})){{\r\n{2}\r\n}}",
                            match.Groups["Name"].Value,
                            argsStr,
                            call);
                        return start + "\r\n" + loggedCall;
                    }));
        code = classLine.Trim().TrimEnd("{".ToCharArray()) + "\n{\n" + code + "\n}\n";

这是一个例子:

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

    void TestMethod3(List<string[]>  a, Object b);
}

然后创建一个名为LoggingTestAdapter的类,该类实现ITestAdapter,让Visual Studio自动实现所有方法,然后通过上面的代码运行它。然后,您应该具有以下内容:

public class LoggingTestAdapter : ITestAdapter
{

    public void Dispose()
    {
        using (BuildLogger("Dispose"))
        {
            _decorated.Dispose();
        }
    }
    public string TestMethod1()
    {
        using (BuildLogger("TestMethod1"))
        {
            return _decorated.TestMethod1();
        }
    }
    public IEnumerable<string> TestMethod2(int a)
    {
        using (BuildLogger("TestMethod2", a))
        {
            return _decorated.TestMethod2(a);
        }
    }
    public void TestMethod3(List<string[]> a, object b)
    {
        using (BuildLogger("TestMethod3", a, b))
        {
            _decorated.TestMethod3(a, b);
        }
    }
}

这就是支持代码:

public class DebugLogger : ILogger
{
    private Stopwatch _stopwatch;
    public DebugLogger()
    {
        _stopwatch = new Stopwatch();
        _stopwatch.Start();
    }
    public void Dispose()
    {
        _stopwatch.Stop();
        string argsStr = string.Empty;
        if (Args.FirstOrDefault() != null)
        {
            argsStr = string.Join(",",Args.Select(a => (a ?? (object)"null").ToString()));
        }

        System.Diagnostics.Debug.WriteLine(string.Format("{0}({1}) @ {2}ms", Name, argsStr, _stopwatch.ElapsedMilliseconds));
    }

    public string Name { get; set; }

    public object[] Args { get; set; }
}

public interface ILogger : IDisposable
{
    string Name { get; set; }
    object[] Args { get; set; }
}


public class LoggingTestAdapter<TLogger> : ITestAdapter where TLogger : ILogger,new()
{
    private readonly ITestAdapter _decorated;

    public LoggingTestAdapter(ITestAdapter toDecorate)
    {
        _decorated = toDecorate;
    }

    private ILogger BuildLogger(string name, params object[] args)
    {
        return new TLogger { Name = name, Args = args };
    }

    public void Dispose()
    {
        _decorated.Dispose();
    }

    public string TestMethod1()
    {
        using (BuildLogger("TestMethod1"))
        {
            return _decorated.TestMethod1();
        }
    }
    public IEnumerable<string> TestMethod2(int a)
    {
        using (BuildLogger("TestMethod2", a))
        {
            return _decorated.TestMethod2(a);
        }
    }
    public void TestMethod3(List<string[]> a, object b)
    {
        using (BuildLogger("TestMethod3", a, b))
        {
            _decorated.TestMethod3(a, b);
        }
    }
}
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.