Interception vs Injection: une décision d'architecture cadre

28

Il y a ce cadre que j'aide à concevoir. Certaines tâches courantes doivent être effectuées à l'aide de composants communs: la journalisation, la mise en cache et le déclenchement d'événements en particulier.

Je ne sais pas s'il est préférable d'utiliser l'injection de dépendances et d'introduire tous ces composants dans chaque service (comme propriétés par exemple) ou dois-je disposer d'une sorte de métadonnées sur chaque méthode de mes services et utiliser l'interception pour effectuer ces tâches courantes ?

Voici un exemple des deux:

Injection:

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

et voici l'autre version:

Interception:

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

    }
}

Voici mes questions:

  1. Quelle solution est la meilleure pour un cadre complexe?
  2. Si l'interception réussit, quelles sont mes options pour interagir avec les valeurs internes d'une méthode (à utiliser avec le service de cache par exemple?)? Puis-je utiliser d'autres méthodes plutôt que des attributs pour implémenter ce comportement?
  3. Ou peut-être qu'il peut y avoir d'autres solutions pour résoudre le problème?
Beatles1692
la source
2
Je n'ai pas d'opinion sur 1 et 2, mais concernant 3: envisagez d'examiner AoP ( programmation orientée aspect ) et plus particulièrement Spring.NET .
Juste pour clarifier: vous cherchez une comparaison entre l'injection de dépendance et la programmation orientée aspect, n'est-ce pas?
M.Babcock
@ M.Babcock Je ne l'ai pas vu de cette façon moi-même mais c'est correct

Réponses:

38

Les préoccupations transversales telles que la journalisation, la mise en cache, etc. ne sont pas des dépendances, elles ne doivent donc pas être injectées dans les services. Cependant, alors que la plupart des gens semblent alors rechercher un cadre AOP entrelacé complet, il existe un joli modèle de conception pour cela: le décorateur .

Dans l'exemple ci-dessus, laissez MyService implémenter l'interface IMyService:

public interface IMyService
{
    void DoSomething();
}

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

Cela maintient la classe MyService complètement exempte de préoccupations transversales, suivant ainsi le principe de responsabilité unique (SRP).

Pour appliquer la journalisation, vous pouvez ajouter un décorateur de journalisation:

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

Vous pouvez implémenter la mise en cache, la mesure, les événements, etc. de la même manière. Chaque décorateur fait exactement une chose, ils suivent donc également le SRP, et vous pouvez les composer de manière arbitrairement complexe. Par exemple

var service = new MyLogger(
    new LoggingService(),
    new CachingService(
        new Cache(),
        new MyService());
Mark Seemann
la source
5
Le modèle de décorateur est un excellent moyen de séparer ces préoccupations, mais si vous avez BEAUCOUP de services, c'est là que j'utiliserais un outil AOP comme PostSharp ou Castle.DynamicProxy, sinon pour chaque interface de classe de service, je dois coder la classe ET un décorateur d'enregistreur, et chacun de ces décorateurs pourrait potentiellement être un code passe-partout très similaire (c'est-à-dire que vous obtenez une modularisation / encapsulation améliorée, mais vous vous répétez toujours beaucoup).
Matthew Groves
4
D'accord. L'année dernière, j'ai donné une conférence qui décrit comment passer de Décorateurs à AOP: channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/…
Mark Seemann
J'ai codé une implémentation simple basée sur ce programmegood.net/2015/09/08/DecoratorSpike.aspx
Dave Mateer
Comment injecter des services et des décorateurs avec une injection de dépendance?
TIKSN
@TIKSN La réponse courte est: comme indiqué ci-dessus . Puisque vous posez la question, cependant, vous devez chercher une réponse à autre chose, mais je ne peux pas deviner ce que c'est. Pourriez-vous élaborer ou peut-être poser une nouvelle question ici sur le site?
Mark Seemann
6

Pour une poignée de services, je pense que la réponse de Mark est bonne: vous n'aurez pas à apprendre ou à introduire de nouvelles dépendances tierces et vous suivrez toujours de bons principes SOLID.

Pour une grande quantité de services, je recommanderais un outil AOP comme PostSharp ou Castle DynamicProxy. PostSharp a une version gratuite (comme dans la bière), et ils ont récemment publié PostSharp Toolkit for Diagnostics , (gratuit comme dans la bière ET la parole) qui vous donnera des fonctionnalités de journalisation prêtes à l'emploi .

Matthew Groves
la source
2

Je trouve que la conception d'un cadre est en grande partie orthogonale à cette question - vous devez d'abord vous concentrer sur l'interface de votre cadre, et peut-être en tant que processus mental d'arrière-plan, pensez à la façon dont quelqu'un pourrait réellement le consommer. Vous ne voulez pas faire quelque chose qui l' empêche d'être utilisé de manière intelligente, mais cela ne devrait être qu'une entrée dans la conception de votre framework; un parmi tant d'autres.


la source
1

J'ai rencontré ce problème à maintes reprises et je pense avoir trouvé une solution simple.

Au départ, je suis allé avec le modèle de décorateur et j'ai implémenté manuellement chaque méthode, lorsque vous avez des centaines de méthodes, cela devient très fastidieux.

J'ai alors décidé d'utiliser PostSharp mais je n'aimais pas l'idée d'inclure une bibliothèque entière juste pour faire quelque chose que je pouvais accomplir avec (beaucoup) de code simple.

J'ai ensuite emprunté la route proxy transparente qui était amusante mais impliquait l'émission dynamique d'IL au moment de l'exécution et ne serait pas quelque chose que je voudrais faire dans un environnement de production.

J'ai récemment décidé d'utiliser des modèles T4 pour implémenter automatiquement le modèle de décorateur au moment de la conception, il s'avère que les modèles T4 sont en fait assez difficiles à travailler et j'avais besoin que cela soit fait rapidement, j'ai donc créé le code ci-dessous. C'est rapide et sale (et il ne prend pas en charge les propriétés) mais j'espère que quelqu'un le trouvera utile.

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

Voici un exemple:

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

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

Créez ensuite une classe appelée LoggingTestAdapter qui implémente ITestAdapter, obtenez Visual Studio pour implémenter automatiquement toutes les méthodes, puis exécutez-le via le code ci-dessus. Vous devriez alors avoir quelque chose comme ceci:

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

C'est tout avec le code de support:

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
la source