Interceptação versus injeção: uma decisão de arquitetura de estrutura


28

Existe essa estrutura que estou ajudando a projetar. Existem algumas tarefas comuns que devem ser executadas usando alguns componentes comuns: Registrando, Armazenando em Cache e levantando eventos em particular.

Não tenho certeza se é melhor usar a injeção de dependência e apresentar todos esses componentes a cada serviço (como propriedades, por exemplo) ou devo colocar algum tipo de metadados sobre cada método dos meus serviços e usar interceptação para executar essas tarefas comuns ?

Aqui está um exemplo de ambos:

Injeção:

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

e aqui está a outra versão:

Interceptação:

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

    }
}

Aqui estão as minhas perguntas:

  1. Qual solução é melhor para uma estrutura complicada?
  2. Se a interceptação vencer, quais são minhas opções para interagir com os valores internos de um método (para usar com o serviço de cache, por exemplo?)? Posso usar outras maneiras, em vez de atributos, para implementar esse comportamento?
  3. Ou talvez haja outras soluções para resolver o problema?

2
Eu não tenho uma opinião sobre 1 e 2, mas sobre 3: considere olhar para o AoP ( programação orientada a aspectos ) e especificamente para o Spring.NET .

Só para esclarecer: você está procurando uma comparação entre Injeção de Dependência e Programação Orientada a Aspectos, correto?
M.Babcock

@ M.Babcock não vi isso dessa maneira mim, mas isso é correto

Respostas:


38

Preocupações transversais como registro em log, armazenamento em cache etc. não são dependências, portanto não devem ser injetadas nos serviços. No entanto, embora a maioria das pessoas pareça alcançar uma estrutura de AOP intercalada completa, há um bom padrão de design para isso: Decorator .

No exemplo acima, deixe o MyService implementar a interface IMyService:

public interface IMyService
{
    void DoSomething();
}

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

Isso mantém a classe MyService completamente livre de preocupações transversais, seguindo o Princípio de responsabilidade única (SRP).

Para aplicar o log, você pode adicionar um Decorator de log:

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

Você pode implementar armazenamento em cache, medição, eventos, etc. da mesma maneira. Cada decorador faz exatamente uma coisa, então eles também seguem o SRP e você pode compor de maneiras arbitrariamente complexas. Por exemplo

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

5
O padrão do decorador é uma ótima maneira de manter essas preocupações separadas, mas se você tiver muitos serviços, é nesse local que eu usaria uma ferramenta de AOP como PostSharp ou Castle.DynamicProxy; caso contrário, para cada interface de classe de serviço, tenho que codificar a classe E um decorador de logger, e cada um desses decoradores pode ser um código clichê muito semelhante (ou seja, você obtém uma modularização / encapsulamento aprimorada, mas ainda está se repetindo bastante).
Matthew Groves

4
Acordado. Eu dei uma palestra no ano passado que descreve como passar de decoradores para AOP: channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/...
Mark Seemann

I codificado de uma implementação simples com base nesta programgood.net/2015/09/08/DecoratorSpike.aspx
Dave Mateer

Como podemos injetar serviços e decoradores com injeção de dependência?
TIKSN

@TIKSN A resposta curta é: como mostrado acima . Como você está perguntando, no entanto, você deve estar procurando uma resposta para outra coisa, mas não consigo adivinhar o que é isso. Você poderia elaborar, ou talvez fazer uma nova pergunta aqui no site?
58568 Mark Marlene

6

Para alguns serviços, acho que a resposta de Mark é boa: você não precisará aprender ou introduzir novas dependências de terceiros e ainda seguirá os bons princípios do SOLID.

Para uma grande quantidade de serviços, eu recomendaria uma ferramenta de AOP como PostSharp ou Castle DynamicProxy. O PostSharp possui uma versão gratuita (como na cerveja) e eles lançaram recentemente o PostSharp Toolkit for Diagnostics (gratuito como na cerveja E fala), que fornecerá alguns recursos de registro prontos para uso.


2

Acho que o design de uma estrutura é amplamente ortogonal a essa pergunta - você deve se concentrar primeiro na interface da sua estrutura e, talvez, como processo mental de fundo, considere como alguém pode realmente consumi-la. Você não deseja fazer algo que impeça que seja usado de maneira inteligente, mas deve ser apenas uma entrada para o design da estrutura; um entre muitos.


1

Já enfrentei esse problema muitas vezes e acho que encontrei uma solução simples.

Inicialmente, segui o padrão decorador e implementei manualmente cada método, quando você tem centenas de métodos, isso se torna muito entediante.

Decidi então usar o PostSharp, mas não gostei da ideia de incluir uma biblioteca inteira apenas para fazer algo que eu pudesse realizar com (muito) código simples.

Depois, segui a rota do proxy transparente que era divertida, mas envolvia a emissão dinâmica de IL em tempo de execução e não seria algo que eu gostaria de fazer em um ambiente de produção.

Recentemente, decidi usar modelos T4 para implementar automaticamente o padrão do decorador em tempo de design. Acontece que os modelos T4 são realmente muito difíceis de trabalhar e eu precisava disso rapidamente, então criei o código abaixo. É rápido e sujo (e não suporta propriedades), mas espero que alguém o ache útil.

Aqui está o código:

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

Aqui está um exemplo:

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

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

Em seguida, crie uma classe chamada LoggingTestAdapter que implemente o ITestAdapter, faça com que o visual studio implemente automaticamente todos os métodos e execute-o pelo código acima. Você deve ter algo parecido com isto:

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

É isso com o código de suporte:

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);
        }
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.