iOS, Чистий код

Для кожного програміста поняття «чистого коду» різне. У цій статті я спробую донести свої думки з цього приводу, і, можливо, навіть змінити старі підходи.

Чистий код формується сукупністю правил, якими керується розробник, щоб його код був зрозумілим, гнучким, читабельним (для інших розробників, а також для самого автора коду). Виходячи зі свого досвіду: якщо розробника підганяли, зменшували йому терміни здачі задач, або ж з якихось інших причин він не структуризував свій код, то він або його колега пізніше витратить в півтора-два рази більше часу на виправлення помилок або додавання нового функціоналу, ніж якби фічу зробили з нуля. Кожна внесена зміна в логіку роботи може ламати логіку в 2-3 інших місцях. Тому можна сказати, що чистота коду дорівнює професіоналізму розробника.

Отже, якими критеріями визначається чистота коду?

Іменування

Іноді це дуже складне і «філософське» питання. Основні правила:

  • ім'я змінної має бути максимально коротким і максимально чітко відповідати на запитання «що зберігається в цій змінній» і «яким чином змінна буде використовуватися». При цьому зрозумілість має вищий пріоритет, ніж стислість. Те ж саме стосується і функцій;
  • використовуємо camelCase для назв змінних і функцій, які складаються з кількох слів;
  • тільки англійська мова. Часто буває, що першу версію проєкту писали розробники з однієї країни, а другу версію — з іншої. І франкомовний розробник вже не зрозуміє, що хотів сказати русомовний, назвавши змінні var moiTovary, var cena, var ssylka;
  • короткі імена типу «var a, let b» має сенс використовувати тільки для змінних місцевого значення, в невеликому фрагменті коду.

Функції

Кожен розробник писав і зустрічав у чужому коді як невеликі функції в кілька рядків, так і функції на 300 рядків і більше. І, думаю, кожен погодиться, що розбиратися з такими гігантами дуже непросто, а вносячи правки і додаючи новий функціонал, ми згадуємо багато поганих слів :) Усі автори книг на тему «чистого коду» сходяться на тому, що функції повинні мати розмір максимум 20-30 рядків (деякі вказують максимум у 15-20 рядків). В наш час, коли ми маємо такі потужні інструменти для роботи з UI, як storyboard і xib, і дуже рідко відчуваємо необхідність програмно створювати і задавати всі параметри для нащадків UIView, від простих, як UILabel, до більш складних, як UITableViewCell — це цілком реальна цифра. Звичайно, існують винятки, але необхідність створювати велику функцію виникає рідко.

Основні критерії, якими я керуюсь при написанні функцій:

  • функція повинна виконувати одне завдання, суть цього завдання відображена в назві функції. Дуже плутає та ускладнює читання коду ситуація, коли, наприклад, функція називається syncParkings(), а в тілі функції виконується: а) отримання даних; б) синхронізація даних; в) в completionBlock { .. } викликається функція syncVehicles(); г) в completionBlock якої виконуються маніпуляції з UI. Цей код самого автора заплутає і поставить у глухий кут через тиждень, коли він забуде, що і навіщо робив;
  • функція повинна приймати мінімум параметрів, в ідеалі 1-2, в рідкісних незручних випадках — 4. Якщо виникає потреба передати в функцію більше 4 параметрів, потрібно створити сутність (об'єкт, структуру, Dictionary, Touple), яка буде містити необхідні параметри і передавати їх — кількість параметрів зменшиться до одного;
  • один рівень абстракції на функцію. Блоки if-else в ідеалі повинні мати лише один рядок — виклик іншої функції. Якщо блоків if {} else if ... else {} більше трьох, то по можливості слід використовувати оператор switch. Всім не раз доводилося мати справу з багатоповерховими конструкціями типу:
if ... {
	var a = ...
	if ... {if ... {// коментар "чому", "що" тощо
			... 
			// код ....}}else{
		...
	}}elseif ... {
....
}
 
...
else{}
 

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

  • створення нової функції замість дублювання коду. Якщо в 2-х місцях є однаковий код на 2-3 або більше рядків, то варто винести цю частину коду в окрему функцію.

Коментарі

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

Форматування

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

Приклад структури проєкту:

- ProjectName (folder)
   - Classes 
	 AppDelegate.swift
	 Constants.swift
     - Models
		MyEntity.swift
		...
	 - Views
		EntityView.swift
		...
     - Controllers 
		- BaseControllers
		HomeViewController.swift
		SettingViewController.swift
		...
	 - Helpers
     - Managers
     - Extensions
     - Protocols
	 - Externals
   	 ... 
 
   - Resources
     - Storyboards
	 - Fonts
     - Supporting Files
	 - AudioFiles
	 - Plists
     .....

Дотримання єдиної структури проєкту прискорює пошук потрібних файлів. Не менш важливим є й форматування класів. Основні його принципи:

Один файл - один клас (структура).

import UIKit
import Foundation //  2.1 додавання бібліотек на початку файлу//  2.2 зробити два порожніх рядка для групування даних
class EntryCell: UITableViewCell {
 
	// MARK: Outlets  		// 4.1. використання Pragma Marks
    @IBOutlet weak var dateLabel: UILabel!           
    @IBOutlet weak var priceLabel: UILabel!
    @IBOutlet weak var dotView: UIView!
 
                             // 4.2. між кожною групою, окремо...// MARK: Properties
    var entry: CDEntry?
 
 
    // MARK: Overriden funcs
    override func awakeFromNib(){
        super.awakeFromNib()
 
        dotView.layer.cornerRadius = dotView.frame.size.width /2}// 5.
    func setSelected(_ selected: Bool, animated: Bool){// Виконати щось}}

Докладніше по пунктах:

  1. Додавання бібліотек на початку файлу групами, робити два вертикальних відступи перед оголошенням класу або структури.
  2. Використання Pragma Mark надає чудову можливість групувати частини коду всередині класу за їх типами та призначенням.
  3. Групування коду за логічними принципами з допомогою вертикальних відступів робить код більш зручним для читання. Як, наприклад, правило завжди робити відступ в 1 рядок між функціями, і відступ в 2 рядки між групами Pragma Mark. Цей очевидний пункт я додав, оскільки маю досвід роботи з розробниками, які не роблять вертикальних відступів і пишуть код одним полотном. Коли читаєш такий код, очі швидко втомлюються, їм важко зафіксуватися на інформації. Уявіть собі газети та журнали без абзаців і заголовків; такий текст читати вкрай незручно, а коли йдеться про код — то й поготів.

Більш повний список Pragma Marks:

// MARK: Outlets// MARK: Properties// MARK: Overriden funcs// MARK: Action funcs// MARK: Notification observers// MARK: Public funcs:// MARK: Private funcs// MARK: Delegate funcs// MARK: Class(Static) funcs
...

Код виглядає ще більш структурованим, якщо функції та змінні форматуються за допомогою PragmaMarks у заданій послідовності, а не розкидані випадковим чином. Особисто я додав такий список в Xcode як снипети, для зручності.

Доброю практикою є наявність у компанії документа з назвою на кшталт «iOS Code Styles Guidline», з яким новий співробітник ознайомлюється перед початком роботи і далі форматуючи код у очікуваному стилі.

Рефакторинг

Рефакторинг — це контрольований процес покращення вашого коду без написання нового функціоналу. Завдання рефакторингу — зробити код чистим.

Написання коду можна поділити на 2 пункти: зробити так, щоб код працював, і провести рефакторинг коду. Що може забруднити наш код? Такі речі, як:

  • коментарі, про які вже йшлося вище. Повторюся: якщо написаний код потребує пояснень, то слід одразу переписати цей код;
  • дублювання коду. В більшості випадків виникає, коли над одним проєктом працює два або більше розробників, які в процесі написання мають схожі завдання, але не знають, що їх колега вже написав такий код. Буває, що різні розробники вирішують одне й те ж завдання різними шляхами, таке дублювання виявити важче. Інша причина дублювання — поспіх (строки здачі, підганяє PM або замовник). Якщо дублювання відбувається в одному класі — слід винести код, який дублюється, в окремий метод. Код дублюється в різних класах — винести код в метод суперкласу; якщо ж створення суперкласу неможливе, то слід винести метод в окремий клас;
  • мертвий код. Часто трапляється, що додавання нового функціоналу доцільніше починати з нуля, ніж редагувати вже написану частину коду. В таких випадках, через поспіх або нестачу досвіду, розробник може додати один або кілька класів, не видаливши старі або більше не потрібні класи, залишивши саме видалення «на потім» і, як це завжди буває, забувши про них. В масштабах одного класу ця ситуація може повторюватися з змінними і функціями, які більше не використовуються. Слід привчити себе завжди видаляти непотрібний код, який в подальшому буде суттєво заважати розумінню і роботі з кодом. Сьогодні заощадиш 2 хвилини, а через кілька місяців витратиш додатково годину, щоб розібратися в здавалося б очевидних речах.

Архітектурні патерни

Використання архітектурних патернів дозволяє збалансовано розділити обов'язки між сутностями. Поняття «патерн» можна пояснити як стиль, шаблон, покликаний вирішити конкретну задачу. Архітектурний патерн задає структуру застосунку в цілому, визначає розподіл обов'язків між класами, кожен з яких виконує лише одну з ролей. Архітектурний патерн (також можна назвати — дизайн-патерн) визначає не тільки роль класів і об'єктів в застосунках, а й те, як об'єкти спілкуються між собою.

Головним дизайн-патерном в Apple (MacOS & iOS) є MVC (Model-View-Controller). З часом з'явилося ще кілька, в результаті сьогодні список основних архітектурних патернів має такий вигляд:

  • MVC(Model-View-Controller);
  • MVP(Model-Passive View-Presenter);
  • MVVM(Model-View-ViewModel);
  • Viper (CleanSwift).

MVC

Це найчастіше використовуваний патерн. Він класифікує об'єкти згідно їх ролі в проєкті, що допомагає чистому розділенню коду. Правильна реалізація патерна MVC означає, що об'єкт потрапляє тільки в одну з 3-х груп.

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

View — об'єкти, що відповідають за візуальне представлення, за те, що користувач бачить. В iOS — це всі класи, які мають префікс UI (і їх нащадки).

Controller — посередник між View і Model. Координує всю роботу: реагує на дії користувача, виконані на view, і оновлює view, використовуючи model.

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

Використовуючи MVC, ми отримуємо такі переваги:

  • краще розуміння класів;
  • повторне використання класів, у тому числі і в інших проектах (в основному це стосується Model і View);
  • тестування класів окремо один від одного;
  • в порівнянні з іншими паттернами, цей паттерн є найпростішим і зрозумілим у використанні, а також менш затратним за часом.

Недоліком MVC є те, що з часом, коли проект поступово збільшується і змінюється, Controller розростається і в деяких проектах може досягати позначки в 1000 рядків коду і більше, і чим далі, тим більше це ускладнює підтримку проекту: виправлення помилок або додавання нового функціоналу. З цієї причини деякі розробники «розшифровують» абревіатуру MVC як «Massive ViewController :)» Поки ми робимо щось одне, ми боїмося зламати щось інше. В результаті, працюючи з таким контролером, розробник не може правильно оцінити поставлене завдання, не вкладається в зазначений час, втрачає довіру і репутацію у замовника. Тут можна було б звинуватити клієнта, тому що:

  • клієнт не хоче розуміти, наскільки важко реалізувати таку велику кількість функціоналу на одному екрані;
  • клієнт не розуміє, що не можна постійно змінювати вимоги в ТЗ;
  • клієнту багаторазово намагалися пояснити, що спочатку треба зосередитися на UX, а не на UI-дизайні;
  • якби клієнт на старті проекту вказав на цей функціонал, то це зайняло б значно менше часу…
  • ваш варіант :)

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

Як можна «розвантажити» ViewController?

  1. Винести dataSource-delegate функції в окремі класи. В Swift ми маємо можливість винести UITableViewDataSource & UITableViewDelegate функції в Extensions;
  2. Винести логіку в модель. В Obj-C можна частину логіки винести в категорії, в Swift чудовим інструментом є Extensions і Protocols.
  3. Винести код налаштування об'єктів групи View в відповідні класи;
  4. Винести Web Service логіку в модель, наприклад, в клас WebService;
  5. Винести в окремий клас логіку роботи з базою даних. Як приклад CoreDataStack (або CoreDataManager);
  6. Не використовувати повторно об'єкти групи Controller (наступники класу UIViewController). Побачивши опис нової функції, ви можете подумати: «У мене вже є дуже схожий viewController, я його використаю, щоб не дублювати код. А для розділення логіки напишу протокол. Це заощадить кілька годин». Якщо ви впевнені, що це фінальна версія проекту — так, це заощадить час. Але якщо, як часто буває, буде змінюватися логіка роботи окремих контролерів або додаватися новий функціонал, такий ViewController дуже скоро перетвориться в «MassiveViewController» і працювати з ним буде надзвичайно важко.

MVP

MVP-паттерн «еволюціонував» з MVC і складається з таких трьох компонентів:

  • Presenter (незалежний посередник UIKit);
  • Passive View (UIView і/або UIViewController);
  • Model.

Цей патерн визначає View як той, що отримує UI-події від користувача і тоді викликає відповідний Presenter, якщо це потрібно. Presenter відповідає за оновлення View новими даними, отриманими з моделі.

Переваги: краще розділення коду, добре тестується.

Недоліки: в порівнянні з MVC має значно більше коду, розробка та підтримка займають більше часу.

MVVM

Цей патерн зручний у проектах, де використовуються такі фреймворки, як ReactiveCocoa і RxSwift, в яких є концепція «зв'язування даних» — зв'язування даних з візуальними елементами в двосторонньому порядку. У цьому випадку використання патерну MVC є дуже незручним, оскільки прив'язка даних до представлення (View) — це порушення принципів MVC.

View (ViewController) і Model мають «посередника» — View Model. View Model — це незалежне від UIKit представлення View. View Model викликає зміни в Model і самостійно оновлюється з уже оновленою Model, і, оскільки зв'язування відбувається через View, то View також оновлюється.

Недоліком є те, що «замість 1000 рядків у ViewController може вийти 1000 рядків у ViewModel». Також одна з проблем використання фреймворків для «реактивного програмування» — досить просто все поламати, і може піти дуже багато часу на виправлення помилок. Комусь може здатися, що RxSwift, наприклад, спрощує написання коду, але досить заглянути в стек викликів друга «rx-» методу, щоб оцінити це «спрощення». Можна сюди ж додати проблеми з документацією та постійні проблеми з автозавершенням у xCode.

Viper (CleanSwift)

Від вищеперелічених патернів відрізняється тим, що не відноситься до категорії MVC. Замість звичних 3-х шарів він пропонує 5: View — ViewController — Router — Presenter — Interaptor.

Interaptor містить бізнес-логіку, пов'язану з даними (Entities).

Presenter містить бізнес-логіку, пов'язану з UI (але незалежну від UIKit), викликає методи в Interaptor.

Entity — прості об'єкти даних, вони не є шаром доступу до даних, оскільки це відповідальність Interaptor.

Router несе відповідальність за переходи між VIPER-модулями.

Навіть при такому поверхневому огляді очевидно, що краще розділення обов'язків досягається завдяки великій кількості класів з невеликою кількістю обов'язків.

Книги:

«Чистий Код», Роберт Мартін.

Посилання:

https://github.com/raywenderlich/swift-style-guide#correctness

https://refactoring.guru/uk

https://www.raywenderlich.com/46988/ios-design-patterns

https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52

https://clean-swift.com/clean-swift-ios-architecture/