Аспект. Найти и икапсулировать изменчивость на стыке областей

1fa3b2ea6c774d9a9753380f531491d2.png В данном посте хочу опубликовать свои мысли о понятии изменчивости в сложных программных продуктах, которые распространяются копиями и обслуживают разные, непохожие друг на друга предприятия (Enterprise решения). Я очень обрадовался, когда под микроскопом рассмотрел эту самую изменчивость и заметил там «аспект».

Аспект — это часть программы, которая обслуживает стык нескольких предметных областей и вносит элемент уникальности в них (через программирование изменчивости). Именно там, на стыке, можно найти что-то новое, инновационное, уникальное. Часто успешные бизнесы держатся на этих маленьких, но уникальных особенностях известного во всем мире процесса, и ни одно из существующих архаичных IT решений не может быть интегрировано на такое инновационное предприятие.

Аспект вносит коррективы в известные области, добавляет изменчивости в давно знакомый процесс. Очень часто нельзя вот так просто взять и подтянуть готовый, устраивающий бизнес модуль, его просто не существует. Все модули устраивают на 99%, но без оставшегося 1% ничего не выйдет, в нем вся соль и отличие от конкурентов. Очень часть нельзя разработать правильное и всеобъемлющее ТЗ на всю систему и все ее модули сразу, приходится идти по «туману войны» и обучаться, натыкаясь на трудности. Аспект поможет избавиться от практики переписывания всего проекта или отдельной части с нуля после каждой набитой шишки.

В статье аспект рассматривается на примере ООП. Но вообще, есть такое понятие, как АОП (аспектно-ориентированное программирование), адекватной и понятной статьи про это на русском языке я не нашел.

Место аспекта в ООП


Попытаюсь в коде ниже показать, где находится аспект в ООП. Точнее место, куда бьет аспект.
//Это интерфейс (абстракция)
interface ShoppingCartInterface {
    public function put(CartElement $element);
    public function get();
    public function getCost();
}

//А ниже - две реализации
class CookieCart implements ShoppingCartInterface {
    public function put(CartElement $element) { //... записываем в куки элемент}
    public function get() { //... забираем все из кук }
    public function getCost() { //считаем сумму заказа }
}

class DbCart implements ShoppingCartInterface  {
    public function put(CartElement $element) { //...кладем в БД элемент}
    public function get() { //... забираем все из БД}
    public function getCost() { //считаем сумму заказа }
}

Место, с которым взаимодействует аспект — методы реализации. Если программист следует принципам SOLID, каждый его метод реализует только одну обязанность. Например, getCost должен только возвращать сумму товаров корзины, а все остальное (например, логгер событий и проверка доступа) должно быть где-то в другом месте. Так вот, где-то в другом месте существует и аспект, он знает о всех модулях и дает указания getCost, как считать сумму, учитывая условия всей среды, учитывая мнение всех участников процесса, учитывая условия конкретной сложившийся ситуации (например, рождественские скидки). Аспект может быть поддержан только теми реализациями, что позволяют влиять на свое поведение извне. Может получить так, что DbCart прослужит 10 лет развития проекта, так как поддерживает аспект, а CookieCart только 1 год, хотя обе реализации в общем-то полиморфны.

Место аспекта в приложении


Аспект проще всего увидеть и применить в приложениях с модульной архитектурой, там четко просматривается нужное место для него (DiC). Любой хороший модуль представляет из себя некую большую реализацию с узким горлышком (сервисом), именно в сервис должен инжектиться аспект.

Именно на модулях чаще всего работают сложные проекты, каждый модуль там обслуживает какую-то одну предметную область, которая в принципе уже неплохо изучена и понятна всем. В модулях необходимо оставить возможность инжектить новизну из приложения. Задача приложения — хранить в себе зависимости всех модулей от всех, их настройки и немного логики, которая описывает уникальность проекта, что-то очень узкое. Множество копий одной программы могут использовать одни и те же модули, но работать все эти копии будут совершенно по-разному, с помощью статических настроек задать параметры поведения порой просто нереально, настолько много вариаций.

Разрабатывать универсальные модули могут только хорошие и опытные программисты. А что отличает хорошего программиста от плохого? Вот мои критерии:

  1. Дублирование кода. Важно писать код так, чтобы его куски не приходилось каждый раз переносить вручную и адаптировать под «здесь и сейчас», все должно быть по щелчку пальцев, одной настройкой и в одном месте.
  2. Переносимость кода. Любой единожды написанный функционал должен быть инкапсулирован от системы. Тогда этот функционал можно взять и просто перенести куда угодно, не нужно тратить кучу времени на интеграцию и обрубание лишнего.
  3. Связанность кода. Функциональные части системы должны быть слабо связаны между собой. «Мохнатые уши» не должны зависеть от конкретной кошки и ее головы. Если мы однажды описали уши, то они должны быть полиморфны, то есть применимы к любому животному, которое соответствует интерфейсу Animal.

У меня получился набор критериев «ДПС», но мейнстримом является набор принципов, аббревиатура SOLID. SOLID-модули можно легко переносить из проекта в проект, тестировать, продавать и распространять в Open Source. Информация о всех модулях приложения и о их зависимостях друг от друга правильно хранить в том самом размытом «отдельном месте» приложения (DiC и ServiceLocator). В DiC хранится уникальность приложения, он формирует предметную область приложения. Как правило, в любой SOLID код, немного разобравшись с ним, можно с легкостью заинжектить аспект. Чем меньше модуль следует принципам SOLID, тем выше вероятность того, что мы рано или поздно вместо очередного костыля и отговорки решим просто написать свой модуль, заточенный под наши требования.

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

Инкапсуляция изменчивости


Чтобы мы могли как-то изменять поведение модуля из предметной области, где он установлен, необходимо отделить (инкапсулировать) всю изменчивость. Приложению необходимо дать возможность давать указания во время выполнения какого-либо действия в модуле. В случае с корзиной — в методе getCost мы должны вставить некий портал, куда приложение может получить доступ и подсказать, что число $cost сейчас должно быть на 10% меньше, чем есть на самом деле, ведь услугами хочет воспользоваться постоянный клиент со скидкой. А за клиентов отвечает совсем другой модуль. Задачей приложения является проследить момент getCost, узнать у модуля Client, есть ли и у покупателя скидка, и дать нужные указания. Такие указания и называются аспектом.

Резюме: аспект находится внутри DiC и дает указание в точках соприкосновения модулей: что делать сейчас. Модуль ShoppingCart и модуль Client не знают о существовании друг-друга (они инкапсулированы друг от друга), они написаны разными людьми и работают вместе только потому что следуют принципам SOLID и потому что поддерживают принятие аспектов.

Когда мы поняли, что такое изменчивость и аспект, нужно перейти к действию — инкапсулировать эту самую изменчивость, чтобы перенести из модуля в проект. Лучше всего реализацию аспекта можно представить в виде callback функции, которая подписана на триггер в каком-то сервисе. Функция получает данные и внедряет свои коррективы в поведение программы модуля здесь и сейчас. Пример из ServiceLocator фреймворка YII2

//yii::$app - ServiceLocator
//$data - простейший DataProvider
yii::$app->set('cart', 'vasya/shoppingcart/Service'); //регистрация в качестве сервиса компонента, поддерживающего события

//Далее, подписываемся на событие
yii::$app->get('cart')->on('cost_calculate', function($data) {
    if($app->client->hasDiscount()) {
        //Применяем скидки
        $data->cost = $data->cost-($data->cost*$app->get('client')->getDiscount())/100;
    }
});

Данный код выполняется в приложении до выполнения любых действий модулей, здесь мы просто подписываемся на события, которые еще не возникли. Само событие 'cost_calculate' встроено где-то внутри распространяемого модуля и указывает, что здесь может потребоваться указание извне:
class CartModel exnends yii\base\Component {
    public function getCost() {
        $cost = ...;
        $data= new DataProvider(['cost' => $cost]);

        yii::$app->get('cart')->trigger('cost_calculate', $data); //Запрашиваем совет от всех слушателей этого события
        
        $cost = $data->cost;
    }
}

yii::$app→get ('cart') указывает на инстанс компонента модуля, который является сервисом (singleton, узкое горлышко доступа, доступное глобально).

Если принцип «единственной обязанности» соблюден и наш getCost только возвращает стоимость, не делая самостоятельно никаких манипуляций с этой стоимостью, не принимая самостоятельных решений, программа всегда будет вести себя адекватно. Теперь вся изменчивость занимает отдельное место в системе. Можно в одном месте, без труда, выполнить любые извращенные просьбы заказчика, не поломав всю систему.

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

Комментарии (0)

© Habrahabr.ru