Чому VIPER це поганий вибір для вашого наступного програми

Цей пост є вільним перекладом статті Why VIPER is a bad choice for your next application by Sergey Petrov
За останній рік про VIPER писали всі, кому не лінь. Ця архітектура реально надихає розробників. Але більшість статей, насправді, досить упереджені. Вони лише показують крутизну цього архітектурного патерну, замовчуючи про його негативні сторони. Адже проблем у нього не менше (а може навіть і більше) ніж у інших. І в цій статті я постараюся пояснити, чому VIPER зовсім не такий хороший, як про нього говорять, і чому він не підійде для більшості ваших додатків.
Деякі статті про порівняння архітектур, як правило, стверджують, що VIPER абсолютно не схожий на інші MVC-архітектури. Але насправді, VIPER — це просто нормальний MVC, де контролер розділений на дві частини: interactor і presenter. View залишився на місці, а модель перейменована в entity. Router заслуговує особливої уваги: так, інші архітектури не згадують цю частину у своїх абревіатурах, але він присутній і в них: у неявному вигляді (коли ви викликаєте
pushViewController
— ви створюєте простий маршрутизатор) або більш очевидному (як приклад — FlowCoordinators).
Поговоримо про "булочками", які пропонує нам VIPER (я буду посилатися на цю книгу). Подивимося на мета номер два, в якій йдеться про SRP (принцип єдиної відповідальності). Це прозвучить грубо, але яким диваком потрібно бути, щоб вважати це перевагою? Вам платять за вирішення завдань, а не за відповідність модним словами. Так, ви досі використовуєте TDD, BDD, юніт-тестування, Realm або SQLite, впровадження залежностей і багато-багато інших речей, але ви використовуєте все це не просто заради використання, а для вирішення проблем клієнта.
Тестування.
Це ще один цікавий аспект і дуже важливе завдання. По хорошому, про тестування можна було б написати окрему статтю, адже багато про це говорять, але мало хто дійсно тестує свої програми, і ще менше людей роблять це правильно.
Одна з головних причин в тому, що немає хороших прикладів. Ви можете знайти досить багато статей на тему того, як написати юніт-тест
assert 2 + 2 == 4
, але реальних прикладів не знайдете (тим не менш Artsy тримає в опен-сорсе свої програми, і вам слід поглянути на їх проекти).
VIPER пропонує відокремити всю логіку в безліч дрібних класів з розділеними обов'язками. Це мало б полегшити тестування, але так не завжди. Так, написати юніт-тест для простого класу легко, але більшість з таких тестів нічого не тестують. Давайте подивимося, наприклад, на більшість методов презентера: вони лише проксі між в'ю та іншими компонентами. Ви можете написати тести для цього проксі, це збільшить тестове покриття вашого коду, але ці тести марні. І у вас буде побічний ефект: ви повинні оновлювати ці даремні тести після кожного внесення змін в код.
Правильний підхід до тестування повинен включати в себе тестування интерактора і презентер відразу, адже ці дві частини сильно пов'язані один з одним. Крім того, оскільки ми поділяємо логіку на два класи, нам потрібно набагато більше тестів порівняно з одним класом. Це проста комбінаторика: у класу
A
є 4 можливих стану, а в класу
B
— 6, відповідно їх комбінація має 24 можливих стану, і вам потрібно протестувати.
Правильний підхід для спрощення тестування представляє з себе чистоту коду замість того, щоб просто розділити складний код на купу класів.
Як не дивно, тестувати в'ю простіше ніж тестувати деякі ділянки бізнес-логіки. В'ю являє собою лише сукупність певних властивостей і успадковує вигляд з цих властивостей. Ви можете використовувати FBSnapshotTestCase для порівняння їх стану за зовнішнім виглядом. Ця штука все ще не обробляє деякі особливі випадки, як кастомні переходи, але наскільки часто ви їх використовуєте?
Оверинжиниринг в проектуванні.
VIPER — те, що відбувається, коли колишні джависты вриваються у світ iOS. — n0damage, коментар на reddit
От чесно, може хто-небудь подивитися на это і сказати: "так, ці додаткові класи та протоколи дійсно покращують моє розуміння того, що відбувається в моєму додатку".
Уявіть собі просту задачу: є кнопка, яка запускає оновлення з сервера і є в'ю, з отриманими від сервера даними. Вгадайте-но скільки класів/протоколів будуть зачеплені такою зміною? Так, як мінімум 3 класу і 4 протоколу будуть змінені для реалізації такої простої функції. Хто-небудь пам'ятає як Spring почали з деяких абстракцій і закінчили з
AbstractSingletonProxyFactoryBean
? Я завжди мріяв про "зручному суперклассе проксі-фабрики для проксі-фабрик, які створюють тільки синглтоны" в моєму коді.

Надлишкові компоненти


(Надлишковий код не так нешкідливий, як я звик думати. Він дає помилковий сигнал про свою необхідність.)
Як я вже згадував раніше, презентер, як правило, досить дурний клас, який просто передає виклики з в'ю интерактор (щось на зразок этого). Так, іноді він містить складну логіку, але в основному, це просто надмірна компонент.

«DI-френдлі» кількість протоколів


(Некрасивий код легко розпізнати і його вартість легко оцінити. Це не так якщо абстракція невірна.)
Існує загальна плутанина з цим скороченням: VIPER реалізує принципи SOLID, де DI — це "інверсія залежностей", а не "впровадження" ("dependency inversion", а не "injection"). Впровадження залежностей — це особливий випадок патерну "інверсія управління" ("Inversion of Control"), який, звичайно пов'язаний, але відрізняється від інверсії залежностей.
Інверсія залежностей — це про відділення модулів різних рівнів шляхом введення абстракцій між ними. Для прикладу, модуль UI не має напряму залежати від мережевого модуля. Інверсія управління — це інше. Це коли модуль (зазвичай з бібліотеки, яку ми не можемо міняти) делегує що-небудь іншого модуля, який зазвичай надано першому модулю як залежність. Так, коли ви реалізуєте data source для вашої
UITableView
ви використовуєте принцип IoC. Використання схожих назв для різних високорівневих речей — джерело плутанини.
Повернемося до VIPER. Є багато протоколів (щонайменше 5) між класами всередині одного модуля. І у всіх них немає необхідності. Презентер і интерактор не є модулями з різних шарів. Застосування принципу IoC може мати сенс, але запитайте себе: як часто у вас буває хоча б два презентера для одного в'ю? Я впевнений, що більшість з вас відповість "ніколи". Так чому ж необхідно створювати цю купу протоколів, які ми ніколи не будемо використовувати?
Крім того, з-за цих протоколів, ви не можете легко переміщатися по коду в IDE. Адже
cmd+click
вас буде викидати в протокол, замість реалізації.

Проблеми з продуктивністю

Це ключовий момент, але багато хто просто не переживають про це, або просто недооцінюють вплив поганих архітектурних рішень.
Я не буду говорити про фреймворку Typhoon (який дуже популярний для впровадження залежностей у світі objective-c). Звичайно, він має деякий вплив на продуктивність, особливо коли використовується автоматичне впровадження, але VIPER не вимагає його використання. Замість цього, я б поговорив про рантайнме і запуску програми, і як VIPER уповільнює ваш додаток буквально всюди.
Час запуску програми. Ця тема рідко обговорюється, але вона важлива. Адже якщо ваш додаток стартує дуже повільно, то користувачі не будуть ним користуватися. На останній WWDC як раз говорили про оптимізації часу запуску додатків. Час старту програми безпосередньо залежить від кількості класів у ньому. Якщо у вас є 100 класів — це нормально, затримка буде непомітною. Однак, якщо у вашому додатку тільки 100 класів — вам дійсно потрібна ця складна архітектура? Але якщо ваш додаток величезна, наприклад, ви працюєте над додатком Facebook (18К класів), то різниця буде відчутна: близько однієї секунди. Так, "холодний старт" програми займе 1 секунду тільки на те, щоб довантажити всі метадані класів і більше нічого, ви правильно зрозуміли.
Рантайм виклики. Тут все складніше і в основному застосовується тільки для Swift-компілятора (оскільки рантайм Objective-C володіє великими можливостями і компілятор не може безпечно виконувати оптимізації). Давайте поговоримо про те, що відбувається "під капотом" коли ви викликаєте який-небудь метод (я кажу "викликаєте", а не "відправляєте повідомлення" тому, що друге не завжди коректно для Swift). Є три види викликів в Swift (від швидкого до повільного): статичний, таблиця дзвінків та відправлення повідомлень. Останній — єдиний, який використовується в Objective-C, і він використовується в Swift коли необхідна сумісність з кодом Objective-C, або коли метод оголошений як
dynamic
. Звичайно, ця частина рантайма буде вельми оптимізована і записана в асемблері для всіх платформ. Але що якщо ми зможемо уникнути цих накладних витрат, давши компілятору поняття про те, що саме буде викликатися, під час компіляції? Це саме те, що Swift-компілятор робить зі статикою і таблицею викликів. Статичні виклики швидкі, але компілятор не може їх використовувати без 100% впевненості в типах. І коли тип нашої змінної є протоколом, компілятор змушений використовувати виклики за допомогою таблиць. Це не занадто повільно, але одна мілісекунда тут, одна — там, і тепер загальний час виконання виростає більш ніж на одну секунду, порівняно з тим, чого можна було б досягти з чистим Swift-кодом. Цей пункт пов'язаний з попереднім про протоколи, але я думаю, що краще відокремити занепокоєння з приводу кількості невикористовуваних протоколів від метушні з компілятором.

Слабке поділ абстракцій

Повинен бути один і, бажано, тільки один очевидний спосіб зробити це.
Один з найпопулярніших питань VIPER-спільноти: "куди мені слід віднести X?" Виходить, що з одного боку є багато правил, як треба робити все правильно, а з іншого — рішення засновані на чиєму-небудь думці. Це можуть бути складні випадки, наприклад обробка
CoreData
з допомогою
NSFetchedResultsController
або
UIWebView
. Але навіть загальні випадки, такі як використання
UIAlertController
— є темою для обговорення. Давайте поглянемо: здесь з нашим алертом взаємодіє роутер, а здесь його показує в'ю. Ви можете відповісти, що цей простий алерт є приватним випадком алерта без будь-яких дій, крім закриття.
Особливі випадки недостатньо особи, щоб порушувати правила.
Все вірно, але навіщо у нас здесь фабрика для створення таких алертів? У підсумку маємо бардак навіть з
UIAlertController
. Ви цього хочете?

Генерація коду

Читаність має значення.
Як це може бути проблемою архітектурної? Це просто генерація купи класів за шаблоном замість написання їх вручну. В чому тут проблема? Проблема в тім, що більшість часу ви читаєте код, а не пишете його. Таким чином ви читаєте шаблонний код упереміш зі своїм кодом більшу частину свого часу. Чи добре це? Не думаю.
Висновок.
Я не переслідую мету відрадити вас від використання VIPER взагалі. Цілком можуть бути додатки, які від нього тільки виграють. Однак, перш ніж почати розробку свого додатку вам слід задати собі кілька питань:
  1. Буде у цього додатка тривалий життєвий цикл?
  2. чи Достатньо стабільні вимоги? В іншому випадку, ви зіткнетеся з нескінченним рефакторінгом навіть при внесенні дрібних змін.
  3. Ви дійсно тестуєте свої програми? Будьте чесні з собою.
Тільки якщо ви відповіли "так" на всі три питання, VIPER міг би бути хорошим вибором для вашого застосування.
І, нарешті, останнє: ви повинні самостійно приймати рішення. Не просто сліпо довіряти якомусь хлопцеві з Медіума (або Хабра), який говорить "Використовуйте X, X — це круто." Цей хлопець теж може помилятися.
Джерело: Хабрахабр

0 коментарів

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