Перехват против Инъекции: решение архитектуры платформы

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. Или, может быть, могут быть другие решения для решения проблемы?
Beatles1692
источник
2
У меня нет мнения на 1 и 2, но в отношении 3: рассмотреть глядя в АОП ( аспектно-ориентированное программирование ) и , в частности в Spring.NET .
Просто чтобы уточнить: вы ищете сравнение между внедрением зависимостей и аспектно-ориентированным программированием, верно?
М.Бабкок
@ M.Babcock Сам такого не видел, но это правильно

Ответы:

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
Шаблон декоратора - отличный способ разделить эти проблемы, но если у вас много сервисов, я бы использовал инструмент AOP, такой как PostSharp или Castle.DynamicProxy, в противном случае для каждого интерфейса класса сервиса мне нужно кодировать класс. И декоратор логгера, и каждый из этих декораторов потенциально может быть очень похож на стандартный код (т.е. вы получаете улучшенную модульность / инкапсуляцию, но вы все еще много повторяете себя).
Мэтью Гроувс
4
Согласовано. В прошлом году я выступал с докладом, в котором описывается, как перейти от декораторов к AOP: channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/…
Марк Симанн
Как мы можем внедрить сервис и декораторы с внедрением зависимостей?
TIKSN
@TIKSN Короткий ответ: как показано выше . Поскольку вы спрашиваете, однако, вы должны искать ответ на что-то еще, но я не могу догадаться, что это такое. Не могли бы вы уточнить, или, возможно, задать новый вопрос здесь, на сайте?
Марк Симанн
6

Для небольшого числа сервисов я думаю, что ответ Марка хороший: вам не придется изучать или вводить какие-либо новые сторонние зависимости, и вы все равно будете следовать хорошим принципам SOLID.

Для большого количества сервисов я бы порекомендовал инструмент AOP, такой как PostSharp или Castle DynamicProxy. PostSharp имеет бесплатную (как в пиве) версию, и они только недавно выпустили PostSharp Toolkit for Diagnostics (бесплатно, как в пиве и речи), которая предоставит вам некоторые функции регистрации из коробки.

Мэтью Гровс
источник
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);
        }
    }
}
Джос
источник