Programmazione orientata agli aspetti: quando iniziare a utilizzare un framework?

22

Ho appena guardato questo talk di Greg Young avvisa la gente di KISS: Keep It Simple Stupid.

Una delle cose che ha suggerito è che per fare programmazione orientata all'aspetto, uno non non ha bisogno di un framework .

Inizia facendo un strong vincolo: che tutti i metodi prendano uno, e solo uno, parametro (anche se lo rilassa un po 'più tardi utilizzando un'applicazione parziale ).

L'esempio che dà è definire un'interfaccia:

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

Se vogliamo emettere un comando:

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

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

Il comando è implementato come:

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

Per eseguire il logging su console, basta implementare:

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

Quindi, la registrazione pre-comando, il servizio comandi e la registrazione post-comando sono solo:

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

e il comando viene eseguito da:

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

Per fare ciò, ad esempio, PostSharp , si annoterebbe il CommandService in questo modo:

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

E poi devi implementare la registrazione in una classe di attributi come:

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

L'argomento che Greg usa è che la connessione dall'attributo all'attuazione dell'attributo è "troppa magia" per essere in grado di spiegare cosa sta succedendo a uno sviluppatore junior. L'esempio iniziale è tutto "solo codice" e facilmente spiegabile.

Quindi, dopo quella build piuttosto lunga, la domanda è: quando si passa dall'approccio non-framework di Greg all'utilizzo di qualcosa come PostSharp per AOP?

    
posta Peter K. 17.06.2011 - 02:56
fonte

6 risposte

17

Sta cercando di scrivere un framework AOP "straight to TDWTF"? Sinceramente non ho ancora capito quale fosse il suo punto di vista. Non appena dici "Tutti i metodi devono prendere esattamente un parametro" allora hai fallito, vero? A quel punto dici, OK, questo impone dei limiti seriamente artificiali alla mia capacità di scrivere software, lasciamo perdere questo punto prima, a tre mesi di distanza abbiamo un incubo completo con il codice base da utilizzare.

E sai cosa? È possibile scrivere un semplice framework di registrazione basato su IL basato su attributi semplicemente con Mono.Cecil . (testarlo è leggermente più complicato, ma ...)

Oh e IMO, se non si utilizzano gli attributi, non è AOP. L'intero punto di fare il codice di entrata / uscita del metodo di registrazione allo stadio di postprocessor è tale da non creare problemi con i file di codice e quindi non è necessario pensarci mentre si refactoring il codice; che è il suo potere.

Tutti i Greg hanno dimostrato che è stato mantenuto stupido paradigma.

    
risposta data 18.06.2011 - 18:17
fonte
8

Mio dio, quel ragazzo è intollerabilmente abrasivo. Vorrei solo leggere il codice nella tua domanda invece di guardare quel discorso.

Non penso che userò mai questo approccio se è solo per il gusto di usare AOP. Greg dice che va bene per le situazioni semplici. Ecco cosa farei in una situazione semplice:

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

Sì, l'ho fatto, mi sono sbarazzato completamente di AOP! Perché? Perché non hai bisogno di AOP in situazioni semplici .

Da un punto di vista della programmazione funzionale, lasciare solo un parametro per funzione in realtà non mi spaventa. Tuttavia, questo non è davvero un design che funziona bene con C # - e andare contro i grani della tua lingua non KISS nulla.

Userei questo approccio solo se fosse necessario creare un modello di comando, ad esempio se avessi bisogno di uno stack di annullamento o se lavorassi con Comandi WPF .

Altrimenti, userei solo un framework o una riflessione. PostSharp funziona anche in Silverlight e Compact Framework, quindi quello che lui chiama "magico" in realtà non è affatto magico .

Inoltre non sono d'accordo con l'evitare i quadri per il gusto di essere in grado di spiegare le cose ai ragazzi. Non li sta facendo bene. Se Greg tratta i suoi juniores nel modo in cui suggerisce di essere trattato, come idioti dai crani spessi, allora sospetto che i suoi sviluppatori senior non siano molto grandi, dato che probabilmente non hanno avuto molte opportunità di imparare qualcosa durante la loro anni junior.

    
risposta data 18.06.2011 - 09:34
fonte
5

Ho fatto uno studio indipendente al college su AOP. In realtà ho scritto un articolo su un approccio al modello AOP con un plug-in Eclipse. Suppongo che sia in qualche modo irrilevante. I punti chiave sono 1) ero giovane e inesperto e 2) stavo lavorando con AspectJ. Posso dirti che la "magia" della maggior parte dei framework AOP non è così complicata. In realtà ho lavorato a un progetto nello stesso periodo in cui tentavo di eseguire l'approccio a parametro singolo utilizzando una tabella hash. IMO, l'approccio a parametro singolo è davvero un framework ed è invasivo. Anche su questo post ho dedicato più tempo a cercare di capire l'approccio a parametro singolo piuttosto che a rivedere l'approccio dichiarativo. Aggiungerò un avvertimento che non ho visto il film, quindi la "magia" di questo approccio potrebbe essere nell'uso di applicazioni parziali.

Penso che Greg abbia risposto alla tua domanda. Dovresti passare a questo approccio quando pensi di trovarti in una situazione in cui passi una quantità eccessiva di tempo a spiegare i framework AOP ai tuoi sviluppatori junior. IMO, se sei su questa barca, probabilmente stai assumendo gli sviluppatori junior sbagliati. Non credo che AOP richieda un approccio dichiarativo, ma per me è molto più chiaro e non invasivo dal punto di vista del design.

    
risposta data 17.06.2011 - 22:00
fonte
4

A meno che non manchi qualcosa il codice che hai mostrato è il pattern di progettazione della "catena di responsabilità" che è fantastico se devi collegare una serie di azioni su un oggetto (come i comandi che attraversano una serie di comandi gestori) in fase di runtime.

L'uso di AOP con PostSharp è buono se sai al momento della compilazione quale sarà il comportamento che desideri aggiungere. L'intreccio di codice di PostSharp significa praticamente che non ci sono overhead di run-time e mantiene il codice molto pulito (specialmente quando inizi a utilizzare elementi come gli aspetti multicast). Non penso che l'utilizzo di base di PostSharp sia particolarmente complesso da spiegare. Lo svantaggio di PostSharp è che aumenta notevolmente i tempi di compilazione.

Uso entrambe le tecniche nel codice di produzione e anche se c'è qualche sovrapposizione in cui possono essere applicate penso che per la maggior parte miravano davvero a diversi scenari.

    
risposta data 18.06.2011 - 21:13
fonte
4

Per quanto riguarda la sua alternativa - stato lì, fatto. Niente è paragonabile alla leggibilità di un attributo a una riga.

Fai una breve lezione ai nuovi ragazzi spiegandoli come funzionano le cose in AOP.

    
risposta data 19.06.2011 - 02:12
fonte
4

Ciò che Greg descrive è assolutamente ragionevole. E c'è anche bellezza. Il concetto è applicabile in un paradigma diverso rispetto all'orientamento agli oggetti puri. È più un approccio procedurale o un approccio progettuale orientato al flusso. Quindi, se stai lavorando con codice legacy, sarà piuttosto difficile applicare questo concetto perché potrebbe essere necessario un sacco di refactoring.

Proverò a dare un altro esempio. Forse non perfetto, ma spero che chiarisca il punto.

Quindi abbiamo un servizio di prodotto che utilizza un repository (in questo caso utilizzeremo uno stub). Il servizio riceverà un elenco di prodotti.

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

Ovviamente potresti anche passare un'interfaccia al servizio.

Successivamente vogliamo mostrare un elenco di prodotti in una vista. Pertanto abbiamo bisogno di un'interfaccia

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

e un comando che contiene l'elenco dei prodotti

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

e la vista

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

Ora abbiamo bisogno di un codice che esegua tutto questo. Questo lo faremo in una classe chiamata Application. Il metodo Run () è il metodo di integrazione che contiene no o almeno una logica di business molto piccola. Le dipendenze sono iniettate nel costruttore come metodi.

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

Infine componiamo l'applicazione nel metodo principale.

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

Ora la cosa interessante è che possiamo aggiungere aspetti come la registrazione o la gestione delle eccezioni senza toccare il codice esistente e senza una struttura o annotazioni. Per la gestione delle eccezioni, ad es. aggiungiamo solo una nuova 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 poi lo colleghiamo durante la composizione al punto di ingresso dell'applicazione. non dobbiamo neppure toccare il codice nella classe Application. Sostituiamo solo una riga:

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

Quindi riprendere: quando abbiamo un design orientato al flusso possiamo aggiungere aspetti aggiungendo la funzionalità all'interno di una nuova classe. Quindi dobbiamo cambiare una riga nel metodo di composizione e il gioco è fatto.

Quindi penso che una risposta alla tua domanda sia che non puoi passare facilmente da un approccio all'altro, ma devi decidere quale tipo di approccio architettonico dovrai seguire nel tuo progetto.

modifica In realtà ho appena realizzato che il modello di applicazione parziale utilizzato con il servizio del prodotto rende le cose un po 'più complicate. Abbiamo bisogno di avvolgere un'altra classe attorno al metodo di servizio del prodotto per poter aggiungere anche qui degli aspetti. Potrebbe essere qualcosa del genere:

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 composizione deve quindi essere cambiata in questo modo:

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);
    
risposta data 19.02.2014 - 19:07
fonte

Leggi altre domande sui tag