it-swarm.com.de

Interception vs Injection: eine Entscheidung zur Framework-Architektur

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

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

Hier ist ein Beispiel für beides:

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 internen Werten einer Methode zu interagieren (z. B. zur Verwendung mit dem Cache-Dienst?), Wenn das Abfangen gewinnt? Kann ich dieses Verhalten auf andere Weise als durch Attribute implementieren?
  3. Oder gibt es vielleicht andere Lösungen, um das Problem zu lösen?
29
Beatles1692

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

Lassen Sie MyService im obigen Beispiel 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 völlig frei von Querschnittsthemen und folgt somit dem Single Responsibility Principle (SRP).

Um die Protokollierung anzuwenden, können Sie einen Protokollierungsdekorator 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 Dekorateur macht genau eine Sache, also folgen sie auch der 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());
39
Mark Seemann

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

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

6
Matthew Groves

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

2
sethcall

Ich habe dieses Problem oft gesehen 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 die Idee, eine ganze Bibliothek einzuschließen, gefiel mir nicht, nur um etwas zu tun, das ich mit (viel) einfachem Code erreichen konnte.

Ich bin dann den transparenten Proxy-Weg gegangen, der Spaß gemacht hat, aber die dynamische Ausgabe von IL zur Laufzeit beinhaltete und nicht das wäre, was ich in einer Produktionsumgebung tun möchte.

Ich habe mich kürzlich für die Verwendung von T4-Vorlagen entschieden, um das Dekorationsmuster zur Entwurfszeit automatisch zu implementieren. Es stellt sich heraus, dass die Arbeit mit T4-Vorlagen ziemlich schwierig ist, und ich musste dies schnell erledigen, also habe ich den folgenden Code erstellt. 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 namens LoggingTestAdapter, die ITestAdapter implementiert, veranlassen Sie Visual Studio, alle Methoden automatisch zu implementieren, und führen Sie sie dann über den obigen Code aus. Sie sollten 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);
        }
    }
}
1
JoeS