Intercepción vs inyección: una decisión de arquitectura marco

28

Existe este marco que estoy ayudando a diseñar. Hay algunas tareas comunes que deben realizarse utilizando algunos componentes comunes: registro, almacenamiento en caché y eventos de elevación en particular.

No estoy seguro de si es mejor usar la inyección de dependencia e introducir todos estos componentes en cada servicio (como propiedades, por ejemplo) o si debería colocar algún tipo de metadatos sobre cada método de mis servicios y utilizar la intercepción para realizar estas tareas comunes ?

Aquí hay un ejemplo de ambos:

Inyección:

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

y aquí está la otra versión:

Interceptación:

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

    }
}

Aquí están mis preguntas:

  1. ¿Qué solución es mejor para un marco complicado?
  2. Si la intercepción gana, ¿cuáles son mis opciones para interactuar con los valores internos de un método (para usar con el servicio de caché, por ejemplo)? ¿Puedo usar otras formas en lugar de atributos para implementar este comportamiento?
  3. ¿O tal vez puede haber otras soluciones para resolver el problema?
Beatles1692
fuente
2
No tengo una opinión sobre 1 y 2, pero con respecto a 3: considere buscar en AoP ( programación orientada a aspectos ) y específicamente en Spring.NET .
Solo para aclarar: está buscando una comparación entre la inyección de dependencia y la programación orientada a aspectos, ¿correcto?
M.Babcock
@ M.Babcock No lo he visto así, pero eso es correcto

Respuestas:

38

Las preocupaciones transversales como el registro, el almacenamiento en caché, etc. no son dependencias, por lo que no deben inyectarse en los servicios. Sin embargo, aunque la mayoría de las personas parecen alcanzar un marco de AOP entrelazado completo, hay un buen patrón de diseño para esto: Decorador .

En el ejemplo anterior, deje que MyService implemente la interfaz IMyService:

public interface IMyService
{
    void DoSomething();
}

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

Esto mantiene a la clase MyService completamente libre de preocupaciones transversales, siguiendo así el Principio de responsabilidad única (SRP).

Para aplicar el registro, puede agregar un Decorador de registro:

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

Puede implementar el almacenamiento en caché, la medición, los eventos, etc. de la misma manera. Cada decorador hace exactamente una cosa, por lo que también siguen el SRP, y puede componerlos de formas arbitrariamente complejas. P.ej

var service = new MyLogger(
    new LoggingService(),
    new CachingService(
        new Cache(),
        new MyService());
Mark Seemann
fuente
55
El patrón de decorador es una excelente manera de mantener esas preocupaciones separadas, pero si tiene MUCHOS servicios, ahí es donde usaría una herramienta AOP como PostSharp o Castle.DynamicProxy, de lo contrario para cada interfaz de clase de servicio, tengo que codificar la clase Y un decorador de registrador, y cada uno de esos decoradores podría ser un código repetitivo muy similar (es decir, obtiene una modularización / encapsulación mejorada, pero aún se repite mucho).
Matthew Groves
44
Convenido. Di
Mark Seemann
Codifiqué
Dave Mateer
¿Cómo podemos inyectar servicio y decoradores con inyección de dependencia?
TIKSN
@TIKSN La respuesta corta es: como se muestra arriba . Sin embargo, como está preguntando, debe estar buscando una respuesta a otra cosa, pero no puedo adivinar qué es eso. ¿Podría dar más detalles o tal vez hacer una nueva pregunta aquí en el sitio?
Mark Seemann
6

Para un puñado de servicios, creo que la respuesta de Mark es buena: no tendrá que aprender o introducir nuevas dependencias de terceros y aún seguirá buenos principios SÓLIDOS.

Para una gran cantidad de servicios, recomendaría una herramienta AOP como PostSharp o Castle DynamicProxy. PostSharp tiene una versión gratuita (como en cerveza), y recientemente lanzaron PostSharp Toolkit for Diagnostics , (gratis como en cerveza Y discurso) que le dará algunas características de registro listas para usar .

Matthew Groves
fuente
2

Creo que el diseño de un marco es en gran parte ortogonal a esta pregunta: primero debe centrarse en la interfaz de su marco y tal vez como un proceso mental de fondo considere cómo alguien realmente podría consumirlo. No desea hacer algo que evite que se use de manera inteligente, pero solo debe ser una entrada en su diseño de marco; uno entre muchos


fuente
1

Me he enfrentado a este problema muchas veces y creo que se me ocurrió una solución simple.

Inicialmente, utilicé el patrón de decorador e implementé manualmente cada método, cuando tienes cientos de métodos, esto se vuelve muy tedioso.

Luego decidí usar PostSharp, pero no me gustó la idea de incluir una biblioteca completa solo para hacer algo que pudiera lograr con (una gran cantidad) de código simple.

Luego seguí la ruta del proxy transparente, que era divertido pero implicaba emitir dinámicamente IL en tiempo de ejecución y no sería algo que quisiera hacer en un entorno de producción.

Recientemente decidí usar plantillas T4 para implementar automáticamente el patrón de decorador en el momento del diseño, resulta que las plantillas T4 son bastante difíciles de trabajar y necesitaba hacer esto rápidamente, así que creé el siguiente código. Es rápido y sucio (y no admite propiedades), pero espero que alguien lo encuentre útil.

Aquí está el 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";

Aquí hay un ejemplo:

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

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

Luego cree una clase llamada LoggingTestAdapter que implemente ITestAdapter, obtenga un estudio visual para implementar automáticamente todos los métodos y luego ejecútelo a través del código anterior. Entonces deberías tener algo como esto:

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

Esto es con el código de soporte:

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
fuente