Перехоплення проти ін'єкції: рішення архітектури рамки


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 Я цього не бачив так, але це правильно

Відповіді:


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 для діагностики (вільний, як у пиві І мова), який надасть вам деякі функції реєстрації поза коробкою.


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, отримайте візуальну студію для автоматичної реалізації всіх методів, а потім запустіть її за допомогою коду вище. Тоді у вас повинно бути щось подібне:

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);
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.