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


22

Ho appena visto questo discorso di Greg Young avvertire le persone di KISS: Keep It Simple Stupid.

Una delle cose che ha suggerito è che per fare una programmazione orientata all'aspetto, non è necessario un framework .

Comincia con un forte vincolo: che tutti i metodi assumano uno e un solo parametro (anche se lo rilassa un po 'più tardi usando un'applicazione parziale ).

L'esempio che fornisce è 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 la registrazione sulla console, uno quindi 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);
    }
}

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 questo in PostSharp , ad esempio , si dovrebbe annotare in CommandServicequesto modo:

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

E quindi è necessario implementare la registrazione in una classe di attributi qualcosa del tipo:

[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 usato da Greg è che la connessione dall'attributo all'implementazione dell'attributo è "troppo magica" per poter spiegare cosa sta succedendo a uno sviluppatore junior. L'esempio iniziale è tutto "solo codice" e facilmente spiegabile.

Quindi, dopo quell'accumulo piuttosto prolisso, la domanda è: quando passi dall'approccio non-framework di Greg all'utilizzo di qualcosa come PostSharp per AOP?


3
+1: sicuramente una buona domanda. Si potrebbe semplicemente dire "... quando già capisci la soluzione senza di essa."
Steven Evers,

1
Forse non sono abituato allo stile, ma l'idea di scrivere un'intera applicazione come questa mi sembra assolutamente folle. Preferirei usare un intercettore di metodo.
Aaronaught il

@Aaronaught: Sì, è parte del motivo per cui volevo pubblicare qui. La spiegazione di Greg è che la configurazione del sistema si sta quindi collegando IN CODICE NORMALE tutti i diversi IConsumespezzi. Piuttosto che dover usare XML esterno o qualche interfaccia fluida --- ancora un'altra cosa da imparare. Si potrebbe sostenere che questa metodologia è anche "un'altra cosa da imparare".
Peter K.

Non sono ancora sicuro di capire la motivazione; l'essenza stessa di concetti come AOP è quella di essere in grado di esprimere dichiarazioni in modo dichiarativo , cioè attraverso la configurazione. Per me questo sta solo reinventando la ruota quadrata. Non una critica a te o alla tua domanda, ma penso che l'unica risposta sensata sia "Non avrei mai usato l'approccio di Greg se non avessero fallito ogni altra opzione".
Aaronaught,

Non che mi dia fastidio, ma non sarebbe leggermente più una domanda di Stack Overflow?
Rei Miyasaka,

Risposte:


17

Sta cercando di scrivere un framework AOP "straight to TDWTF"? Sul serio non ho ancora idea di quale fosse il suo punto. Non appena dici "Tutti i metodi devono prendere esattamente un parametro", allora hai fallito, vero? A quel punto dici, OK, questo impone alcuni vincoli seriamente artificiali alla mia capacità di scrivere software, lasciamo cadere questo ora prima, tre mesi dopo la fine abbiamo una base di codice da incubo completa con cui lavorare.

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

Oh e IMO, se non stai usando gli attributi, non è AOP. Il punto centrale di eseguire il metodo di registrazione / uscita del codice di registrazione nella fase post-processore è che non interferisca con i file di codice e non sia necessario pensarci mentre si esegue il refactoring del codice; questo è il suo potere.

Tutto ciò che Greg ha dimostrato è il paradigma stupido e stupido.


6
+1 per tenerlo stupido stupido. Mi ricorda la famosa citazione di Einstein: "Rendi tutto il più semplice possibile, ma non più semplice".
Rei Miyasaka,

FWIW, F # ha la stessa restrizione, ogni metodo accetta al massimo un argomento.
R0MANARMY,

1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;sembra che ci vogliono due argomenti, cosa mi sto perdendo?

2
@The Mouth - quello che sta realmente accadendo è che con la concat "Hello, "creazione di una funzione che richiede solo ye ha xpredefinito come associazione locale per essere "Hello". Se questa funzione intermedia potesse essere vista, sarebbe simile let concat_x y = "Hello, " + y. E poi a seguire, stai chiamando concat_x "World!". La sintassi la rende meno ovvia, ma ciò consente di "creare" nuove funzioni, ad esempio let printstrln = print "%s\n" ;; printstrln "woof". Inoltre, anche se fai qualcosa del genere let f(x,y) = x + y, in realtà è solo un argomento tupla .
Rei Miyasaka,

1
La prima volta che ho fatto qualsiasi programmazione funzionale era a Miranda all'università, dovrò dare un'occhiata a F #, sembra interessante.

8

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

Non penso che avrei mai usato questo approccio se fosse solo per il gusto di usare AOP. Greg dice che va bene per 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 completamente sbarazzato di AOP! Perché? Perché non hai bisogno di AOP in situazioni semplici .

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

Utilizzerei questo approccio solo se all'inizio fosse necessario creare un modello di comando, ad esempio se avessi bisogno di uno stack di annullamento o se stessi lavorando con i comandi WPF .

Altrimenti, userei solo un quadro o qualche riflessione. PostSharp funziona anche in Silverlight e Compact Framework - così ciò che egli chiama "magia" non è davvero magica a tutti .

Inoltre, non sono d'accordo sull'evitare i framework per poter spiegare le cose ai giovani. Non li sta facendo niente di buono. Se Greg tratta i suoi juniors nel modo in cui suggerisce che vengano trattati, come idioti dal cranio spesso, allora sospetto che anche i suoi sviluppatori senior non siano molto bravi, poiché probabilmente non hanno avuto molta opportunità di imparare qualcosa durante il loro anni junior.


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. Questo è in realtà un po 'irrilevante, suppongo. I punti chiave sono 1) Ero giovane e inesperto e 2) Lavoravo con AspectJ. Posso dirti che la "magia" della maggior parte dei framework AOP non è così complicata. In realtà ho lavorato su un progetto nello stesso periodo in cui stavo cercando di fare l'approccio a singolo parametro usando una tabella hash. IMO, l'approccio a parametro singolo è davvero un framework ed è invasivo. Anche su questo post, ho trascorso più tempo a cercare di capire l'approccio a parametro singolo 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 troppo tempo a spiegare i framework AOP ai tuoi sviluppatori junior. IMO, se sei su questa barca, probabilmente stai assumendo sviluppatori junior sbagliati. Non credo che AOP richieda un approccio dichiarativo, ma per me è solo molto più chiaro e non invasivo dal punto di vista del design.


+1 per "Ho trascorso più tempo a cercare di capire l'approccio a parametro singolo che a rivedere l'approccio dichiarativo". Ho trovato l' IConsume<T>esempio troppo complicato per ciò che è stato realizzato.
Scott Whitlock,

4

A meno che non mi manchi qualcosa, il codice che hai mostrato è il modello di progettazione della "catena di responsabilità" che è ottimo se devi collegare una serie di azioni su un oggetto (come i comandi che passano attraverso una serie di gestori di comandi) in runtime.

AOP che utilizza PostSharp è utile se sai in fase di compilazione quale sarà il comportamento che desideri aggiungere. La trama del codice di PostSharp significa praticamente che non ci sono sovraccarichi di runtime e mantiene il codice molto pulito (specialmente quando inizi a usare cose come gli aspetti multicast). Non credo che l'utilizzo di base di PostSharp sia particolarmente complesso da spiegare. Il rovescio della medaglia di PostSharp è che aumenta significativamente i tempi di compilazione.

Uso entrambe le tecniche nel codice di produzione e anche se ci sono alcune sovrapposizioni in cui possono essere applicate, penso che per la maggior parte abbiano mirato davvero a diversi scenari.


4

Per quanto riguarda la sua alternativa - ci sono stato, l'ho fatto. Nulla è paragonabile alla leggibilità di un attributo di una riga.

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


4

Ciò che Greg descrive è assolutamente ragionevole. E c'è anche bellezza. Il concetto è applicabile in un paradigma diverso dal puro orientamento dell'oggetto. È più un approccio procedurale o un approccio alla progettazione orientato al flusso. Quindi, se stai lavorando con il codice legacy, sarà abbastanza difficile applicare questo concetto perché potrebbe essere necessario un sacco di refactoring.

Proverò a fare 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 useremo uno stub). Il servizio otterrà 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 po 'di codice che esegua tutto questo. Questo lo faremo in una classe chiamata Applicazione. Il metodo Run () è il metodo di integrazione che contiene nessuna o nessuna logica aziendale minima. Le dipendenze vengono 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 il bello è che possiamo aggiungere aspetti come la registrazione o la gestione delle eccezioni senza toccare il codice esistente e senza un framework o annotazioni. Per la gestione delle eccezioni, ad esempio, 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 quindi lo colleghiamo insieme durante la composizione nel punto di ingresso dell'applicazione. non dobbiamo nemmeno toccare il codice nella classe Application. Sostituiamo solo una riga:

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

Quindi, per 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 basta.

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

modificare: In realtà mi sono appena reso conto che il modello di applicazione parziale utilizzato con il servizio prodotto rende le cose un po 'più complicate. Dobbiamo includere 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 modificata 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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.