Programación orientada a aspectos: ¿cuándo comenzar a usar un marco?

22

Acabo de ver esta charla de Greg Young advirtiendo a la gente a KISS: Keep It Simple Stupid.

Una de las cosas que él sugiere es que para hacer la programación orientada a aspectos, uno qué no se necesita un marco .

Comienza haciendo una fuerte restricción: que todos los métodos toman uno, y solo un parámetro, (aunque lo relaja un poco más tarde usando una aplicación parcial ).

El ejemplo que da es definir una interfaz:

public interface IConsumes<T>
{
    void Consume(T message);
}

Si queremos emitir un comando:

public class Command
{
    public string SomeInformation;
    public int ID;

    public override string ToString()
    {
       return ID + " : " + SomeInformation + Environment.NewLine;
    }
}

El comando se implementa como:

public class CommandService : IConsumes<Command>
{
    private IConsumes<Command> _next;

    public CommandService(IConsumes<Command> cmd = null)
    {
        _next = cmd;
    }
    public void Consume(Command message)
    {
       Console.WriteLine("Command complete!");
        if (_next != null)
            _next.Consume(message);
    }
}

Para iniciar sesión en la consola, uno solo implementa:

public class Logger<T> : IConsumes<T>
{
    private readonly IConsumes<T> _next;

    public Logger(IConsumes<T> next)
    {
        _next = next;
    }
    public void Consume(T message)
    {
        Log(message);
        if (_next != null)
            _next.Consume(message);
    }

    private void Log(T message)
    {
        Console.WriteLine(message);
    }
}

Entonces, el registro previo al comando, el servicio de comando y el registro posterior al comando son solo:

var log1 = new Logger<Command>(null);
var svr  = new CommandService(log);
var startOfChain = new Logger<Command>(svr);

y el comando es ejecutado por:

var cmd = new Command();
startOfChain.Consume(cmd);

Para hacer esto en, por ejemplo, PostSharp , uno anotaría deCommandService esta manera:

public class CommandService : IConsumes<Command>
{
    [Trace]
    public void Consume(Command message)
    {
       Console.WriteLine("Command complete!");
    }
}

Y luego tiene que implementar el registro en una clase de atributo algo así como:

[Serializable]
public class TraceAttribute : OnMethodBoundaryAspect
{
    public override void OnEntry( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : Entered!" );   
    }

    public override void OnSuccess( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : Exited!" );
    }

    public override void OnException( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : EX : " + args.Exception.Message );
    }
}

El argumento que usa Greg es que la conexión del atributo a la implementación del atributo es "demasiada magia" para poder explicar lo que le está sucediendo a un desarrollador junior. El ejemplo inicial es todo "solo código" y se explica fácilmente.

Entonces, después de esa acumulación bastante larga, la pregunta es: ¿cuándo cambias el enfoque no marco de Greg para usar algo como PostSharp para AOP?

Peter K.
fuente
3
+1: Definitivamente una buena pregunta. Uno podría simplemente decir "... cuando ya entiendes la solución sin ella".
Steven Evers
1
Tal vez simplemente no estoy acostumbrado al estilo, pero la idea de escribir una aplicación completa como esta me parece completamente una locura. Prefiero usar un método interceptor.
Aaronaught
@Aaronaught: Sí, eso es parte de por qué quería publicar aquí. La explicación de Greg es que la configuración del sistema es simplemente conectar EN CÓDIGO NORMAL todas las IConsumespiezas diferentes . En lugar de tener que usar XML externo o alguna interfaz fluida, otra cosa más que aprender. Se podría argumentar que esta metodología es "otra cosa para aprender" también.
Peter K.
Todavía no estoy seguro de entender la motivación; La esencia misma de conceptos como AOP es poder expresar preocupaciones de manera declarativa , es decir, a través de la configuración. Para mí esto es solo reinventar la rueda cuadrada. No es una crítica a usted ni a su pregunta, pero creo que la única respuesta sensata es "Nunca usaría el enfoque de Greg a menos que fallara cualquier otra opción".
Aaronaught
No es que me moleste en absoluto, pero ¿no sería una pregunta un poco más de Stack Overflow?
Rei Miyasaka

Respuestas:

17

¿Está tratando de escribir un marco AOP "directo a TDWTF"? En serio, todavía no tengo idea de cuál era su punto. Tan pronto como diga "Todos los métodos deben tomar exactamente un parámetro", entonces ha fallado, ¿no? En esa etapa, usted dice: OK, esto impone algunas limitaciones muy artificiales en mi capacidad para escribir software, dejemos esto ahora antes, tres meses después tenemos una base de código de pesadilla completa para trabajar.

¿Y sabes qué? Puede escribir un marco de registro basado en IL basado en atributos simple con bastante facilidad con Mono.Cecil . (probarlo es un poco más complicado, pero ...)

Ah e IMO, si no estás usando atributos, no es AOP. El objetivo de hacer el código de registro de entrada / salida del método en la etapa de postprocesador es para que no se enrede con los archivos de código y no tenga que pensar en ello mientras refactoriza su código; Ese es su poder.

Todo lo que Greg ha demostrado es mantener el estúpido paradigma estúpido.


fuente
66
+1 para mantenerlo estúpido estúpido. Me recuerda a la famosa cita de Einstein: "haz que todo sea lo más simple posible, pero no más simple".
Rei Miyasaka
FWIW, F # tiene la misma restricción, cada método toma como máximo un argumento.
R0MANARMY
1
let concat (x : string) y = x + y;; concat "Hello, " "World!";; Parece que se necesitan dos argumentos, ¿qué me estoy perdiendo?
2
@The Mouth: lo que realmente está sucediendo es que con concat "Hello, "eso estás creando una función que toma solo yy se ha xpredefinido como un enlace local para ser "Hola". Si se pudiera ver esta función intermedia, se vería algo así let concat_x y = "Hello, " + y. Y luego de eso, estás llamando concat_x "World!". La sintaxis hace que sea menos obvio, pero esto le permite "hornear" nuevas funciones - por ejemplo, let printstrln = print "%s\n" ;; printstrln "woof". Además, incluso si haces algo como let f(x,y) = x + yeso, en realidad es solo un argumento de tupla .
Rei Miyasaka
1
La primera vez que hice una programación funcional estaba en Miranda en la universidad, tendré que echar un vistazo a F #, suena interesante.
8

Dios mío, ese tipo es intolerablemente abrasivo. Desearía haber leído el código en tu pregunta en lugar de haber visto esa charla.

No creo que usaría este enfoque si fuera solo por el uso de AOP. Greg dice que es bueno para situaciones simples. Esto es lo que haría en una situación simple:

public void DeactivateInventoryItem(CommandServices cs, Guid item, string reason)
{
    cs.Log.Write("Deactivated: {0} ({1})", item, reason);
    repo.Deactivate(item, reason);
}

Sí, lo hice, me deshice de AOP por completo. ¿Por qué? Porque no necesitas AOP en situaciones simples .

Desde el punto de vista de la programación funcional, permitir solo un parámetro por función realmente no me asusta. Sin embargo, este realmente no es un diseño que funcione bien con C #, y ir contra la corriente de tu lenguaje no BESA nada.

Solo usaría este enfoque si fuera necesario hacer un modelo de comando para comenzar, por ejemplo, si necesitaba una pila de deshacer o si estaba trabajando con los comandos de WPF .

De lo contrario, solo usaría un marco o alguna reflexión. PostSharp incluso funciona en Silverlight y Compact Framework, por lo que lo que él llama "magia" realmente no es mágico en absoluto .

Tampoco estoy de acuerdo con evitar los frameworks por el simple hecho de poder explicar las cosas a los juniors. No les está haciendo ningún bien. Si Greg trata a sus juniors de la forma en que sugiere que sean tratados, como idiotas de cráneos gruesos, entonces sospecho que sus desarrolladores senior tampoco son muy buenos, ya que probablemente no se les haya dado la oportunidad de aprender nada durante su años junior

Rei Miyasaka
fuente
5

Hice un estudio independiente en la universidad sobre AOP. De hecho, escribí un artículo sobre un enfoque para el modelo AOP con un complemento Eclipse. Eso en realidad es algo irrelevante, supongo. Los puntos clave son 1) era joven e inexperto y 2) estaba trabajando con AspectJ. Les puedo decir que la "magia" de la mayoría de los marcos de AOP no es tan complicada. De hecho, trabajé en un proyecto al mismo tiempo que intentaba hacer el enfoque de parámetro único usando una tabla hash. En mi opinión, el enfoque de parámetro único realmente es un marco y es invasivo. Incluso en esta publicación, pasé más tiempo tratando de comprender el enfoque de parámetro único que revisando el enfoque declarativo. Agregaré una advertencia de que no he visto la película, por lo que la "magia" de este enfoque puede estar en el uso de aplicaciones parciales.

Creo que Greg respondió tu pregunta. Debes cambiar a este enfoque cuando creas que estás en una situación en la que pasas una cantidad excesiva de tiempo explicando los marcos de AOP a tus desarrolladores junior. En mi opinión, si estás en este barco, probablemente estés contratando a los desarrolladores junior incorrectos. No creo que AOP requiera un enfoque declarativo, pero para mí, es mucho más claro y no invasivo desde una perspectiva de diseño.

kakridge
fuente
+1 para "Pasé más tiempo tratando de comprender el enfoque de parámetro único que revisando el enfoque declarativo". Encontré el IConsume<T>ejemplo demasiado complicado para lo que se está logrando.
Scott Whitlock
4

A menos que me falte algo, el código que ha mostrado es el patrón de diseño de 'cadena de responsabilidad' que es excelente si necesita conectar una serie de acciones en un objeto (como los comandos que pasan por una serie de controladores de comandos) en tiempo de ejecución

El AOP que utiliza PostSharp es bueno si sabe en el momento de la compilación qué comportamiento desea agregar. El tejido de código de PostSharp significa que no hay sobrecarga de tiempo de ejecución y mantiene el código muy limpio (especialmente cuando comienzas a usar cosas como aspectos de multidifusión). No creo que el uso básico de PostSharp sea particularmente complejo de explicar. La desventaja de PostSharp es que aumenta significativamente los tiempos de compilación.

Utilizo ambas técnicas en el código de producción y, aunque hay cierta superposición en el lugar donde se pueden aplicar, creo que en su mayor parte realmente apuntaron a diferentes escenarios.

FinnNk
fuente
4

Con respecto a su alternativa: estado allí, hecho eso. Nada se compara con la legibilidad de un atributo de una línea.

Dé una breve conferencia a los nuevos chicos explicándoles cómo funcionan las cosas en AOP.

Danny Varod
fuente
4

Lo que Greg describe es absolutamente razonable. Y también hay belleza en ello. El concepto es aplicable en un paradigma diferente al de la orientación pura a objetos. Es más un enfoque de procedimiento o un enfoque de diseño orientado al flujo. Por lo tanto, si está trabajando con código heredado, será bastante difícil aplicar este concepto porque podría ser necesaria una gran refactorización.

Trataré de dar otro ejemplo. Tal vez no sea perfecto, pero espero que aclare el punto.

Entonces tenemos un servicio de producto que usa un repositorio (en este caso usaremos un código auxiliar). El servicio obtendrá una lista de productos.

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public override string ToString() { return String.Format("{0}, {1}", Name, Price); }
}

public static class ProductService
{
    public static IEnumerable<Product> GetAllProducts(ProductRepositoryStub repository)
    {
        return repository.GetAll();
    }
}

public class ProductRepositoryStub
{
    public ProductRepositoryStub(string connStr) {}

    public IEnumerable<Product> GetAll()
    {
        return new List<Product>
        {
            new Product {Name = "Cd Player", Price = 49.99m},
            new Product {Name = "Yacht", Price = 2999999m }
        };
    }
}

Por supuesto, también podría pasar una interfaz al servicio.

A continuación, queremos mostrar una lista de productos en una vista. Por eso necesitamos una interfaz

public interface Handles<T>
{
    void Handle(T message);
}

y un comando que contiene la lista de productos

public class ShowProductsCommand
{
    public IEnumerable<Product> Products { get; set; }
}

y la vista

public class View : Handles<ShowProductsCommand>
{
    public void Handle(ShowProductsCommand cmd)
    {
        cmd.Products.ToList().ForEach(x => Console.WriteLine(x.ToString()));
    }
}

Ahora necesitamos un código que ejecute todo esto. Esto lo haremos en una clase llamada Aplicación. El método Run () es el método de integración que no contiene o al menos muy poca lógica de negocios. Las dependencias se inyectan en el constructor como métodos.

public class Application
{
    private readonly Func<IEnumerable<Product>> _getAllProducts;
    private readonly Action<ShowProductsCommand> _showProducts;

    public Application(Func<IEnumerable<Product>> getAllProducts, Action<ShowProductsCommand> showProducts)
    {
        _getAllProducts = getAllProducts;
        _showProducts = showProducts;
    }

    public void Run()
    {
        var products = _getAllProducts();
        var cmd = new ShowProductsCommand { Products = products };
        _showProducts(cmd);
    }
}

Finalmente componimos la aplicación en el método principal.

static void Main(string[] args)
{
    // composition
    Func<IEnumerable<Product>> getAllProducts = () => ProductService.GetAllProducts(new ProductRepositoryStub(""));
    Action<ShowProductsCommand> showProducts = (x) => new View().Handle(x);
    var app = new Application(getAllProducts, showProducts);

    app.Run();
}

Ahora lo bueno es que podemos agregar aspectos como el registro o el manejo de excepciones sin tocar el código existente y sin un marco o anotaciones. Para el manejo de excepciones, por ejemplo, simplemente agregamos una nueva clase:

public class ExceptionHandler<T> : Handles<T>
{
    private readonly Handles<T> _next;

    public ExceptionHandler(Handles<T> next) { _next = next; }

    public void Handle(T message)
    {
        try
        {
            _next.Handle(message);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

Y luego lo conectamos durante la composición en el punto de entrada de la aplicación. ni siquiera tenemos que tocar el código en la clase de Aplicación. Solo reemplazamos una línea:

Action<ShowProductsCommand> showProducts = (x) => new ExceptionHandler<ShowProductsCommand>(new View()).Handle(x);

Para resumir: cuando tenemos un diseño orientado al flujo, podemos agregar aspectos agregando la funcionalidad dentro de una nueva clase. Luego tenemos que cambiar una línea en el método de composición y listo.

Entonces, creo que una respuesta a su pregunta es que no puede cambiar fácilmente de un enfoque a otro, sino que debe decidir qué tipo de enfoque arquitectónico adoptará en su proyecto.

editar: En realidad, me acabo de dar cuenta de que el patrón de aplicación parcial utilizado con el servicio del producto hace las cosas un poco más complicadas. Necesitamos incluir otra clase alrededor del método de servicio del producto para poder agregar aspectos aquí también. Podría ser algo como esto:

public class ProductQueries : Queries<IEnumerable<Product>>
{
    private readonly Func<IEnumerable<Product>> _query;

    public ProductQueries(Func<IEnumerable<Product>> query)
    {
        _query = query;
    }

    public IEnumerable<Product> Query()
    {
        return _query();
    }
}

public interface Queries<TResult>
{
    TResult Query();
}

La composición debe cambiarse así:

Func<IEnumerable<Product>> getAllProducts = () => ProductService.GetAllProducts(new ProductRepositoryStub(""));
Func<IEnumerable<Product>> queryAllProducts = new ProductQueries(getAllProducts).Query;
Action<ShowProductsCommand> showProducts = (x) => new ExceptionHandler<ShowProductsCommand>(new View()).Handle(x);
var app = new Application(queryAllProducts, showProducts);
Leifbattermann
fuente