Сухої антипаттерн

Довгий час я думав, що ж не в порядку з деякими частинами коду. Раз за разом, в кожному з проектів знаходиться якийсь «особливо уразливий компонент, який весь час «валиться». Замовник має властивість періодично міняти вимоги, і канони agile заповідають нам все хотілки втілювати, запускаючи change request-и в наш scrum-механізм. І як тільки зміни стосуються його компонента, через пару днів QA знаходять у ньому кілька нових дефектів, переоткрывают старі, а то і повідомляють про повну його непрацездатності в одній з точок застосування. Так чому ж один із компонентів весь час на вустах, чому так часто вимовляється фраза а-ля «знову #компонент# зламався»? Чому цей компонент наводиться як антиприклад, в контексті «лише б не вийшов ще один такий же»? З-за чого цей компонент так нестійкий до змін?

Коли знаходять причину, що призвела до, або сприяла розвитку такого пороку в додатку, цю причину позначають, як антипаттерн.
Цього разу каменем спотикання став патерн Strategy. Зловживання цим паттерном призвело до створення самих делікатних частин наших проектів. Патерн сам по собі має цілком «мирний» застосування, проблема скоріше в тому, що його пхають туди де він підходить, а не туди, де він потрібен. «Якщо ви розумієте про що я» ©.

Класифікація
Патерн існує у декількох «іпостасях». Суть його від цього змінюється не сильно, небезпека його застосування існує в будь-якому з них.
Перший, класичний вигляд — це якийсь довговічний об'єкт, який приймає інший об'єкт по інтерфейсу, власне стратегію, через сетер, при деякій зміні стану.
Другий вид, вироджений варіант першого — стратегія приймається один раз на весь час життя об'єкта. Тобто для одного сценарію використовується одна стратегія, для іншого інша.
Третій вид, це виконуваний метод, або статичний, або в короткоживущем об'єкті, що приймає в якості вхідного параметра стратегію по інтерфейсу. У «банди чотирьох» цей вид названий як «Template method».
Четвертий вид — інтерфейсний, ака UI-ний. Іноді називається як патерн «шаблон», іноді як «контейнер». На прикладі веб-розробки — представляє з себе якийсь, шматок розмітки, що містить в собі плейсхолдер (а то і не один), куди під час виконання отрендерится змінна, що має декілька різних реалізацій, частина розмітки. Паралельно розмітці, у JavaScript коді також живуть паралельні в'ю-моделі або контролери, в залежності від архітектури, прийнятої в додатку, організовані за другого виду.

Загальні риси цих всіх випадків:
1)деяка незмінна частина ака має одну реалізацію, далі, я буду називати її контейнером
2)змінна частина, вона ж стратегія
3)місце використання, що створює/викликає контейнер, який визначає, яку стратегію контейнер повинен використовувати, далі буду називати це сценарієм.

Розвиток хвороби
Спочатку, коли компонент, з використанням цього патерну тільки реалізовувався в проекті, він не здавався таким вже поганим. Застосований він був, коли було потрібно створити дві однакові сторінки(знову ж, на прикладі веб-розробки), які лише трохи відрізняються контентом в середині. Навіть навпаки, розробник порадів, наскільки красиво і витончено вийшло реалізувати принцип DRY, тобто повністю уникнути дублювання коду. Саме такі епітети я чув про компоненті, коли він був тільки створений. Той самий, який став попаболью всього проекту декількома місяцями пізніше.
І раз вже я почав теоретизувати, зайду трохи далі — саме спроби реалізувати принцип DRY, через патерн strategy, власне як і через успадкування, призводять до пітьмі. Коли на догоду DRY, сам того не помічаючи, розробник жертвує принципом SRP, першим і головним постулатом з SOLID. До речі, DRY не є частиною SOLID, і при конфлікті, треба жертвувати саме їм, бо він не сприяє створенню стійкого коду на відміну від, власне, SOLID. Як виявилося — швидше навіть навпаки. Переиспользование коду має бути приємним бонусом тих чи інших дизайн-рішень, а не метою прийняття оних.
А спокуса перевикористання виникає, коли замовник приходить з новою історією на створення третьої сторінки. Адже вона так схожа на перші дві. Ще цьому чимало сприяє бажання замовника реалізувати всі «подешевше», Адже переиспользовать раніше створений контейнер швидше, ніж реалізувати сторінку повністю. Історія потрапила до іншого розробнику, який швидко з'ясував, що функціоналу контейнера недостатньо, а повноцінний рефакторинг не вписується в оцінки. Ще одна з помилок тут в тому, що розробник продовжує слідувати плану, поставленим оцінками, і це відбувається «в тиші», адже відповідальності як би немає, вона лежить на всій команді, яка прийняла таке рішення і таку оцінку.
І ось в контейнер додається новий функціонал, інтерфейс стратегії додаються нові методи і поля. У контейнері з'являються if-и, а в старих реалізації стратегії з'являються «заглушки», щоб не зламати вже наявні сторінки. На момент здачі другої історії, компонент вже був приречений, і чим далі, тим гірше. Все складніше розробникам розуміти як він влаштований, в тому числі тим, які «зовсім недавно в ньому щось робили». Все складніше вносити зміни. Все частіше доводиться консультуватися з «попередніми потрогавшими», щоб запитати як це працює, чому були внесені деякі зміни. Все більше ймовірність того, що навіть найменша зміна внесе новий дефект. Власне вже мова починає йти про те, що все більше ймовірність внесення двох і більше дефектів, бо один дефект з'являється вже з околоединичной ймовірністю. І ось настає момент, коли нове вимоги замовника реалізувати неможливо. Виходу два: або повністю переписати, або зробити явний хак. А в ангуляре як раз є відповідний хак-інструмент — можна зробити emit події знизу вгору, потім broadcast зверху вниз, коли закінчив брудні справи нагорі. Технічний борг при цьому вже не збільшується, він і так вже давно дорівнює вартості реалізації цього компонента з нуля.

Суха альтернатива
Спадкування нерідко засуджується, а корпорація добра у своїй мові Go взагалі вирішила обійтися без нього, і як мені здається, негатив до спадкоємства частково виходить з досвіду реалізації принципу DRY через нього. «Стратежный» DRY так само призводить до сумних результатів. Залишається пряма агрегація. Для ілюстрації я візьму простий приклад і покажу як він може бути представлений у вигляді стратегії, тобто шаблонного методу і без нього.
Припустимо у нас є два дуже схожих сценарію, представлених наступним псевдокодом:
В них повторюються 10 рядків X на початку і 15 рядків Y в кінці. В середині ж один сценарій має рядка А інший — рядки B
СценарийА{
рядок X1
...рядок X10
Рядок А1
...Рядок А5
рядок Y1
...Рядок Y15
}

СценарийВ{
рядок X1
...рядок X10
Рядок B1
...Рядок B3
рядок Y1
...Рядок Y15
}


Варіант позбавлення від дублювання через стратегію

Контейнер{
рядок X1
...рядок X10
ВызовСтратегии()
рядок Y1
...Рядок Y15
}

СтратегияА{
Рядок А1
...Рядок А5
}

СтратегияВ{
Рядок B1
...Рядок B3
}

СценарийА{
Контейнер(new СтратегияА)
}

СценарийВ{
Контейнер(new СтратегияВ)
}


Варіант через пряму агрегацію

МетодХ{
рядок X1
...рядок X10
}

Методом{
рядок Y1
...Рядок Y15
}

Методу{
Рядок А1
...Рядок А5
}

МетодВ{
Рядок B1
...Рядок B3
}

СценарийА{
МетодХ()
Методу()
Методом()
}

СценарийВ{
МетодХ()
МетодВ()
Методом()
}

Тут передбачається, що всі методи в різних класах.
Як я вже говорив, на момент реалізації, перший варіант виглядає не так вже і погано. Недолік його у тому, що він спочатку поганий, а в тому, що він нестійкий до змін. І, все ж, гірше читається, хоча на простому прикладі це може бути і не очевидно. Коли потрібно реалізувати третій сценарій, який схожий на перші два, але не на 100%, виникає бажання переиспользовать код, що міститься в контейнері. Але його не вийде переиспользовать частково, можна лише взяти його цілком, тому доводиться вносити зміни в контейнер, що відразу несе ризик зламати інші сценарії. Теж саме відбувається, коли нова вимога передбачає зміни у сценарії А, але це не повинно зачіпати сценарій B. У випадку ж з агрегацією, метод X можна з легкістю замінити на метод X' в одному сценарії, зовсім не зачіпаючи інші. При цьому неважко припустити, що методи X і X' можуть також майже повністю збігатися, і їх також можна подразбить. При «стратежном» підході, якщо каскадировать таким же «стратежным» чином, те зло, що поміщається в проект зводиться в другу ступінь.

Коли можна
Багато приклади використання патерну strategy лежать на увазі, і часто використовуються. Їх всіх об'єднує одне просте правило — в контейнері немає бізнес логіки. Зовсім. Там може бути алгоритмічне наповнення, наприклад хеш-таблиця, пошук чи сортування. Стратегія містить бізнес-логіку. Правило, за яким один елемент дорівнює іншому або більше-менше є бізнес-логіка. Всі оператори linq також є втіленням патерну, наприклад оператор .Where() також є шаблонним методом, і приймається їм лямбда є стратегія.
Крім алгоритмічного наповнення, це може бути наповнення пов'язане із зовнішнім світом, асинхронні запити наприклад, або, у прикладі від «банди чотирьох» — підписка на подію натискання миші. Те що називають коллбэками по суті є та ж стратегія, сподіваюся, мені пробачать всі мої гіпер-узагальнення. Також, якщо мова йде про UI, то це можуть бути таби, або спливаюче вікно.
Словом, це може бути що завгодно, повністю абстрагированное від бізнес-логіки.
Якщо ж ви в розробці використовуєте патерн strategy, і в контейнер бізнес-логіка потрапила — знайте, лінію ви вже переступили, і стоїте по щиколотку… ммм, болоті.

Запахи
Іноді важко зрозуміти, де межа між бізнес-логікою та загальними завданнями програмування. І спочатку, коли компонент тільки створений, визначити, що він у майбутньому принесе геморою, непросто. Та й якщо бізнес-вимоги ніколи не будуть мінятися, то цей компонент може ніколи і не спливти. Але якщо зміни будуть, то неминуче будуть з'являтися наступний code-smells:
1. Кількість переданих методів. Параметр обговорюваний, сам по собі не шкідливий, але таки може натякнути. Два-три ще нормально, але якщо в стратегії міститься з десяток методів, то напевно щось уже не так.
2. Прапори. Якщо в стратегії крім методів є поля/властивості, варто звернути на те як вони називаються. Такі поля як Name, Header, ContentText — допустимі. Але якщо бачите такі поля як SkipSomeCheck, IsSomethingAllowed, це означає, що стратегія вже пахне.
3. Умовні виклики. Пов'язано з прапорами. Якщо в контейнері є схожий код, значить ви вже пішли в болото по пояс
if(!strategy.SkipSomeCheck)
{
strategy.CheckSomething().
}

4. Неадекватний код. На прикладі з JavaScript —
if(strategy.doSomething)

З назви видно, що doSomething це метод, а перевіряється як прапор. Тобто розробники полінувались створити прапор, що позначає тип, а використовували в якості прапора наявність/відсутність методу, причому всередині блоку if навіть не він викликався. Якщо ви зустрічаєте таке, знайте — компонент вже по горло в технічному борг.

Висновок
Ще раз хочу позначити свою думку, що першопричина всього того, що я описав, не в паттерні як такому, а в тому, що він використовувався заради принципу DRY, і цей принцип був поставлений вище принципу єдиної відповідальності, ака SRP. І, до речі, не раз стикався з тим, що принцип єдиною ответсвтенности як то не зовсім адекватно трактується. Приблизно як «мій божественний клас управляє супутником, управляти супутником — це єдина його відповідальність». Цього ноті хочу закінчити свій опус і побажати якомога рідше у відповідь на «чому так», чути фразу «історично так склалося».

Джерело: Хабрахабр

0 коментарів

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