Intercettazione vs Iniezione: una decisione di architettura quadro

27

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

Non sono sicuro che sia meglio usare l'integrazione delle dipendenze e introdurre tutti questi componenti in ogni servizio (come le proprietà per esempio) o dovrei avere qualche tipo di metadati collocato su ciascun metodo dei miei servizi e usare l'intercettazione per fare questi compiti 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. Quale soluzione è la migliore per un quadro complicato?
  2. Se l'intercettazione vince, quali sono le opzioni per interagire con i valori interni di un metodo (da utilizzare con il servizio cache, ad esempio?)? Posso usare altri modi piuttosto che attributi per implementare questo comportamento?
  3. O forse ci possono essere altre soluzioni per risolvere il problema?
posta Beatles1692 10.03.2012 - 01:01
fonte

4 risposte

37

Preoccupazioni trasversali come la registrazione, il caching, ecc. non sono delle dipendenze, quindi non dovrebbero essere iniettati nei servizi. Tuttavia, mentre la maggior parte delle persone sembra raggiungere un framework AOP interleaving completo, esiste un modello di design piacevole per questo: Decorator .

Nell'esempio precedente, lascia che MyService implementi l'interfaccia IMyService:

public interface IMyService
{
    void DoSomething();
}

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

Ciò mantiene la classe MyService completamente priva di preoccupazioni trasversali, seguendo quindi il principio di singola responsabilità (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");
    }
}

Puoi implementare la memorizzazione nella cache, la misurazione, l'eventing, ecc. allo stesso modo. Ogni decoratore fa esattamente una cosa, quindi seguono anche l'SRP e puoi comporli in modi arbitrariamente complessi. Per es.

var service = new MyLogger(
    new LoggingService(),
    new CachingService(
        new Cache(),
        new MyService());
    
risposta data 10.03.2012 - 11:45
fonte
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 i buoni principi SOLID.

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 AND speech) che ti fornirà alcune funzionalità di registrazione fuori dalla scatola.

    
risposta data 12.03.2012 - 18:31
fonte
2

Trovo che il design di un framework sia in gran parte ortogonale a questa domanda - dovresti concentrarti prima sull'interfaccia del tuo framework, e forse come un processo mentale di base considerare come qualcuno potrebbe effettivamente consumarlo. Non si vuole fare qualcosa che impedisce di essere usato in modi intelligenti, ma dovrebbe essere solo un input nella progettazione del framework; uno tra tanti.

    
risposta data 10.03.2012 - 01:19
fonte
0

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

Inizialmente sono andato con il pattern di decoratore e ho implementato manualmente ciascun 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 avrei potuto realizzare con (molto) codice semplice.

Poi sono andato lungo il percorso del proxy trasparente che è stato divertente ma ha comportato l'emissione dinamica di IL in fase di esecuzione e non sarebbe stato qualcosa che avrei voluto fare in un ambiente di produzione.

Recentemente ho deciso di utilizzare i modelli T4 per implementare automaticamente il pattern di decoratore in fase di progettazione, risulta che i modelli T4 sono in realtà abbastanza difficili da lavorare e ho avuto bisogno di farlo rapidamente, quindi ho creato il codice seguente. È veloce e sporco (e non supporta le proprietà) ma spero che qualcuno lo trovi 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 di simile a questo:

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 è 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);
        }
    }
}
    
risposta data 03.12.2015 - 14:39
fonte

Leggi altre domande sui tag