Intercettazione vs iniezione: una decisione di architettura del quadro


28

C'è questo quadro che sto aiutando a progettare. Esistono alcune attività comuni che devono essere eseguite utilizzando alcuni componenti comuni: registrazione, memorizzazione nella cache e raccolta di eventi in particolare.

Non sono sicuro se è meglio usare l'iniezione di dipendenza e introdurre tutti questi componenti in ciascun servizio (come proprietà per esempio) o dovrei avere qualche tipo di metadati posizionati su ogni metodo dei miei servizi e usare l'intercettazione per fare queste attività comuni ?

Ecco un esempio di entrambi:

Iniezione:

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

ed ecco l'altra versione:

intercettazione:

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

    }
}

Ecco le mie domande:

  1. Qual è la soluzione migliore per un framework complicato?
  2. Se l'intercettazione vince, quali sono le mie opzioni per interagire con i valori interni di un metodo (da utilizzare ad esempio con il servizio cache?)? Posso usare altri modi piuttosto che attributi per implementare questo comportamento?
  3. O forse ci possono essere altre soluzioni per risolvere il problema?

2
Non ho un'opinione su 1 e 2, ma riguardo a 3: considera di esaminare AoP ( programmazione orientata agli aspetti ) e in particolare Spring.NET .

Giusto per chiarire: stai cercando un confronto tra Iniezione delle dipendenze e Programmazione orientata agli aspetti, giusto?
M.Babcock

@ M.Babcock Non l'ho visto in questo modo, ma è corretto

Risposte:


38

Preoccupazioni trasversali come la registrazione, la memorizzazione nella cache ecc. Non sono dipendenze, pertanto non devono essere inserite nei servizi. Tuttavia, mentre la maggior parte delle persone sembra raggiungere un framework AOP interleaving completo, c'è un bel modello di design per questo: Decorator .

Nell'esempio sopra, consenti a MyService di implementare l'interfaccia IMyService:

public interface IMyService
{
    void DoSomething();
}

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

Ciò mantiene la classe MyService completamente libera da preoccupazioni trasversali, seguendo quindi il principio della responsabilità singola (SRP).

Per applicare la registrazione, puoi aggiungere un decoratore di registrazione:

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

È possibile implementare la memorizzazione nella cache, la misurazione, gli eventi, ecc. Allo stesso modo. Ogni decoratore fa esattamente una cosa, quindi segue anche l'SRP e puoi comporli in modi arbitrariamente complessi. Per esempio

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

5
Il modello di decorazione è un ottimo modo per tenere separate queste preoccupazioni, ma se hai MOLTI servizi, è lì che utilizzerei uno strumento AOP come PostSharp o Castle.DynamicProxy, altrimenti per ogni interfaccia della classe di servizio, devo codificare la classe E un decoratore di logger, e ciascuno di questi decoratori potrebbe essere potenzialmente un codice di caldaia molto simile (cioè ottieni una migliore modularizzazione / incapsulamento, ma ti stai ancora ripetendo molto).
Matthew Groves,

4
Concordato. Ho tenuto un discorso l'anno scorso che descrive come passare da Decoratori ad AOP: channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/…
Mark Seemann

Ho codificato una semplice implementazione basata su questo programmagood.net/2015/09/08/DecoratorSpike.aspx
Dave Mateer

Come possiamo iniettare servizi e decoratori con iniezione di dipendenza?
TIKSN,

@TIKSN La risposta breve è: come mostrato sopra . Dato che stai chiedendo, tuttavia, devi cercare una risposta a qualcos'altro, ma non riesco a indovinare di cosa si tratta. Potresti elaborare o forse porre una nuova domanda qui sul sito?
Mark Seemann,

6

Per una manciata di servizi, penso che la risposta di Mark sia buona: non dovrai imparare o introdurre nuove dipendenze di terze parti e seguirai comunque buoni principi SOLIDI.

Per una grande quantità di servizi, consiglierei uno strumento AOP come PostSharp o Castle DynamicProxy. PostSharp ha una versione gratuita (come nella birra) e recentemente ha rilasciato PostSharp Toolkit for Diagnostics (gratuito come nella birra E nel parlato) che ti darà alcune funzionalità di registrazione pronte all'uso .


2

Trovo che la progettazione di un framework sia in gran parte ortogonale a questa domanda: dovresti prima concentrarti sull'interfaccia del tuo framework e forse come processo mentale di fondo considerare come qualcuno potrebbe effettivamente consumarlo. Non vuoi fare qualcosa che ne impedisca l'utilizzo in modi intelligenti, ma dovrebbe essere solo un input nella progettazione del tuo framework; uno tra i tanti.


1

Ho affrontato questo problema molte volte e penso di aver trovato una soluzione semplice.

Inizialmente sono andato con il modello di decorazione e ho implementato manualmente ogni metodo, quando hai centinaia di metodi questo diventa molto noioso.

Ho quindi deciso di utilizzare PostSharp ma non mi piaceva l'idea di includere un'intera libreria solo per fare qualcosa che potevo realizzare con (molto) codice semplice.

Ho quindi seguito la route proxy trasparente che è stata divertente ma ha comportato l'emissione dinamica di IL in fase di esecuzione e non sarebbe qualcosa che avrei voluto fare in un ambiente di produzione.

Di recente ho deciso di utilizzare i modelli T4 per implementare automaticamente il modello di decorazione in fase di progettazione, si scopre che i modelli T4 sono in realtà abbastanza difficili da lavorare e ne avevo bisogno rapidamente, quindi ho creato il codice qui sotto. È veloce e sporco (e non supporta le proprietà) ma si spera che qualcuno lo troverà utile.

Ecco il codice:

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

Ecco un esempio:

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

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

Quindi creare una classe chiamata LoggingTestAdapter che implementa ITestAdapter, ottenere Visual Studio per implementare automaticamente tutti i metodi e quindi eseguirlo attraverso il codice sopra. Dovresti quindi avere qualcosa del genere:

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

Questo è con il codice di supporto:

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);
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.