Programação Orientada a Aspectos: Quando começar a usar uma estrutura?

22

Acabei de assistir essa palestra de Greg Young alertando as pessoas para o KISS: Keep It Simple Stupid.

Uma das coisas que ele sugeriu é que, para fazer uma programação orientada a aspectos, não é necessário um framework .

Ele começa fazendo uma forte restrição: que todos os métodos usem um, e apenas um, parâmetro (embora ele relaxe isso um pouco mais tarde usando aplicativo parcial ).

O exemplo que ele dá é definir uma interface:

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

Se queremos emitir um comando:

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

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

O comando é implementado 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 fazer o logon no console, basta implementar:

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

Em seguida, o log de pré-comando, serviço de comando e log de pós-comando são apenas:

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

e o comando é executado por:

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

Para fazer isso, por exemplo, no PostSharp , é possível fazer anotações da CommandServiceseguinte maneira:

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

E então tem que implementar o log em uma classe de atributo algo 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 );
    }
}

O argumento que Greg usa é que a conexão do atributo com a implementação do atributo é "muita mágica" para poder explicar o que está acontecendo com um desenvolvedor júnior. O exemplo inicial é todo "apenas código" e facilmente explicado.

Então, após essa construção bastante demorada, a questão é: quando você muda da abordagem não-estrutural de Greg para usar algo como PostSharp para AOP?

Peter K.
fonte
3
+1: Definitivamente, uma boa pergunta. Pode-se simplesmente dizer "... quando você já entende a solução sem ela".
Steven Evers
1
Talvez eu não esteja acostumada com o estilo, mas a idéia de escrever um aplicativo inteiro como esse me parece totalmente insana. Prefiro usar um interceptador de método.
Aaronaught
@Aaronaught: Sim, é por isso que eu queria postar aqui. A explicação de Greg é que a configuração do sistema está apenas conectando IN CÓDIGO NORMAL a todas as IConsumespartes diferentes . Em vez de ter que usar XML externo ou alguma interface Fluent - mais uma coisa a aprender. Alguém poderia argumentar que esta metodologia é "outra coisa a aprender" também.
Peter K.
Ainda não tenho certeza de entender a motivação; a própria essência de conceitos como AOP é poder expressar preocupações declarativamente , ou seja, através da configuração. Para mim, isso é apenas reinventar a roda quadrada. Não é uma crítica a você ou à sua pergunta, mas acho que a única resposta sensata é "Eu nunca usaria a abordagem de Greg, a menos que todas as outras opções falhassem".
Aaronaught
Não que isso me incomode, mas isso não seria um pouco mais uma questão de Stack Overflow?
Rei Miyasaka

Respostas:

17

Ele está tentando escrever uma estrutura de AOP "direta para TDWTF"? Sério, ainda não tenho idéia do que ele queria dizer. Assim que você diz "Todos os métodos devem ter exatamente um parâmetro", você falha, não é? Nesse estágio, você diz: OK, isso impõe algumas restrições seriamente artificiais à minha capacidade de escrever software, vamos deixar isso agora antes, daqui a três meses, temos uma base de código de pesadelo completa para trabalhar.

E sabe de uma coisa? Você pode escrever uma estrutura de log baseada em IL baseada em atributos simples com bastante facilidade com o Mono.Cecil . (testar é um pouco mais complicado, mas ...)

Ah e IMO, se você não estiver usando atributos, não é AOP. O objetivo principal de executar o código de registro de entrada / saída do método no estágio pós-processador é para que ele não mexa nos seus arquivos de código e, portanto, você não precisa pensar nisso ao refatorar o seu código; esse é o seu poder.

Tudo o que Greg demonstrou é o paradigma estúpido de mantê-lo estúpido.


fonte
6
+1 para mantê-lo estúpido. Me lembra a famosa citação de Einstein: "faça tudo o mais simples possível, mas não mais simples".
Rei Miyasaka
FWIW, F # tem a mesma restrição, cada método utiliza no máximo um argumento.
R0MANARMY
1
let concat (x : string) y = x + y;; concat "Hello, " "World!";; parece que são necessários dois argumentos, o que estou perdendo?
2
@ The Mouth - o que realmente está acontecendo é que, concat "Hello, "na verdade, você está criando uma função que requer apenas ye xpredefiniu como uma ligação local como "Hello". Se essa função intermediária pudesse ser vista, seria algo parecido let concat_x y = "Hello, " + y. E depois disso, você está ligando concat_x "World!". A sintaxe torna menos óbvio, mas isso permite "assar" novas funções - por exemplo let printstrln = print "%s\n" ;; printstrln "woof",. Além disso, mesmo que você faça algo assim let f(x,y) = x + y, esse é apenas um argumento de tupla .
Rei Miyasaka
1
A primeira vez que fiz uma programação funcional foi em Miranda na universidade, vou ter que dar uma olhada no F #, parece interessante.
8

Meu Deus, esse cara é intoleravelmente abrasivo. Eu gostaria de ler o código da sua pergunta em vez de assistir à conversa.

Eu acho que nunca usaria essa abordagem se fosse apenas para usar o AOP. Greg diz que é bom para situações simples. Aqui está o que eu faria em uma situação simples:

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

Sim, eu fiz isso, me livrei totalmente da AOP! Por quê? Porque você não precisa de AOP em situações simples .

Do ponto de vista da programação funcional, permitir apenas um parâmetro por função não me assusta. No entanto, esse realmente não é um design que funcione bem com C # - e ir contra os grãos do seu idioma não beija nada.

Eu usaria essa abordagem apenas se fosse necessário criar um modelo de comando, por exemplo, se eu precisasse de uma pilha de desfazer ou se estivesse trabalhando com os comandos do WPF .

Caso contrário, eu usaria apenas uma estrutura ou alguma reflexão. O PostSharp até funciona no Silverlight e no Compact Framework - então o que ele chama de "mágica" realmente não é mágico em tudo .

Também não concordo em evitar estruturas para poder explicar as coisas para os juniores. Não está fazendo nenhum bem a eles. Se Greg trata seus juniores da maneira que ele sugere que eles sejam tratados, como idiotas de caveira grossa, então suspeito que seus desenvolvedores seniores também não são muito bons, pois provavelmente não tiveram a oportunidade de aprender qualquer coisa durante seus estudos. anos juniores.

Rei Miyasaka
fonte
5

Eu fiz um estudo independente na faculdade sobre AOP. Na verdade, escrevi um artigo sobre uma abordagem para modelar AOP com um plug-in Eclipse. Isso é realmente um pouco irrelevante, suponho. Os pontos principais são: 1) eu era jovem e inexperiente e 2) estava trabalhando com o AspectJ. Eu posso lhe dizer que a "mágica" da maioria dos frameworks de AOP não é tão complicada. Na verdade, trabalhei em um projeto na mesma época que tentava fazer a abordagem de parâmetro único usando uma hashtable. Na IMO, a abordagem de parâmetro único é realmente uma estrutura e é invasiva. Mesmo neste post, passei mais tempo tentando entender a abordagem de parâmetro único do que revisando a abordagem declarativa. Acrescentarei uma ressalva de que não assisti ao filme; portanto, a "mágica" dessa abordagem pode estar no uso de aplicativos parciais.

Eu acho que Greg respondeu sua pergunta. Você deve mudar para essa abordagem quando achar que está em uma situação em que gasta um tempo excessivo explicando estruturas de AOP para seus desenvolvedores juniores. IMO, se você estiver neste barco, provavelmente está contratando os desenvolvedores juniores errados. Não acredito que a AOP exija uma abordagem declarativa, mas, para mim, é muito mais clara e não invasiva do ponto de vista do design.

kakridge
fonte
+1 em "Passei mais tempo tentando entender a abordagem de parâmetro único do que analisando a abordagem declarativa". Achei o IConsume<T>exemplo excessivamente complicado para o que está sendo realizado.
Scott Whitlock
4

A menos que esteja faltando alguma coisa, o código que você mostrou é o padrão de design da 'cadeia de responsabilidade', o que é ótimo se você precisar conectar uma série de ações em um objeto (como comandos passando por uma série de manipuladores de comando) em tempo de execução.

AOP usando PostSharp é bom se você souber em tempo de compilação qual será o comportamento que você deseja adicionar. A tecelagem de código do PostSharp significa praticamente que não há sobrecarga de tempo de execução e mantém o código muito limpo (especialmente quando você começa a usar coisas como aspectos multicast). Eu não acho que o uso básico do PostSharp seja particularmente complexo para explicar. A desvantagem do PostSharp é que ele aumenta significativamente o tempo de compilação.

Uso as duas técnicas no código de produção e, embora exista alguma sobreposição em que elas podem ser aplicadas, acho que, na maioria das vezes, elas realmente visavam cenários diferentes.

FinnNk
fonte
4

Em relação à sua alternativa - esteve lá, fez isso. Nada se compara à legibilidade de um atributo de uma linha.

Faça uma breve palestra para os novos caras, explicando como as coisas funcionam na AOP.

Danny Varod
fonte
4

O que Greg descreve é ​​absolutamente razoável. E há beleza nela também. O conceito é aplicável em um paradigma diferente da orientação pura ao objeto. É mais uma abordagem processual ou uma abordagem de design orientada ao fluxo. Portanto, se você estiver trabalhando com código legado, será bastante difícil aplicar esse conceito, pois pode ser necessária muita refatoração.

Vou tentar dar outro exemplo. Talvez não seja perfeito, mas espero que isso deixe o argumento mais claro.

Portanto, temos um serviço de produto que usa um repositório (neste caso, usaremos um stub). O serviço receberá uma lista de produtos.

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

Claro que você também pode passar uma interface para o serviço.

Em seguida, queremos mostrar uma lista de produtos em uma exibição. Portanto, precisamos de uma interface

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

e um comando que contém a lista de produtos

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

e a vista

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

Agora precisamos de algum código que execute tudo isso. Isso faremos em uma classe chamada Application. O método Run () é o método de integração que contém nenhuma ou pelo menos muito pouca lógica de negócios. As dependências são injetadas no construtor 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, compomos a aplicação no 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();
}

Agora, o mais interessante é que podemos adicionar aspectos como registro ou tratamento de exceções sem tocar no código existente e sem uma estrutura ou anotações. Para tratamento de exceções, por exemplo, basta adicionar uma nova classe:

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

E depois a conectamos durante a composição no ponto de entrada do aplicativo. nem precisamos tocar no código na classe Application. Apenas substituímos uma linha:

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

Então, resumindo: quando temos um design orientado ao fluxo, podemos adicionar aspectos adicionando a funcionalidade dentro de uma nova classe. Então temos que mudar uma linha no método de composição e é isso.

Portanto, acho que uma resposta para sua pergunta é que você não pode alternar facilmente de uma abordagem para outra, mas precisa decidir sobre que tipo de abordagem arquitetural será adotada em seu projeto.

edit: Na verdade, acabei de perceber que o padrão de aplicativo parcial usado com o serviço do produto torna as coisas um pouco mais complicadas. Precisamos agrupar outra classe em torno do método de serviço do produto para poder adicionar aspectos aqui também. Pode ser algo assim:

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

A composição deve ser alterada assim:

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
fonte