Aspektorientierte Programmierung: Wann soll mit der Verwendung eines Frameworks begonnen werden?

22

Ich habe gerade diesen Vortrag von Greg Young gesehen, der die Leute zu KISS warnt: Keep It Simple Stupid.

Eines der Dinge , schlug er vor, dass die aspektorientierte Programmierung zu tun, man hat nicht einen Rahmen braucht .

Er beginnt mit einer starken Einschränkung: Alle Methoden verwenden nur einen einzigen Parameter ( dies wird jedoch etwas später durch teilweise Anwendung gelockert ).

Das Beispiel, das er gibt, besteht darin, eine Schnittstelle zu definieren:

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

Wenn wir einen Befehl ausgeben wollen:

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

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

Der Befehl wird implementiert als:

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

Um sich an der Konsole anzumelden, muss man nur Folgendes implementieren:

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

Dann sind die Protokollierung vor dem Befehl, der Befehlsservice und die Protokollierung nach dem Befehl einfach:

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

und der Befehl wird ausgeführt von:

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

Um dies zum Beispiel in PostSharp zu tun , würde man dies folgendermaßen kommentieren CommandService:

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

Und dann müssen Sie die Protokollierung in einer Attributklasse implementieren, wie etwa:

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

Das Argument, das Greg verwendet, ist, dass die Verbindung vom Attribut zur Implementierung des Attributs "zu magisch" ist, um erklären zu können, was mit einem Junior-Entwickler geschieht. Das erste Beispiel ist alles "nur Code" und leicht zu erklären.

Nach diesem langwierigen Aufbau lautet die Frage also: Wann stellen Sie den Wechsel von Gregs Nicht-Framework-Ansatz auf die Verwendung von PostSharp für AOP um?

Peter K.
quelle
3
+1: Auf jeden Fall eine gute Frage. Man könnte einfach sagen "... wenn Sie die Lösung ohne sie bereits verstehen."
Steven Evers
1
Vielleicht bin ich einfach nicht an den Stil gewöhnt, aber die Idee, eine ganze Anwendung wie diese zu schreiben, kommt mir völlig verrückt vor. Ich würde lieber eine Methode Abfangjäger verwenden.
Aaronaught
@Aaronaught: Ja, das ist ein Teil dessen, warum ich hier posten wollte. Gregs Erklärung ist, dass die Systemkonfiguration dann einfach IN NORMAL CODE alle verschiedenen IConsumesTeile miteinander verbindet. Anstatt externes XML oder eine Fluent-Schnittstelle verwenden zu müssen - noch eine andere Sache zum Lernen. Man könnte argumentieren, dass diese Methodik auch "eine andere Sache zu lernen" ist.
Peter K.
Ich bin mir immer noch nicht sicher, ob ich die Motivation verstehe. Das Wesen von Konzepten wie AOP ist, Bedenken deklarativ ausdrücken zu können , dh durch Konfiguration. Für mich ist das nur eine Neuerfindung des Vierkantrads. Keine Kritik an Ihnen oder Ihrer Frage, aber ich denke, die einzig vernünftige Antwort lautet: "Ich würde niemals Gregs Ansatz anwenden, wenn nicht jede andere Option fehlschlägt."
Aaronaught
Nicht, dass es mich überhaupt stört, aber wäre das nicht eher eine Stapelüberlauf-Frage?
Rei Miyasaka

Antworten:

17

Versucht er, ein AOP-Framework "Straight to TDWTF" zu schreiben? Ich habe immer noch keine Ahnung, worum es ihm ging. Sobald Sie sagen "Alle Methoden müssen genau einen Parameter annehmen", sind Sie gescheitert, nicht wahr? Zu diesem Zeitpunkt sagen Sie, OK, dies bringt einige ernsthafte künstliche Einschränkungen für meine Fähigkeit zum Schreiben von Software mit sich. Lassen Sie uns dies jetzt, drei Monate später, fallen. Wir haben eine komplette Albtraum-Codebasis, mit der wir arbeiten können.

Und weisst du was? Mit Mono.Cecil können Sie ganz einfach ein einfaches, attributgesteuertes, IL-basiertes Protokollierungsframework schreiben . (Testen ist etwas komplizierter, aber ...)

Oh und IMO, wenn Sie keine Attribute verwenden, ist es nicht AOP. Der Sinn und Zweck des Protokolliercodes für die Methodenein- und -ausgabe in der Postprozessor-Phase besteht darin, dass er nicht mit Ihren Codedateien in Konflikt gerät und Sie beim Umgestalten Ihres Codes nicht darüber nachdenken müssen. das ist seine Macht.

Alles, was Greg gezeigt hat, ist, dass es ein dummes, dummes Paradigma gibt.


quelle
6
+1, um es dumm zu halten. Erinnert mich an Einsteins berühmtes Zitat: "Mach alles so einfach wie möglich, aber nicht einfacher."
Rei Miyasaka
FWIW, F # hat die gleiche Einschränkung, jede Methode benötigt höchstens ein Argument.
R0MANARMY
1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;sieht aus wie es zwei Argumente braucht, was fehle ich?
2
@ The Mouth - was tatsächlich passiert, ist, dass concat "Hello, "Sie tatsächlich eine Funktion erstellen, die gerecht yist und xdie als lokale Bindung als "Hallo" vordefiniert wurde. Wenn diese Zwischenfunktion zu sehen wäre, würde sie ungefähr so ​​aussehen let concat_x y = "Hello, " + y. Und danach rufst du an concat_x "World!". Die Syntax macht es weniger offensichtlich, aber dadurch können Sie neue Funktionen "backen" - zum Beispiel let printstrln = print "%s\n" ;; printstrln "woof". Auch wenn Sie so etwas tun let f(x,y) = x + y, ist das eigentlich nur ein Tupelargument .
Rei Miyasaka
1
Wenn ich zum ersten Mal funktionale Programmierung in Miranda an der Universität gemacht habe, muss ich mir F # ansehen, das hört sich interessant an.
8

Mein Gott, dieser Typ ist unerträglich aggressiv. Ich wünschte, ich würde nur den Code in Ihrer Frage lesen, anstatt diesen Vortrag zu sehen.

Ich glaube nicht, dass ich diesen Ansatz jemals verwenden würde, wenn es nur um die Verwendung von AOP geht. Greg sagt, es sei gut für einfache Situationen. Folgendes würde ich in einer einfachen Situation tun:

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

Ja, ich habe es geschafft, ich habe AOP komplett beseitigt! Warum? Weil Sie in einfachen Situationen kein AOP benötigen .

Vom Standpunkt der funktionalen Programmierung aus macht es mir wenig Angst, nur einen Parameter pro Funktion zuzulassen. Trotzdem ist dies wirklich kein Design, das gut mit C # zusammenarbeitet - und wenn Sie sich gegen die Gegebenheiten Ihrer Sprache richten, können Sie nichts KÜSSEN.

Ich würde diesen Ansatz nur verwenden, wenn zunächst ein Befehlsmodell erstellt werden müsste, z. B. wenn ich einen Rückgängig-Stapel benötige oder mit WPF-Befehlen arbeite .

Ansonsten würde ich nur einen Rahmen oder eine Reflexion verwenden. Postsharp funktioniert auch in Silverlight und Compact Framework - so wie er es nennt „Magie“ nicht wirklich magisch ist überhaupt .

Ich bin auch nicht damit einverstanden, Frameworks zu vermeiden, um Junioren die Dinge erklären zu können. Es tut ihnen nicht gut. Wenn Greg seine Junioren so behandelt, wie er es vorschlägt, wie dickschädlige Idioten behandelt zu werden, dann vermute ich, dass seine Senior-Entwickler auch nicht besonders gut sind, da ihnen wahrscheinlich nicht die Gelegenheit gegeben wurde, während ihrer Zeit etwas zu lernen Junior Jahre.

Rei Miyasaka
quelle
5

Ich habe ein unabhängiges College-Studium über AOP absolviert. Ich habe tatsächlich eine Arbeit über einen Ansatz zum Modellieren von AOP mit einem Eclipse-Plug-In geschrieben. Das ist eigentlich etwas irrelevant, nehme ich an. Die wichtigsten Punkte sind: 1) Ich war jung und unerfahren und 2) Ich habe mit AspectJ gearbeitet. Ich kann Ihnen sagen, dass die "Magie" der meisten AOP-Frameworks nicht so kompliziert ist. Ich habe ungefähr zur gleichen Zeit an einem Projekt gearbeitet, bei dem versucht wurde, den Einzelparameter-Ansatz mithilfe einer Hash-Tabelle durchzuführen. IMO, der Single-Parameter-Ansatz ist wirklich ein Framework und ist invasiv. Selbst in diesem Beitrag habe ich mehr Zeit darauf verwendet, den Single-Parameter-Ansatz zu verstehen, als den deklarativen Ansatz zu überprüfen. Ich werde eine Einschränkung hinzufügen, dass ich den Film nicht gesehen habe, sodass die "Magie" dieses Ansatzes in der Verwendung von Teilanwendungen liegen kann.

Ich glaube, Greg hat Ihre Frage beantwortet. Sie sollten zu diesem Ansatz wechseln, wenn Sie glauben, in einer Situation zu sein, in der Sie Ihren Junior-Entwicklern übermäßig viel Zeit damit verbringen, AOP-Frameworks zu erklären. IMO, wenn Sie in diesem Boot sind, stellen Sie wahrscheinlich die falschen Junior-Entwickler ein. Ich glaube nicht, dass AOP einen deklarativen Ansatz erfordert, aber für mich ist es aus gestalterischer Sicht viel klarer und nicht-invasiver.

kakridge
quelle
+1 für "Ich habe mehr Zeit damit verbracht, den Einzelparameteransatz zu verstehen als den deklarativen Ansatz." Ich fand das IConsume<T>Beispiel zu kompliziert für das, was erreicht wurde.
Scott Whitlock
4

Sofern mir nichts fehlt, ist der Code, den Sie gezeigt haben, das Entwurfsmuster der Verantwortungskette, das sich hervorragend eignet, wenn Sie eine Reihe von Aktionen für ein Objekt ausführen müssen (z. B. Befehle, die eine Reihe von Befehlshandlern durchlaufen) Laufzeit.

AOP mit PostSharp ist gut, wenn Sie beim Kompilieren wissen, welches Verhalten Sie hinzufügen möchten. Das Code-Weben von PostSharp bedeutet so gut wie, dass der Zeitaufwand für die Ausführung Null ist, und hält den Code in der Tat sehr sauber (insbesondere, wenn Sie anfangen, Dinge wie Multicast-Aspekte zu verwenden). Ich denke nicht, dass die grundlegende Verwendung von PostSharp besonders schwierig zu erklären ist. Der Nachteil von PostSharp ist, dass sich die Kompilierzeiten erheblich verlängern.

Ich verwende beide Techniken im Produktionscode und obwohl es einige Überlappungen gibt, in denen sie angewendet werden können, denke ich, dass sie größtenteils wirklich auf verschiedene Szenarien abzielten.

FinnNk
quelle
4

In Bezug auf seine Alternative - dort gewesen, das getan. Nichts ist vergleichbar mit der Lesbarkeit eines einzeiligen Attributs.

Geben Sie neuen Leuten einen kurzen Vortrag und erklären Sie ihnen, wie die Dinge in AOP funktionieren.

Danny Varod
quelle
4

Was Greg beschreibt, ist absolut vernünftig. Und auch darin steckt Schönheit. Das Konzept ist in einem anderen Paradigma anwendbar als die reine Objektorientierung. Es ist eher ein prozeduraler Ansatz oder ein Flow-orientierter Design-Ansatz. Wenn Sie also mit Legacy-Code arbeiten, ist die Anwendung dieses Konzepts sehr schwierig, da möglicherweise umfangreiche Umgestaltungen erforderlich sind.

Ich werde versuchen, ein anderes Beispiel zu geben. Vielleicht nicht perfekt, aber ich hoffe, es macht den Punkt klarer.

Wir haben also einen Produktservice, der ein Repository verwendet (in diesem Fall verwenden wir einen Stub). Der Service erhält eine Liste der Produkte.

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

Natürlich können Sie auch eine Schnittstelle an den Dienst übergeben.

Als nächstes möchten wir eine Liste der Produkte in einer Ansicht anzeigen. Dafür brauchen wir eine Schnittstelle

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

und einen Befehl, der die Liste der Produkte enthält

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

und die Aussicht

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

Jetzt brauchen wir Code, der all dies ausführt. Dies machen wir in einer Klasse namens Application. Die Run () -Methode ist die integrierende Methode, die keine oder nur sehr wenig Geschäftslogik enthält. Die Abhängigkeiten werden als Methoden in den Konstruktor eingefügt.

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

Zuletzt erstellen wir die Anwendung in der Hauptmethode.

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

Das Coole ist nun, dass wir Aspekte wie Protokollierung oder Ausnahmebehandlung hinzufügen können, ohne den vorhandenen Code zu berühren und ohne ein Framework oder Anmerkungen. Für die Ausnahmebehandlung fügen wir zB einfach eine neue Klasse hinzu:

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

Und dann stecken wir es während der Komposition am Einstiegspunkt der Anwendung zusammen. Wir müssen nicht einmal den Code in der Application-Klasse berühren. Wir ersetzen nur eine Zeile:

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

Um es wieder aufzunehmen: Wenn wir ein flussorientiertes Design haben, können wir Aspekte hinzufügen, indem wir die Funktionalität innerhalb einer neuen Klasse hinzufügen. Dann müssen wir eine Zeile in der Kompositionsmethode ändern und das wars.

Ich denke, eine Antwort auf Ihre Frage ist, dass Sie nicht einfach von einem Ansatz zum anderen wechseln können, sondern sich entscheiden müssen, welchen architektonischen Ansatz Sie in Ihrem Projekt verfolgen.

edit: Eigentlich habe ich gerade gemerkt, dass das Teilanwendungsmuster, das für den Produktservice verwendet wird, die Dinge etwas komplizierter macht. Wir müssen eine weitere Klasse um die Product-Service-Methode wickeln, um auch hier Aspekte hinzufügen zu können. Es könnte so etwas sein:

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

Die Komposition muss dann folgendermaßen geändert werden:

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
quelle