Аспектно-ориентированное программирование: когда начинать использовать фреймворк?


22

Я просто смотрел этот разговор по Greg Молодых предупреждений людей к ПОЦЕЛУЮ: Keep It Simple Stupid.

Одна из вещей , которые он предложил, что делать аспектно-ориентированное программирование, один вовсе не нужны рамки .

Он начинает с строгого ограничения: все методы принимают один и только один параметр (хотя он ослабляет это чуть позже, используя частичное применение ).

Пример, который он приводит, - определить интерфейс:

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

Если мы хотим выполнить команду:

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

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

Команда реализована так:

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

Чтобы войти в консоль, нужно просто выполнить:

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

Затем ведение журнала перед командой, служба команд и запись после команды просто:

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

и команда выполняется:

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

Чтобы сделать это, например, в PostSharp , можно аннотировать CommandServiceтак:

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

И тогда придется реализовать запись в класс атрибута что-то вроде:

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

Аргумент, который использует Грег, заключается в том, что связь между атрибутом и реализацией атрибута «слишком большая магия», чтобы можно было объяснить, что происходит с младшим разработчиком. Начальный пример - это просто код и его легко объяснить.

Итак, после этого довольно затянувшегося процесса создания возникает вопрос: когда вы переключаетесь с неструктурного подхода Грега на использование чего-то вроде PostSharp для AOP?


3
+1: определенно хороший вопрос. Кто-то может просто сказать «... когда вы уже понимаете решение без него».
Стивен Эверс

1
Может быть, я просто не привык к стилю, но сама идея написать такое приложение кажется мне совершенно безумной. Я бы лучше использовал метод перехватчик.
Aaronaught

@Aaronaught: Да, это часть того, почему я хотел опубликовать здесь. Грег объясняет, что конфигурация системы просто соединяет В НОРМАЛЬНОМ КОДЕ все разные IConsumesчасти. Вместо того, чтобы использовать внешний XML или некоторый интерфейс Fluent - еще одна вещь для изучения. Можно утверждать, что эта методология также «еще одна вещь для изучения».
Питер К.

Я все еще не уверен, что понимаю мотивацию; Сама суть таких понятий, как AOP, заключается в том, чтобы иметь возможность декларативно выражать озабоченность , т.е. Для меня это просто изобретать квадратное колесо. Не критика вас или вашего вопроса, но я думаю, что единственный разумный ответ: «Я бы никогда не воспользовался подходом Грега, если бы не проиграл любой другой вариант».
Aaronaught

Не то чтобы это меня вообще беспокоило, но разве это не было бы скорее вопросом переполнения стека?
Рей Миясака

Ответы:


17

Пытается ли он написать фреймворк AOP "прямо в TDWTF"? Я серьезно до сих пор не понимаю, в чем его смысл. Как только вы говорите «Все методы должны принимать ровно один параметр», значит, вы потерпели неудачу, не так ли? На этом этапе вы говорите: «ОК», это накладывает некоторые серьезные искусственные ограничения на мою способность писать программы, давайте отбросим это сейчас, через три месяца, у нас будет полная ночная кошмарная кодовая база для работы.

И знаешь, что? С помощью Mono.Cecil вы можете легко написать простую инфраструктуру ведения журналов на основе атрибутов, основанную на IL . (тестирование это немного сложнее, но ...)

Да, и IMO, если вы не используете атрибуты, это не AOP. Весь смысл создания кода входа / выхода из метода на этапе постпроцессора заключается в том, чтобы он не мешал вашим файлам кода и не требовал думать об этом при рефакторинге кода; что является его мощность.

Все, что Грег продемонстрировал, - это глупая парадигма.


6
+1 за глупость. Напоминает мне знаменитую цитату Эйнштейна: «Сделай все как можно проще, но не проще».
Рей Миясака

FWIW, F # имеет то же ограничение, каждый метод принимает не более одного аргумента.
R0MANARMY

1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;Похоже, что требуется два аргумента, что мне не хватает?

2
@ The Mouth - на самом деле происходит то, что concat "Hello, "когда вы на самом деле создаете функцию, которая принимает просто yи имеет xпредопределенную локальную привязку, которая будет «Hello». Если бы эту промежуточную функцию можно было увидеть, она бы выглядела примерно так let concat_x y = "Hello, " + y. И после этого вы звоните concat_x "World!". Синтаксис делает его менее очевидным, но это позволяет «запекать» новые функции - например, let printstrln = print "%s\n" ;; printstrln "woof". Кроме того, даже если вы делаете что-то подобное let f(x,y) = x + y, на самом деле это всего лишь один аргумент кортежа .
Рей Миясака

1
Когда я работал над функциональным программированием в Миранде, еще в университете, мне нужно взглянуть на F #, это звучит интересно.

8

Боже мой, этот парень невыносимо абразивен. Я хотел бы просто прочитать код в вашем вопросе, а не смотреть этот разговор.

Я не думаю, что когда-либо использовал бы этот подход, если бы только ради использования AOP. Грег говорит, что это хорошо для простых ситуаций. Вот что я бы сделал в простой ситуации:

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

Да, я сделал это, я полностью избавился от АОП! Зачем? Потому что вам не нужен АОП в простых ситуациях .

С точки зрения функционального программирования, использование только одного параметра для каждой функции меня не пугает. Тем не менее, это действительно не тот дизайн, который хорошо работает с C # - и идти вразрез с языком вашего языка ничего не целует.

Я бы использовал этот подход только в том случае, если для начала нужно было создать модель команд, например, если мне нужен стек отмены или я работал с WPF Commands .

В противном случае я бы просто использовал рамки или некоторые отражения. PostSharp работает даже в Silverlight и Compact Framework - так , что он называет «волшебной» на самом деле не магическая вообще .

Я также не согласен с тем, чтобы избегать фреймворков ради возможности объяснить вещи юниорам. Это не приносит им никакой пользы. Если Грег относится к своим младшим так, как он предлагает, чтобы они относились к ним, как к тупым идиотам, то я подозреваю, что его старшие разработчики тоже не очень хороши, поскольку им, вероятно, не дали особой возможности чему-то научиться во время их младшие годы.


5

Я сделал самостоятельное обучение в колледже на АОП. На самом деле я написал статью о подходе к модели AOP с помощью подключаемого модуля Eclipse. Это на самом деле несколько не имеет значения, я полагаю. Ключевые моменты: 1) я был молод и неопытен и 2) я работал с AspectJ. Я могу вам сказать, что «магия» большинства фреймворков AOP не так уж сложна. Я фактически работал над проектом примерно в то же время, когда пытался использовать подход с одним параметром, используя хеш-таблицу. ИМО, подход с одним параметром действительно является структурой, и это агрессивно. Даже в этом посте я потратил больше времени, пытаясь понять подход с одним параметром, чем анализируя декларативный подход. Я добавлю предостережение о том, что я не смотрел фильм, поэтому «магия» этого подхода может заключаться в использовании частичных приложений.

Я думаю, что Грег ответил на ваш вопрос. Вам следует переключиться на этот подход, когда вы думаете, что находитесь в ситуации, когда вы тратите слишком много времени на объяснение сред AOP для своих младших разработчиков. ИМО, если вы находитесь в этой лодке, вы, вероятно, нанимаете не тех младших разработчиков. Я не верю, что АОП требует декларативного подхода, но для меня это просто намного яснее и неинвазивен с точки зрения дизайна.


+1 за «Я потратил больше времени, пытаясь понять подход с одним параметром, чем проверяя декларативный подход». Я нашел IConsume<T>пример слишком сложным для того, что делается.
Скотт Уитлок

4

Если я не пропустил что-то, то код, который вы показали, является шаблоном проектирования «цепочки ответственности», который хорош, если вам нужно связать последовательность действий над объектом (например, команды, проходящие через серию обработчиков команд) в во время выполнения.

Использование AOP с помощью PostSharp хорошо, если во время компиляции вы знаете, какое поведение вы хотите добавить. Плетение кода в PostSharp в значительной степени означает отсутствие накладных расходов во время выполнения и делает код действительно чистым (особенно, когда вы начинаете использовать такие вещи, как многоадресные аспекты). Я не думаю, что базовое использование PostSharp особенно сложно объяснить. Недостатком PostSharp является то, что он значительно увеличивает время компиляции.

Я использую оба метода в производственном коде, и хотя есть некоторые совпадения в том, где они могут быть применены, я думаю, что по большей части они действительно нацелены на разные сценарии.


4

Что касается его альтернативы - был там, сделал это. Ничто не сравнится с удобочитаемостью однострочного атрибута.

Прочитайте небольшую лекцию новым парням, объяснив им, как все работает в АОП.


4

То, что описывает Грег, абсолютно разумно. И в этом тоже есть красота. Концепция применима в иной парадигме, чем чисто объектная ориентация. Это скорее процедурный подход или ориентированный на поток подход к проектированию. Поэтому, если вы работаете с унаследованным кодом, применить эту концепцию будет довольно сложно, поскольку может потребоваться много рефакторинга.

Я постараюсь привести еще один пример. Может быть, не идеально, но я надеюсь, что это проясняет ситуацию.

Таким образом, у нас есть служба продукта, которая использует репозиторий (в этом случае мы будем использовать заглушку). Сервис получит список товаров.

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

Конечно, вы также можете передать интерфейс для службы.

Далее мы хотим показать список продуктов в представлении. Поэтому нам нужен интерфейс

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

и команда, которая содержит список продуктов

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

и вид

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

Теперь нам нужен код, который выполняет все это. Это мы будем делать в классе под названием Application. Метод Run () - это интегрирующий метод, который не содержит или, по крайней мере, очень мало бизнес-логики. Зависимости вводятся в конструктор как методы.

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

Наконец, мы составляем приложение в основном методе.

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

Крутая вещь в том, что мы можем добавлять такие аспекты, как ведение журнала или обработка исключений, не затрагивая существующий код и не создавая структуру или аннотации. Например, для обработки исключений мы просто добавляем новый класс:

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

А затем мы подключаем его вместе во время композиции в точке входа приложения. нам даже не нужно трогать код в классе Application. Мы просто заменим одну строку:

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

Итак, возобновим: когда у нас есть ориентированный на поток дизайн, мы можем добавлять аспекты, добавляя функциональность в новый класс. Затем мы должны изменить одну строку в методе композиции и все.

Поэтому я думаю, что ответ на ваш вопрос заключается в том, что вы не можете легко переключиться с одного подхода на другой, но вам нужно решить, какой архитектурный подход вы выберете в своем проекте.

редактировать: На самом деле я только что понял, что частичный шаблон приложения, используемый со службой продукта, делает вещи немного сложнее. Нам нужно обернуть другой класс вокруг метода обслуживания продукта, чтобы иметь возможность добавлять аспекты и здесь. Это может быть что-то вроде этого:

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

Затем состав должен быть изменен следующим образом:

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);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.