Programmation orientée aspect: quand commencer à utiliser un framework?

22

Je viens de regarder cette conférence de Greg Young avertissant les gens de KISS: Keep It Simple Stupid.

Une des choses qu'il a suggérées, c'est que pour faire de la programmation orientée aspect, on n'a pas besoin d'un cadre .

Il commence par faire une forte contrainte: que toutes les méthodes prennent un et un seul paramètre (bien qu'il assouplisse cela un peu plus tard en utilisant une application partielle ).

L'exemple qu'il donne est de définir une interface:

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

Si nous voulons émettre une commande:

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

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

La commande est implémentée comme:

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

Pour se connecter à la console, on implémente alors simplement:

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

Ensuite, la journalisation pré-commande, le service de commande et la journalisation post-commande sont alors simplement:

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

et la commande est exécutée par:

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

Pour ce faire dans, par exemple, PostSharp , on annoterait de CommandServicecette façon:

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

Et puis implémenter la journalisation dans une classe d'attribut quelque chose comme:

[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'argument que Greg utilise est que la connexion de l'attribut à l'implémentation de l'attribut est "trop ​​magique" pour pouvoir expliquer ce qui arrive à un développeur junior. L'exemple initial est tout "juste du code" et facile à expliquer.

Donc, après cette accumulation plutôt longue, la question est: quand passez-vous de l'approche non-framework de Greg à l'utilisation de quelque chose comme PostSharp pour AOP?

Peter K.
la source
3
+1: Certainement une bonne question. On pourrait simplement dire "... quand vous comprenez déjà la solution sans elle".
Steven Evers
1
Peut-être que je ne suis tout simplement pas habitué au style, mais l'idée d'écrire une application entière comme celle-ci me semble complètement folle. Je préfère utiliser un intercepteur de méthode.
Aaronaught
@Aaronaught: Oui, c'est en partie pourquoi j'ai voulu poster ici. L'explication de Greg est que la configuration du système est simplement en train de connecter EN CODE NORMAL toutes les différentes IConsumespièces. Plutôt que d'avoir à utiliser du XML externe ou une interface Fluent --- encore une autre chose à apprendre. On pourrait soutenir que cette méthodologie est "une autre chose à apprendre" également.
Peter K.
Je ne suis toujours pas sûr de comprendre la motivation; l'essence même de concepts tels que l'AOP est de pouvoir exprimer des préoccupations de manière déclarative , c'est-à-dire à travers la configuration. Pour moi, c'est juste réinventer la roue carrée. Pas une critique de vous ou de votre question, mais je pense que la seule réponse sensée est "Je n'utiliserais jamais l'approche de Greg à moins que toutes les autres options échouent."
Aaronaught
Cela ne me dérange pas du tout, mais ne serait-ce pas un peu plus une question de débordement de pile?
Rei Miyasaka

Réponses:

17

Essaie-t-il d'écrire un cadre AOP «directement vers TDWTF»? Sérieusement, je n'ai toujours pas la moindre idée de son argument. Dès que vous dites "Toutes les méthodes doivent prendre exactement un paramètre", vous avez échoué, n'est-ce pas? À ce stade, vous dites: OK, cela impose des contraintes très artificielles à ma capacité à écrire des logiciels, abandonnons cela maintenant avant, trois mois plus tard, nous avons une base de code cauchemardesque complète avec laquelle travailler.

Et tu sais quoi? Vous pouvez écrire un cadre de journalisation basé sur IL basé sur un attribut simple assez facilement avec Mono.Cecil . (le tester est un peu plus compliqué, mais ...)

Oh et IMO, si vous n'utilisez pas d'attributs, ce n'est pas AOP. L'intérêt de la méthode d'entrée / sortie du code de journalisation au stade du post-processeur est de ne pas gâcher vos fichiers de code et vous n'avez donc pas besoin d'y penser lorsque vous refactorisez votre code; qui est son pouvoir.

Tout ce que Greg a démontré, c'est le paradigme stupide de la garder stupide.


la source
6
+1 pour le garder stupide stupide. Cela me rappelle la célèbre citation d'Einstein: "rendre tout aussi simple que possible, mais pas plus simple".
Rei Miyasaka
FWIW, F # a la même restriction, chaque méthode prend au plus un argument.
R0MANARMY
1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;on dirait qu'il faut deux arguments, qu'est-ce qui me manque?
2
@The Mouth - ce qui se passe réellement, c'est qu'avec concat "Hello, "vous créez en fait une fonction qui prend juste y, et a xprédéfinie comme une liaison locale pour être "Bonjour". Si cette fonction intermédiaire pouvait être vue, elle ressemblerait à quelque chose let concat_x y = "Hello, " + y. Et après cela, vous appelez concat_x "World!". La syntaxe rend moins évidente, mais cela vous permet de « cuire » de nouvelles fonctions - par exemple, let printstrln = print "%s\n" ;; printstrln "woof". De plus, même si vous faites quelque chose comme let f(x,y) = x + yça, ce n'est en fait qu'un argument de tuple .
Rei Miyasaka
1
La dernière fois que j'ai fait une programmation fonctionnelle, j'étais à Miranda à l'université, je vais devoir jeter un œil à F #, ça a l'air intéressant.
8

Mon dieu, ce gars est intolérablement abrasif. Je souhaite que je viens de lire le code dans votre question au lieu de regarder cette conversation.

Je ne pense pas que j'utiliserais cette approche si ce n'est que pour utiliser AOP. Greg dit que c'est bon pour les situations simples. Voici ce que je ferais dans une situation simple:

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

Ouais, je l'ai fait, je me suis complètement débarrassé d'AOP! Pourquoi? Parce que vous n'avez pas besoin d'AOP dans des situations simples .

Du point de vue de la programmation fonctionnelle, autoriser un seul paramètre par fonction ne me fait pas vraiment peur. Néanmoins, ce n'est vraiment pas une conception qui fonctionne bien avec C # - et aller à l'encontre des grains de votre langage ne BAISERA rien.

Je n'utiliserais cette approche que s'il était nécessaire de créer un modèle de commande pour commencer, par exemple si j'avais besoin d'une pile d'annulation ou si je travaillais avec des commandes WPF .

Sinon, j'utiliserais simplement un cadre ou une réflexion. PostSharp fonctionne même dans Silverlight et Compact Framework - donc ce qu'il appelle la «magie» n'est vraiment pas magique du tout .

Je ne suis pas non plus d'accord avec l'idée d'éviter les cadres pour pouvoir expliquer les choses aux juniors. Cela ne leur fait aucun bien. Si Greg traite ses juniors de la façon dont il suggère qu'ils soient traités, comme des idiots au crâne épais, alors je soupçonne que ses développeurs seniors ne sont pas très grands non plus, car ils n'ont probablement pas eu beaucoup l'occasion d'apprendre quoi que ce soit pendant leur années juniors.

Rei Miyasaka
la source
5

J'ai fait une étude indépendante au collège sur AOP. J'ai en fait écrit un article sur une approche du modèle AOP avec un plug-in Eclipse. C'est en fait quelque peu hors de propos, je suppose. Les points clés sont 1) j'étais jeune et inexpérimenté et 2) je travaillais avec AspectJ. Je peux vous dire que la "magie" de la plupart des frameworks AOP n'est pas si compliquée. J'ai en fait travaillé sur un projet à la même époque qui essayait de faire l'approche à paramètre unique en utilisant une table de hachage. OMI, l'approche à paramètre unique est vraiment un cadre et elle est invasive. Même sur ce post, j'ai passé plus de temps à essayer de comprendre l'approche à paramètre unique que j'ai passé en revue l'approche déclarative. J'ajouterai une mise en garde que je n'ai pas regardé le film, donc la "magie" de cette approche peut être dans l'utilisation d'applications partielles.

Je pense que Greg a répondu à votre question. Vous devriez passer à cette approche lorsque vous pensez que vous êtes dans une situation où vous passez trop de temps à expliquer les cadres AOP à vos développeurs juniors. OMI, si vous êtes dans ce bateau, vous embauchez probablement les mauvais développeurs juniors. Je ne crois pas que l'AOP nécessite une approche déclarative, mais pour moi, c'est juste beaucoup plus clair et non invasif du point de vue de la conception.

kakridge
la source
+1 pour "J'ai passé plus de temps à essayer de comprendre l'approche à paramètre unique que j'ai passé en revue l'approche déclarative." J'ai trouvé l' IConsume<T>exemple trop compliqué pour ce qui est accompli.
Scott Whitlock
4

À moins que quelque chose me manque, le code que vous avez montré est le modèle de conception de la `` chaîne de responsabilité '', ce qui est idéal si vous devez câbler une série d'actions sur un objet (telles que des commandes passant par une série de gestionnaires de commandes) à exécution.

AOP utilisant PostSharp est bon si vous savez au moment de la compilation quel comportement vous souhaitez ajouter. Le tissage de code de PostSharp signifie à peu près qu'il n'y a pas de temps d'exécution et maintient le code très propre (en particulier lorsque vous commencez à utiliser des choses comme les aspects de multidiffusion). Je ne pense pas que l'utilisation de base de PostSharp soit particulièrement complexe à expliquer. L'inconvénient de PostSharp est qu'il augmente considérablement les temps de compilation.

J'utilise les deux techniques dans le code de production et bien qu'il y ait un certain chevauchement dans leur application, je pense que la plupart du temps, elles visaient vraiment différents scénarios.

FinnNk
la source
4

En ce qui concerne son alternative - été là, fait ça. Rien ne se compare à la lisibilité d'un attribut d'une ligne.

Donnez une courte conférence aux nouveaux gars en leur expliquant comment les choses fonctionnent dans AOP.

Danny Varod
la source
4

Ce que Greg décrit est absolument raisonnable. Et il y a aussi de la beauté. Le concept est applicable dans un paradigme différent de l'orientation d'objet pur. Il s'agit plus d'une approche procédurale ou d'une approche de conception orientée flux. Donc, si vous travaillez avec du code hérité, il sera assez difficile d'appliquer ce concept car beaucoup de refactoring pourraient être nécessaires.

Je vais essayer de donner un autre exemple. Peut-être pas parfait, mais j'espère que cela clarifie ce point.

Nous avons donc un service produit qui utilise un référentiel (dans ce cas, nous utiliserons un stub). Le service obtiendra une liste de produits.

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

Bien sûr, vous pouvez également passer une interface au service.

Ensuite, nous voulons afficher une liste de produits dans une vue. Nous avons donc besoin d'une interface

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

et une commande qui contient la liste des produits

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

et la vue

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

Maintenant, nous avons besoin d'un code qui exécute tout cela. Nous le ferons dans une classe appelée Application. La méthode Run () est la méthode d'intégration qui ne contient pas ou très peu de logique métier. Les dépendances sont injectées dans le constructeur en tant que méthodes.

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

Enfin, nous composons l'application dans la méthode 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();
}

Maintenant, la chose intéressante est que nous pouvons ajouter des aspects comme la journalisation ou la gestion des exceptions sans toucher au code existant et sans cadre ni annotations. Pour la gestion des exceptions, par exemple, nous ajoutons simplement une nouvelle 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);
        }
    }
}

Et puis on le branche pendant la composition au point d'entrée de l'application. nous n'avons même pas besoin de toucher le code dans la classe Application. Nous remplaçons juste une ligne:

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

Donc, pour reprendre: lorsque nous avons une conception orientée flux, nous pouvons ajouter des aspects en ajoutant la fonctionnalité à l'intérieur d'une nouvelle classe. Ensuite, nous devons changer une ligne dans la méthode de composition et c'est tout.

Je pense donc qu'une réponse à votre question est que vous ne pouvez pas facilement passer d'une approche à l'autre, mais vous devez décider quel type d'approche architecturale vous allez privilégier dans votre projet.

edit: En fait, je viens de réaliser que le modèle d'application partielle utilisé avec le service produit rend les choses un peu plus compliquées. Nous devons envelopper une autre classe autour de la méthode de service des produits pour pouvoir ajouter des aspects ici également. Cela pourrait être quelque chose comme ceci:

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 composition doit ensuite être modifiée comme ceci:

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
la source