Рубріки: Фронтенд

Патерни проєктування для фронтенду та піца — що спільного? Розбираємо на зрозумілих прикладах

Вікторія Цукан

Можливо, ви не помічали, але в повсякденних задачах часто використовуєте різні патерни. Як інструмент розробника вони роблять нашу роботу простішою та ефективнішою, дозволяють писати більш якісний код. Як саме? Про це розповім далі.

У своєму блозі я хочу познайомити вас із поширеними патернами для фронтенду та ситуаціями, коли їх слід використовувати.

Патерни в IT — що це

З англійської мови pattern перекладається як шаблон. У нашому випадку це шаблон проєктування, який є простим розв’язанням типової проблеми. Патерни можуть використовуватись на рівні функцій, створення об’єкта чи на рівні архітектури. Певною мірою ці інструменти нагадують математичні формули для розв’язання задач.

Поняття патернів започаткував архітектор Крістофер Александер. Він помітив, що після ремонту та облаштування помешкань, його клієнти часто переробляли щось під себе. З’ясовуючи, що не влаштовує людей, він виявив декілька шаблонів — найбільш доцільне для багатьох розташування вікон і стін, висоту стель тощо. Всі напрацювання лягли в основну його книги — A Pattern Language.

Через деякий час чотири програмісти прочитали цю книгу і так надихнулися нею, що написали свою. Це були Еріх Гамма, Річард Хелм, Ральф Джонсон та Джон Вліссідес. В історію вони увійшли як Банда чотирьох (Gang of Four). Їхня книга — Design Patterns — описує шаблони проєктування для об’єктноорієнтованих систем. Автори виділили базові патерни, від яких походять інші. Саме ці шаблони розглянемо у статті.

Фахівці виділили три категорії патернів:

  • Породжувальні — відповідають за створення об’єкта
  • Структурні — призначені для опису ієрархії
  • Поведінкові — визначають взаємодію елементів.

Для того, щоб ви краще зрозуміли принцип використання патернів, пропоную розглянути їх на прикладі… роботи піцерії.

Породжувальні патерни

Такі патерни допомагають створювати різні об’єкти: без копіпасту, з гнучкістю та можливістю перевикористання. До цієї категорії відносяться багато шаблонів. Розглянемо кілька з них у деталях.

Prototype

Визначає шаблон, за яким створюються необхідні об’єкти. Прототип дозволяє створювати перевикористовуваний код. Тобто за одним шаблоном можна зробити безліч незалежних один від одного об’єктів. При цьому кількість коду зменшується.

Проводячи паралель із роботою піцерії, такий патерн допоміг би автоматизувати випічку піци. Зробити вручну сотню «Маргарит» складно. Однак можна написати формулу з інгредієнтами та рецептом піци, віддати її машині — і вона зробить за шаблоном 100 копій.

У коді цей патерн реалізується досить легко. У прикладі нижче наведено об’єкт прототипу з інформацією про піцу. За допомогою Object.create створюються необхідні копії:

Приклад коду

Factory

Надає інтерфейс для створення об’єктів. Його можна порівняти з фабрикою та її конвеєром. Якщо ви замовляєте на підприємстві партію продукції, у працівників це не викликає питань. Вони знають, які деталі потрібні, який процес збірки та який має бути результат. Ви ж отримуєте готову продукцію в потрібній кількості.

Цей патерн має кілька переваг. Насамперед зберігається незалежність між Фабрикою та об’єктами, які вона виробляє. Також дотримується single responsibility principle — принцип єдиної відповідальності. Відповідно до нього, вся логіка створення об’єкта перебуває всередині класу і не контролюється зовні. Також тут діє open-closed principle — принцип відкритості/закритості. Тобто коли всі об’єкти незалежні одне від одного. Але є і суттєвий недолік. Цей патерн стає завеликим і складним, коли прописано багато логіки. Його важко підтримувати, якщо потрібно створювати безліч об’єктів.

На прикладі піци такий патерн дуже показовий. Клієнт обирає в меню піцу, що найбільше сподобалася. Далі замовлення передається кухарям, вони готують страву. На кухні досконало володіють знаннями з приготування кожної піци, тому видають потрібний результат.

У коді цього патерну є два об’єкти у перших рядках: піци «Маргарита» та «Карбонара». Вони містять певні дані: склад піци, її розміри тощо. Далі розміщується сама Фабрика з інформацією pizzaClass — це конфігурація піци. Потім — метод createPizza, який дозволяє встановити саму піцу. Фабрика приймає назву піци та під капотом асоціює це з певним об’єктом чи сутністю, що містить всю необхідну інформацію. В результаті ви віддаєте назву у метод createPizza — і Фабрика повертає потрібний об’єкт:

Приклад коду

Builder

Дозволяє створювати об’єкти крок за кроком, таким чином контролюючи цей процес. Також зберігається принцип єдиної відповідальності. Хоча ми й керуємо створенням об’єкта, але логіка знаходиться всередині патерну. Недолік Будівельника подібний до того, що має Фабрика: якщо потрібен складний, універсальний об’єкт, то цей патерн буде важко підтримувати через велику кількість методів усередині.

У піцерії такий патерн стане в нагоді, коли клієнт хоче замовити піцу не з меню, а за власним рецептом. У коді для цього використовується клас PizzaBuilder з інформацією про піцу, що зберігається у змінній _pizza. Також є методи, які можуть оновлювати цю інформацію. У нашому випадку це можливість змінити інгредієнти та назву піци.

На ілюстрації показана реалізація цього патерну. Викликаємо метод changeName і по одному додаємо інгредієнти:

Приклад коду

Структурні патерни

Описують взаємозв’язок кількох сутностей об’єктів. Їхня мета — побудувати гнучку та ефективну системи з чіткою ієрархією. Зараз поговоримо про ключові структурні патерни.

Adapter

Розповсюджений патерн, який перетворює одні дані на інші. Наприклад, ви отримуєте з бекенду певні дані, формат яких вам не підходить. Потрібні інші поля, назви полів, кількість полів тощо. І саме Адаптер виконає необхідну трансформацію.

Цей патерн дотримується принципів єдиної відповідальності та відкритості/закритості. Логіка переходу між форматом знаходиться окремо від бізнеслогіки. Можемо додавати та прибирати Адаптери, які не будуть пов’язані. Щодо мінусів — застосування Адаптера не завжди виправдане. Інколи простіше додати зміни до бізнеслогіки, а не вбудовувати патерн.

У піцерії цей патерн може знадобитись у різних ситуаціях. Припустимо, для зміни назви піци. Запропоноване меню містить поля Назва, Інгредієнти та інші. Однак у кастомних моделей немає назви. У коді це не викликає питань, але при замовленні такої піци у чеку у відповідній графі буде порожньо. Проблему можна вирішити зміною логіки з додаванням поля Name.

Повернемось до нашої піцерії. У прикладі коду маємо дві піци: «Маргариту» і кастомну, відповідно до поля для назви та без нього. Нижче можете побачити, як створюються обидві піци. Маргарита у цьому випадку створюється звичайним способом через виклик new MargaritaPizza(). Для «безіменної» піци використовується адаптер, що встановлює їй ім’я. Таким чином margarita та adoptedPizza мають однаковий інтерфейс. Тепер після обробки замовлення у чеку все буде позначено, як треба:

Приклад коду

Decorator

Патерн дозволяє додавати функціонал до наявного класу. Завдяки йому не треба описувати багато логіки всередині. Достатньо винести її з класу в окреме місце, описати там і потім навісити Декоратор. Подібні патерни можна легко додавати та прибирати, якщо їх забагато. Щоправда, звідси витікає їхній мінус. Якщо Декоратор являє собою функцію, яка набуває значення і додає щось своє, то за наявності безлічі таких функцій-шарів прибрати один Декоратор складно. Шари мають залежність один від одного. До того ж Декоратори виглядають некрасиво, через що доводиться дробити всі функції. Загалом із цим патерном виконується принцип єдиної відповідальності, і логіка розбита на класи. В залежності від ситуації, це може бути і зручно, і одночасно викликати складнощі.

Наведу приклад у вигляді знижки на деякі піци. Ви можете додавати в коді до всіх піц поле Знижки або логіку з таким полем, але це буде довго. Простіше використовувати Декоратори. У нас є клас simplePizza. Це стандартна піца з методом getCost, який повертає вартість. Також є декоратор PizzaWithDiscount, який приймає піцу та розмір знижки, перезаписує у цієї піци getCost, а потім ставить нове значення з урахуванням знижки:

Приклад коду

Також тут можна побачити метод getCost return pizza.getCost мінус discount. Дисконт — це конкретна сума, а не відсоток. У кінці ми створюємо піцу через SimplePizza та перевіряємо її дефолтне значення. Наприклад, 100. Далі обертаємо піцу в декоратор PizzaWithDiscont і встановлюємо знижку 20. Наступного разу після перевірки вартості патерн порахує знижку, перезапише метод та поверне потрібний результат — 80.

Поведінкові патерни

Вони описують логіку поведінки між об’єктами та відповідають за розподіл відповідальності між сутностями та частинами. Дещо схожі на алгоритми. Хоча алгоритми теж є своєрідними патернами — тільки не проєктування, а обчислення. З багатьох поведінкових патернів я виокремлю два таких…

Chain of responsibility

Цей патерн є набором обробників. Замість обробки по черзі в одній функції, розбиваємо все на окремі функції, обробники та класи. В результаті запит проходить кілька етапів.

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

У цьому патерні руйнується принцип єдиної відповідальності. Логіка розділяється на дрібні частини: логіку для виконання операцій, для виклику і менеджменту запиту. При цьому процес може завершитись нічим. Наприклад, якщо в техпідтримку зателефонує людина з неадекватним запитом, оператор може перервати розмову. Тоді людина не отримає відповідь, оскільки запит від початку був некоректним.

Ланцюжок відповідальності може виглядати по-різному. У випадку з піцерією є один поширений сценарій. Клієнт замовляє піцу, а касир ставить уточнювальні питання — чи бажає клієнт салат, чи не хоче щось на десерт або випити. Простежується чіткий ланцюжок від одного питання до іншого.

Для реалізації такого патерну в коді створюємо два обробники з напоями та салатами. Також є клас AbstractHandler, який містить логіку з методом setNext. Він вказує поточному обробнику на наступний. Функція askQuestions виступає в ролі касира, який ставить питання і приймає обробник. Далі створюємо обробник для напоїв — New DrinksHandler, й аналогічний для салатів. Додаємо setNext(salads) для напоїв. Він вкаже, що після питання про напої касир має запитати про салати:

Приклад коду

У нижній частині зображена реалізація. Питання ставляться у правильній послідовності. Відповідь для напоїв і салатів буде «Так». Але якщо йдеться лише про салати, то для них обробник не вказаний. Тому для салатів буде просто «Так».

Strategy

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

Згадайте складання маршруту в Google-картах. Уявімо, ви хочете доїхати з дому на роботу на громадському транспорті. Відкриваєте застосунок, вказуєте стартову та фінішну точки та натискаєте на іконку з автобусом. Система будує маршрут згідно із заданою Стратегією. Раптом вирішуєте, що хочете йти пішки та натискаєте іконку з чоловічком. Система змінює Стратегію та перебудовує маршрут під нові критерії.

Головний плюс такого патерну — швидка переорієнтація системи.

Вам не треба застосовувати множину «if» з описом логіки в одному місці. Також цей спосіб допомагає використовувати композицію замість успадкування. Без цього зазвичай доводиться робити один загальний шаблонний алгоритм і успадковуватись від нього. Це призводить до переписування логіки. На противагу цьому з композицією всі алгоритми незалежні. Тому можна додавати принцип відкритості/закритості або підставляти щось нове, і це не вплине на старе.

Зверність увагу, цей патерн може бути громіздким. Раджу не використовувати його, якщо стратегія змінюється рідко. Також слід добре розуміти наявність кількох стратегій та різницю між ними. А для цього — ретельно продумати інтерфейс сервісу. У тих же Google-картах користувач бачить іконки автобуса, чоловічка або машини та відразу бачить, які типи маршрутів йому доступні.

Якщо ж повернутися до піцерії, то тут цей патерн допоможе побудувати логіку для отримання замовлення (доставлення або самовивіз). Ці Стратегії міститимуть два алгоритми та дві логіки. При виборі доставки маршрут виглядає так: кур’єр отримує замовлення, забирає піцу, привозить її клієнту, приймає оплату. У разі самовивозу клієнт робить замовлення, приходить до закладу в певний час, оплачує, отримує чек і забирає піцу. Якщо у користувача змінилися плани, він може перемикатися між стратегіями.

На рівні коду це виглядає так:

Приклад коду

Є дві стратегії з логікою, яка описує процес отримання піци. Також є більш високорівнева ReleaseOrderSystem. Вона містить логіку, метод setStrategy для зміни Стратегії та метод release для виконання Стратегії. У нижній частині показано, як це використовується. Спочатку створюється об’єкт OrderSystem. Далі ставимо стратегію доставки та викликаємо release — і піцу доставили. Як альтернатива, ставимо самовивіз, знову викликаємо release — і самовивіз відбувся.

Антипатерни

Ці рішення викликають безліч проблем із кодом. Щоб уникнути помилок в роботі, варто обов’язково знати про типові антипатерни:

  • Magic numbers

Це прописані в коді змінні або значення, походження яких сторонньому спостерігачеві не є очевидним. Наприклад, ваш проєкт пов’язаний із математичними обчисленнями, де щось множиться на два. Автор коду чітко розуміє, звідки взялася цифра. Однак інший розробник не знає цього. Так ускладнюється процес рев’ю, правок та роботи над продуктом. Для вирішення проблеми варто виносити все як мінімум у змінні. Також можна називати функції по-іншому, що зробить код зрозумілішим.

  • Hard code

Поширена проблема серед багатьох розробників. У цьому випадку вхідні дані зашиті в код і не можуть бути змінені без редагування коду. Якщо розробник та рев’юери забули перевірити цей фактор, згодом після заливки та деплою можуть виникнути серйозні проблеми. Тому варто перевірити, що це відносні значення, а не абсолютні.

  • Boat anchor

Цей патерн описує сутності, функції та класи, які залишаються у проєкті «про всяк випадок». Ви можете написати круту універсальну фічу та використовувати її. Однак з часом бізнес-логіка змінюється, і ваша функція стає непотрібною. Логічне рішення — прибрати її. Та розробник може залишити, бо раптом вона знадобиться і потім заощадить час написання коду. Це «потім» може так і не настати, а «якір» усе залишатиметься.

  • Lava flow

Схожий на попередній антипатерн, але в цьому випадку сутності залишаються в коді через брак часу на рефакторинг. Здавалося б, немає нічого страшного. Адже фрагменти, що не використовуються, нікому не заважають. Та згодом це перетворюється ніби на потік лави та викликає хаос. Іноді варто відірватися від нових фіч та очистити код від усього старого.

  • Reinventing wheel

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

  • Reinventing square wheel

Фактично це погіршений варіант попереднього антипатерну. Тільки тут ви не просто винаходите велосипед, але й робите його гірше, ніж аналоги. Тоді рішення має баги, милиці та врешті не вирішує поставлені завдання.

Використовувати патерни чи ні?

Єдиної правильної відповіді на це питання немає. Варто оцінювати переваги та недоліки різних патернів у конкретній ситуації. З одного боку, шаблони зручні та надійні. Крім того, завжди можна дізнатися у профільній спільноті про нюанси їх використання та адаптацію під свої задачі. Команді вони теж зрозумілі. Ви можете запропонувати в якомусь місці коду використати певний патерн — й інші розробники одразу побачать цілісну картину, як це працюватиме.

З іншого боку, патерни не завжди підходять для певних задач. Потрібно вміти аналізувати свої можливості, можливості цих шаблонів та зусилля, які витрачаються на їхнє впровадження. Варто прорахувати результат у перспективі. Недаремно кажуть: коли в тебе в руках молоток — увесь світ здається цвяхами. Після вивчення та освоєння патерну часто хочеться застосовувати його мало не в кожному проєкті. Однак це може спровокувати додаткові проблеми. Тому завжди замислюйтесь, які з ваших рішень дійсно спростять написання коду.

Якщо ви знайшли помилку, будь ласка, виділіть фрагмент тексту та натисніть Ctrl+Enter.

Останні статті

IT в Україні йде до свого фінального кінця. І потраплятимуть туди виключно за покликом душі

Коротко про українську IT-сферу у 2024 році Це коли на одну вакансію Middle розробника по…

26.03.2024

Блокчейн-розробка сьогодні: зарплати і перспективи на ринку праці

Формування криптовалютної галузі в Україні почалося ще у 2014 – саме тоді з'явилися перші стартапи,…

18.03.2024

Скільки рішень ухвалює розробник? Погляд новачка, який запускає продукт

Автор цього блогу — Python-девелопер Сергій Солдатов, який вирішив створити досить унікальний продукт. І це…

12.03.2024

Чи треба готуватись до співбесіди?

Думки шукачів діляться на: «так, однозначно» і «ні, не вартує, я все і так про…

04.03.2024

Відкладаєте до останнього? Що таке «синдром студента» і як з ним боротися

Синдром студента — це форма прокрастинації, яка полягає в тому, що людина, якій дали завдання,…

23.02.2024

Вчимося працювати з Git: основи конфігурації, гілки, додавання файлів та директорій

Git — це найпопулярніша CVS прямо зараз, яка дозволяє відстежувати історію розробки і спільно працювати.…

20.02.2024