Programowanie zorientowane na aspekt: ​​Kiedy zacząć korzystać z frameworka?

22

Właśnie oglądałem tę rozmowę przez Grega Younga ostrzegania ludzi całować: Keep It Simple głupie.

Jedną z rzeczy, zasugerował, że aby zrobić programowanie aspektowe, jeden ma nie potrzebują ramy .

Zaczyna od silnego ograniczenia: wszystkie metody przyjmują jeden i tylko jeden parametr (choć nieco później rozluźnia to, stosując częściową aplikację ).

Podaje przykład, aby zdefiniować interfejs:

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

Jeśli chcemy wydać polecenie:

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

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

Polecenie jest realizowane jako:

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

Aby zalogować się do konsoli, wystarczy zaimplementować:

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

Następnie rejestrowanie przed komendą, obsługa komend i rejestrowanie po komendach są następujące:

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

a polecenie jest wykonywane przez:

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

Aby to zrobić na przykład w PostSharp , należy opisać w CommandServicenastępujący sposób:

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

A potem trzeba zaimplementować rejestrowanie w klasie atrybutów, takiej jak:

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

Argument, który wykorzystuje Greg, jest taki, że połączenie atrybutu z implementacją atrybutu to „zbyt dużo magii”, aby móc wyjaśnić, co dzieje się z młodszym programistą. Początkowy przykład to „tylko kod” i można go łatwo wyjaśnić.

Tak więc po tym dość długotrwałym narastaniu pojawia się pytanie: kiedy przejdziesz z nieszkieletowego podejścia Grega do używania czegoś takiego jak PostSharp dla AOP?

Peter K.
źródło
3
+1: Zdecydowanie dobre pytanie. Można po prostu powiedzieć „… kiedy już zrozumiesz rozwiązanie bez niego”.
Steven Evers,
1
Może po prostu nie jestem przyzwyczajony do tego stylu, ale pomysł napisania całej takiej aplikacji wydaje mi się całkowicie szalony. Wolałbym użyć metody przechwytującej.
Aaronaught
@Aaronaught: Tak, właśnie dlatego chciałem tutaj pisać. Wyjaśnienie Grega jest takie, że konfiguracja systemu po prostu łączy W KODIE NORMALNYM wszystkie różne IConsumeselementy. Zamiast korzystać z zewnętrznego XML lub jakiegoś płynnego interfejsu --- jeszcze jedna rzecz do nauczenia się. Można argumentować, że ta metodologia to także „kolejna rzecz do nauczenia się”.
Peter K.
Nadal nie jestem pewien, czy rozumiem motywację; sama istota pojęć takich jak AOP polega na deklaratywnym wyrażaniu obaw , tzn. poprzez konfigurację. Dla mnie to tylko nowe odkrycie kwadratowego koła. Nie krytyka ciebie ani twojego pytania, ale myślę, że jedyną sensowną odpowiedzią jest: „Nigdy nie zastosowałbym podejścia Grega, chyba że zawiodłyby wszystkie inne opcje”.
Aaronaught
Nie to, że w ogóle mi to przeszkadza, ale czy nie byłoby to trochę więcej pytań o przepełnienie stosu?
Rei Miyasaka,

Odpowiedzi:

17

Czy on próbuje napisać strukturę AOP „prosto do TDWTF”? Poważnie wciąż nie mam pojęcia, o co mu chodziło. Jak tylko powiesz „Wszystkie metody muszą przyjąć dokładnie jeden parametr”, to nie udało ci się, prawda? Na tym etapie mówisz: OK, to nakłada pewne poważne sztuczne ograniczenia na moją zdolność do pisania oprogramowania, zostawmy to teraz przedtem, trzy miesiące później mamy pełną koszmarną bazę kodu do pracy.

I wiesz co? Za pomocą Mono.Cecil możesz dość łatwo napisać prostą strukturę rejestrowania opartą na IL opartą na IL . (testowanie jest nieco bardziej skomplikowane, ale ...)

Aha i IMO, jeśli nie używasz atrybutów, to nie jest AOP. Cały sens wprowadzania kodu wejścia / wyjścia metody rejestrowania na etapie postprocesora polega na tym, że nie powoduje on bałaganu w plikach kodu i nie trzeba się nad tym zastanawiać, zmieniając kod; taka jest jego moc.

Wszystko, co Greg pokazał, to głupi, głupi paradygmat.


źródło
6
+1 za zachowanie głupie głupie. Przypomina mi słynny cytat Einsteina: „uczyń wszystko tak prostym, jak to możliwe, ale nie prostszym”.
Rei Miyasaka,
FWIW, F # ma to samo ograniczenie, każda metoda przyjmuje maksymalnie jeden argument.
R0MANARMY
1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;wygląda na to, że potrzeba dwóch argumentów, czego mi brakuje?
2
@ Usta - tak naprawdę dzieje się tak, że concat "Hello, "tworzysz funkcję, która jest sprawiedliwa yi xwstępnie zdefiniowana jako lokalne powiązanie na „Cześć”. Gdyby można było zobaczyć tę funkcję pośrednią, wyglądałaby ona jak let concat_x y = "Hello, " + y. A potem dzwonisz concat_x "World!". Składnia czyni to mniej oczywistym, ale pozwala to „upiec” nowe funkcje - na przykład let printstrln = print "%s\n" ;; printstrln "woof". Ponadto, nawet jeśli robisz coś takiego let f(x,y) = x + y, to tak naprawdę tylko jeden argument krotki .
Rei Miyasaka,
1
Kiedy pierwszy raz programowałem funkcjonalnie w Mirandzie na uniwersytecie, muszę rzucić okiem na F #, to brzmi interesująco.
8

Mój Boże, ten facet jest nie do zniesienia. Chciałbym po prostu przeczytać kod z twojego pytania zamiast oglądać tę rozmowę.

Nie sądzę, żebym kiedykolwiek użył tego podejścia, gdyby tylko ze względu na użycie AOP. Greg mówi, że nadaje się do prostych sytuacji. Oto, co zrobiłbym w prostej sytuacji:

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

Tak, zrobiłem to, całkowicie pozbyłem się AOP! Czemu? Ponieważ nie potrzebujesz AOP w prostych sytuacjach .

Z punktu widzenia programowania funkcjonalnego, dopuszczenie tylko jednego parametru na funkcję tak naprawdę mnie nie przeraża. Niemniej jednak, to naprawdę nie jest projekt, który działa dobrze w C # - i sprzeczanie się z ziarnami twojego języka niczego nie całuje.

Użyłbym tego podejścia tylko wtedy, gdyby konieczne było stworzenie modelu poleceń na początek, na przykład gdybym potrzebował stosu cofania lub gdybym pracował z poleceniami WPF .

W przeciwnym razie użyłbym ramy lub refleksji. PostSharp działa nawet w Silverlight i Compact Framework - więc to, co nazywa „magia” naprawdę nie jest magiczne w ogóle .

Nie zgadzam się również z unikaniem ram, aby móc wyjaśnić rzeczy młodym. To im nie pomaga. Jeśli Greg traktuje swoich juniorów tak, jak sugeruje, by byli traktowani jak idioci z grubymi czaszkami, to podejrzewam, że jego starsi programiści też nie są wspaniali, ponieważ prawdopodobnie nie mieli oni okazji dowiedzieć się czegoś podczas ich młodsze lata.

Rei Miyasaka
źródło
5

Zrobiłem niezależne badanie na studiach na temat AOP. Właściwie napisałem artykuł na temat podejścia do modelowania AOP z wtyczką Eclipse. To chyba trochę nieistotne. Kluczowe punkty to: 1) byłem młody i niedoświadczony oraz 2) pracowałem z AspectJ. Mogę powiedzieć, że „magia” większości platform AOP nie jest tak skomplikowana. Właściwie pracowałem nad projektem mniej więcej w tym samym czasie, w którym próbowałem zastosować podejście jednoparametrowe za pomocą tablicy hashtable. IMO, podejście jednoparametrowe jest naprawdę strukturą i jest inwazyjne. Nawet w tym poście spędziłem więcej czasu próbując zrozumieć podejście jednoparametrowe niż przeglądając podejście deklaratywne. Dodam zastrzeżenie, że nie oglądałem filmu, więc „magią” tego podejścia może być zastosowanie częściowych aplikacji.

Myślę, że Greg odpowiedział na twoje pytanie. Powinieneś przejść na to podejście, jeśli uważasz, że znajdujesz się w sytuacji, w której spędzasz zbyt dużo czasu na wyjaśnianiu struktur AOP młodszym programistom. IMO, jeśli jesteś na tej łodzi, prawdopodobnie zatrudniasz niewłaściwych młodszych programistów. Nie sądzę, aby AOP wymagało deklaratywnego podejścia, ale dla mnie jest to po prostu o wiele bardziej jasne i nieinwazyjne z punktu widzenia projektowania.

Kakridge
źródło
+1 za „Spędziłem więcej czasu próbując zrozumieć podejście jednoparametrowe, niż przeglądałem podejście deklaratywne”. Uznałem, że IConsume<T>przykład jest zbyt skomplikowany z punktu widzenia tego, co zostało osiągnięte.
Scott Whitlock,
4

O ile mi czegoś brakuje, kod, który pokazałeś, jest wzorcem projektowym „łańcucha odpowiedzialności”, który jest świetny, jeśli potrzebujesz połączyć szereg działań na obiekcie (takich jak polecenia przechodzące przez szereg programów obsługi poleceń) w środowisko uruchomieniowe.

AOP korzystający z PostSharp jest dobry, jeśli wiesz w czasie kompilacji, jakie zachowanie chcesz dodać. Tkanie kodu PostSharp praktycznie oznacza, że ​​nie ma narzutu w czasie wykonywania i rzeczywiście utrzymuje kod w czystości (zwłaszcza, gdy zaczynasz używać rzeczy takich jak aspekty multiemisji). Nie sądzę, że podstawowe użycie PostSharp jest szczególnie skomplikowane do wyjaśnienia. Minusem PostSharp jest to, że znacząco skraca czas kompilacji.

Używam obu technik w kodzie produkcyjnym i chociaż istnieje pewne nakładanie się ich zastosowania, myślę, że w większości dotyczyły one różnych scenariuszy.

FinnNk
źródło
4

Jeśli chodzi o jego alternatywę - byłem tam, zrobiłem to. Nic nie jest porównywalne z czytelnością atrybutu jednowierszowego.

Wyprowadź krótkiego wykładu nowym chłopakom, wyjaśniając im, jak działają rzeczy w AOP.

Danny Varod
źródło
4

To, co opisuje Greg, jest absolutnie uzasadnione. I jest w tym także piękno. Ta koncepcja ma zastosowanie w innym paradygmacie niż orientacja czysto obiektowa. To bardziej podejście proceduralne lub podejście projektowe zorientowane na przepływ. Tak więc, jeśli pracujesz ze starszym kodem, zastosowanie tej koncepcji będzie dość trudne, ponieważ może być konieczne dużo refaktoryzacji.

Spróbuję podać inny przykład. Może nie jest idealny, ale mam nadzieję, że to wyjaśnia sprawę.

Mamy więc usługę produktu, która korzysta z repozytorium (w tym przypadku użyjemy kodu pośredniczącego). Usługa otrzyma listę produktów.

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

Oczywiście możesz również przekazać interfejs do usługi.

Następnie chcemy wyświetlić listę produktów w widoku. Dlatego potrzebujemy interfejsu

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

oraz polecenie, które przechowuje listę produktów

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

i widok

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

Teraz potrzebujemy kodu, który wykonuje to wszystko. Zrobimy to w klasie o nazwie Aplikacja. Metoda Run () jest metodą integrującą, która nie zawiera logiki biznesowej lub jest w niej bardzo mało. Zależności są wprowadzane do konstruktora jako metody.

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

Na koniec tworzymy aplikację w głównej metodzie.

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

Fajną rzeczą jest to, że możemy dodawać takie aspekty, jak rejestrowanie lub obsługa wyjątków bez dotykania istniejącego kodu i bez ram lub adnotacji. Do obsługi wyjątków np. Dodajemy nową klasę:

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

A następnie łączymy to razem podczas komponowania w punkcie wejścia aplikacji. nie musimy nawet dotykać kodu w klasie Application. Po prostu zastępujemy jedną linię:

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

Aby wznowić: Gdy mamy projekt zorientowany na przepływ, możemy dodać aspekty, dodając funkcjonalność w nowej klasie. Następnie musimy zmienić jedną linię w metodzie kompozycji i to wszystko.

Myślę więc, że odpowiedź na twoje pytanie brzmi: nie możesz łatwo przejść z jednego podejścia do drugiego, ale musisz zdecydować, jakie podejście architektoniczne zastosujesz w swoim projekcie.

edycja: Właściwie właśnie zdałem sobie sprawę, że częściowy wzorzec aplikacji użyty z usługą produktu czyni sprawy nieco bardziej skomplikowanymi. Musimy owinąć kolejną klasę wokół metody obsługi produktu, aby również tutaj dodać aspekty. Może to być coś takiego:

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

Następnie należy zmienić skład w następujący sposób:

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
źródło