Interception vs Injection: eine Entscheidung zur Framework-Architektur

28

Es gibt diesen Rahmen, den ich mitgestalte. Es gibt einige allgemeine Aufgaben, die mit einigen allgemeinen Komponenten ausgeführt werden sollten: Protokollieren, Zwischenspeichern und Auslösen von Ereignissen im Besonderen.

Ich bin mir nicht sicher, ob es besser ist, die Abhängigkeitsinjektion zu verwenden und alle diese Komponenten für jeden Dienst einzuführen (z. B. als Eigenschaften), oder ob ich über jede Methode meiner Dienste Metadaten platzieren und diese allgemeinen Aufgaben mithilfe von Interception ausführen soll ?

Hier ist ein Beispiel für beide:

Injektion:

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);
    }
}

und hier ist die andere Version:

Abfangen:

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

    }
}

Hier sind meine Fragen:

  1. Welche Lösung eignet sich am besten für ein kompliziertes Framework?
  2. Welche Möglichkeiten habe ich, um mit den internen Werten einer Methode zu interagieren, wenn das Abfangen erfolgreich ist (z. B. mit dem Cache-Service)? Kann ich dieses Verhalten auf andere Weise als mit Attributen implementieren?
  3. Oder gibt es vielleicht andere Lösungen, um das Problem zu lösen?
Beatles1692
quelle
2
Ich habe keine Meinung zu 1 und 2, aber zu 3: Betrachten Sie AoP ( aspektorientierte Programmierung ) und speziell Spring.NET .
Nur zur Verdeutlichung: Sie suchen einen Vergleich zwischen Abhängigkeitsinjektion und aspektorientierter Programmierung, richtig?
M.Babcock
@ M.Babcock Habe es selbst nicht so gesehen, aber das ist richtig

Antworten:

38

Querschnittsthemen wie Protokollierung, Zwischenspeicherung usw. sind keine Abhängigkeiten und sollten daher nicht in Dienste eingebunden werden. Während die meisten Leute dann nach einem vollständigen Interleaving-AOP-Framework zu greifen scheinen, gibt es dafür ein schönes Entwurfsmuster: Decorator .

Lassen Sie im obigen Beispiel MyService die IMyService-Schnittstelle implementieren:

public interface IMyService
{
    void DoSomething();
}

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

Dies hält die MyService-Klasse vollständig frei von übergreifenden Bedenken und folgt somit dem Single Responsibility Principle (SRP).

Um die Protokollierung anzuwenden, können Sie einen Protokollierungs-Decorator hinzufügen:

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");
    }
}

Sie können Caching, Metering, Eventing usw. auf die gleiche Weise implementieren. Jeder Decorator macht genau eins, folgt also auch dem SRP, und Sie können sie auf beliebig komplexe Weise zusammenstellen. Z.B

var service = new MyLogger(
    new LoggingService(),
    new CachingService(
        new Cache(),
        new MyService());
Mark Seemann
quelle
5
Das Dekorationsmuster ist eine großartige Möglichkeit, um diese Bedenken zu trennen. Wenn Sie jedoch viele Services haben, würde ich ein AOP-Tool wie PostSharp oder Castle.DynamicProxy verwenden. Andernfalls muss ich für jede Serviceklassenschnittstelle die Klasse codieren UND ein Logger-Dekorateur, und jeder dieser Dekorateure könnte möglicherweise sehr ähnlich sein (dh Sie erhalten eine verbesserte Modularisierung / Kapselung, wiederholen sich aber immer noch häufig).
Matthew Groves
4
Einverstanden. Ich habe letztes Jahr einen Vortrag gehalten, der beschreibt, wie man von Decorators zu AOP wechselt: channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/…
Mark Seemann
Ich habe eine einfache Implementierung basierend auf dieser programgood.net/2015/09/08/DecoratorSpike.aspx
Dave Mateer
Wie können wir Dienstleister und Dekorateure mit Abhängigkeitsinjektion versorgen?
TIKSN
@TIKSN Die kurze Antwort lautet: wie oben gezeigt . Da Sie jedoch fragen, müssen Sie eine Antwort auf etwas anderes suchen, aber ich kann nicht erraten, was das ist. Könnten Sie etwas näher erläutern oder hier auf der Website eine neue Frage stellen?
Mark Seemann
6

Für eine Handvoll Services finde ich Marks Antwort gut: Sie müssen keine neuen Abhängigkeiten von Drittanbietern lernen oder einführen und folgen weiterhin guten SOLID-Grundsätzen.

Für eine große Anzahl von Diensten würde ich ein AOP-Tool wie PostSharp oder Castle DynamicProxy empfehlen. PostSharp hat eine kostenlose (wie in Bier) Version, und sie haben kürzlich das PostSharp Toolkit for Diagnostics veröffentlicht (kostenlos wie in Bier UND Sprache), mit dem Sie sofort einige Protokollierungsfunktionen nutzen können.

Matthew Groves
quelle
2

Ich finde das Design eines Frameworks weitgehend orthogonal zu dieser Frage - Sie sollten sich zuerst auf die Schnittstelle Ihres Frameworks konzentrieren und als Hintergrund den mentalen Prozess überlegen, wie jemand es tatsächlich konsumieren könnte. Sie möchten nicht etwas tun, das verhindert , dass es auf clevere Weise verwendet wird, sondern es sollte nur eine Eingabe in Ihr Framework-Design sein. einer unter vielen.


quelle
1

Ich habe mich oft mit diesem Problem auseinandergesetzt und denke, dass ich eine einfache Lösung gefunden habe.

Anfangs habe ich mich für das Dekorationsmuster entschieden und jede Methode manuell implementiert. Wenn Sie Hunderte von Methoden haben, wird dies sehr mühsam.

Ich habe mich dann für PostSharp entschieden, aber mir gefiel die Idee nicht, eine ganze Bibliothek einzubinden, um etwas zu tun, das ich mit (viel) einfachem Code erreichen konnte.

Ich bin dann die Route des transparenten Proxys gegangen, was Spaß machte, aber beinhaltete, IL zur Laufzeit dynamisch zu emittieren und nicht etwas zu sein, was ich in einer Produktionsumgebung tun möchte.

Ich habe vor kurzem beschlossen, T4-Vorlagen zu verwenden, um das Dekorationsmuster zur Entwurfszeit automatisch zu implementieren. Es hat sich herausgestellt, dass T4-Vorlagen tatsächlich sehr schwer zu bearbeiten sind, und ich musste dies schnell erledigen, sodass ich den folgenden Code erstellte. Es ist schnell und schmutzig (und es unterstützt keine Eigenschaften), aber hoffentlich findet es jemand nützlich.

Hier ist der Code:

        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";

Hier ist ein Beispiel:

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

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

Erstellen Sie dann eine Klasse mit dem Namen LoggingTestAdapter, die ITestAdapter implementiert. Lassen Sie Visual Studio alle Methoden automatisch implementieren und führen Sie sie dann über den obigen Code aus. Du solltest dann so etwas haben:

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);
        }
    }
}

Dies ist es mit dem unterstützenden Code:

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);
        }
    }
}
JoeS
quelle