Аспект. Знайти і икапсулировать мінливість на стику областей

В цьому пості хочу опублікувати свої думки про поняття мінливості в складних програмних продуктах, які поширюються копіями і обслуговують різні, несхожі один на одного підприємства (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('кошик', 'vasya/shoppingcart/Service'); //реєстрація у якості сервісу компонента, що підтримує події

//Далі, підписуємося на подію
yii::$app->get('кошик')->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('кошик')->trigger('cost_calculate', $data); //даємо запит рада від всіх слухачів цієї події

$cost = $data->cost;
}
}

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

Якщо принцип «єдиної обов'язки» дотриманий і наш getCost тільки повертає вартість, не робить самостійно ніяких маніпуляцій з цією вартістю, не приймаючи самостійних рішень, програма завжди буде вести себе адекватно. Тепер вся мінливість займає окреме місце в системі. Можна в одному місці, без праці, виконати будь-збочені прохання замовника, не поламавши всю систему.

Раджу бачити і відокремлювати мінливість, щоб замовники і роботодавці не бачили у вас інваліда, який не в змозі зробити найпростішу зміну, не поламавши або не переписавши всю систему.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.