Програмно-орієнтоване програмування: коли почати використовувати рамку?


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" рамку AOP? Я серйозно досі не маю поняття, в чому його суть. Як тільки ви скажете "Усі методи повинні приймати рівно один параметр", ви не змогли? На цьому етапі ви говорите: «ОК», це накладає серйозні штучні обмеження на мою здатність писати програмне забезпечення, давайте відкинемо це вже раніше, на три місяці, у нас є повна база кодерів кошмару.

А ви знаєте що? За допомогою Mono.Cecil ви можете легко записати простий фреймворк, заснований на ІЛ, атрибуті, заснованому на IL . (тестувати його трохи складніше, але ...)

О та IMO, якщо ви не використовуєте атрибути, це не AOP. Вся суть у виконанні методу введення / виходу журналу реєстрації на етапі після процесора полягає в тому, що він не псується з вашими кодовими файлами ans, тому вам не потрібно думати про це під час рефакторингу коду; що є його потужність.

Все, що Грег продемонстрував, є дурна парадигма дурного.


6
+1 за те, щоб не було дурним дурнем. Нагадує мені відому цитату Ейнштейна: "зробіть все максимально просто, але не простіше".
Рей Міясака

FWIW, F # має однакове обмеження, кожен метод бере щонайменше один аргумент.
R0MANARMY

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

2
@ Рот - що насправді відбувається, це те, що concat "Hello, "ти фактично створюєш функцію, яка займає просто y, і xпопередньо визначила як локальне прив'язування до "Привіт". Якби цю проміжну функцію можна було побачити, вона виглядала б приблизно так let concat_x y = "Hello, " + y. А потім слідуючи за цим, ви телефонуєте concat_x "World!". Синтаксис робить його менш очевидним, але це дозволяє «запікати» нові функції - наприклад, let printstrln = print "%s\n" ;; printstrln "woof". Крім того, навіть якщо ви робите щось подібне let f(x,y) = x + y, це насправді лише один аргумент кортежу .
Rei Miyasaka

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 .

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

Я також не згоден з тим, щоб уникати рамок задля того, щоб можна було пояснити речі юніорам. Це не приносить їм користі. Якщо Грег ставиться до своїх юніорів так, як він пропонує до них ставитися, як до ідіотів з товстими скульптурами, то я підозрюю, що і його старші розробники теж не дуже великі, оскільки, напевно, їм не дано багато можливості навчитися чомусь під час їх молодші роки.


5

Я робив незалежне навчання в коледжі на АОП. Я фактично написав документ про підхід до моделювання AOP із плагіном Eclipse. Я думаю, це насправді дещо неактуально. Ключові моменти: 1) Я був молодим та недосвідченим та 2) працював з AspectJ. Я можу вам сказати, що "магія" більшості фреймворків AOP не така вже й складна. Я фактично працював над проектом приблизно в той же час, який намагався зробити підхід з єдиним параметром, використовуючи хешблет. ІМО, єдиний параметричний підхід насправді є рамкою і є інвазивним. Навіть на цій посаді я витратив більше часу, намагаючись зрозуміти підхід одного параметра, ніж переглядав декларативний підхід. Я додам застереження про те, що я не переглядав фільм, тому "магія" такого підходу може полягати у використанні часткових додатків.

Я думаю, Грег відповів на ваше запитання. Ви повинні перейти на такий підхід, коли вважаєте, що опинилися в ситуації, коли витрачаєте зайву кількість часу, пояснюючи рамки AOP своїм молодшим розробникам. IMO, якщо ви перебуваєте в цьому човні, ви, мабуть, наймаєте неправильних молодших розробників. Я не вірю, що АОП вимагає декларативного підходу, але для мене це просто набагато чіткіше і неінвазивне з точки зору дизайну.


+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.