Для каждого программиста понятие «чистого кода» разное. В этой статье я попробую донести свои мысли по данному поводу, и, возможно, кое в чем даже изменить старые подходы.
Чистый код формируется совокупностью правил, которыми руководствуется разработчик для того, чтобы его код был понятным, гибким, читаемым (для других разработчиков, да и для самого автора кода). Исходя из своего опыта: если разработчика подгоняли, уменьшали ему сроки сдачи задач, или же разработчик по каким-то иным причинам не структурировал свой код, то он или его коллега позже потратит в полтора-два раза больше времени на багфиксинг или добавление нового функционала, чем если бы фичу сделали с нуля. Каждое внесенное изменение в логику работы может ломать логику в 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 ... {// comment “why”, “what” etc ... // code ....}}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){// Do something}}
Подробнее по пунктам:
- Добавление библиотек в шапке файла сгруппированным, делать два вертикальных отступа перед объявлением класса или структуры.
- Использование Pragma Mark предоставляет чудесную возможность группировать часть кода внутри класса по их типах и назначению.
- Группирование кода по логических принципах с помощью вертикальных отступов делает код более удобным для чтения. Как, например, правило всегда делать отступ в 1 строку между функциями, и отступ в 2 строки между группами Pragma Mark. Этот очевидный пункт я добавил потому, что имею опыт работы с разработчиками, которые вертикальных отступов не делают и пишут код одним полотном. Когда читаешь такой код, то глаза быстро устают, им тяжело зацепиться за информацию. Представьте себе газеты и журналы без абзацев и заголовков; такой текст читать крайне неудобно, а когда речь заходит о коде — так и подавно.
Более полный список Pragma Marks:
// MARK: Outlets// MARK: Properties// MARK: Overriden funcs// MARK: Action funcs// MARK: Notifcation observers// MARK: Public funcs:// MARK: Private funcs// MARK: Delegate funcs// MARK: Class(Static) funcs ...
Код выглядит еще более структурированным, если функции и переменные форматируются с помощью PragmaMarks в заданной последовательности, а не разбросаны случайным образом. Лично я добавил такой список в Xcode как снипеты, для удобства.
Хорошая практика, когда в компании есть документ с названием вроде «iOS Code Styles Guidline», с которым новый сотрудник ознакамливается перед началом работы и далее форматирует код в ожидаемом стиле.
Рефакторинг
Рефакторинг — это контролируемый процесс улучшения вашего кода без написания нового функционала. Задача рефакторинга — сделать код чистым.
Написание кода можно поделить на 2 пункта: сделать, чтобы код работал и провести рефакторинг кода. Что может пачкать наш код? Такие вещи, как:
- комментарии, о которых уже говорилось выше. Повторюсь: если написанный код требует объяснений, то следует сразу переписать этот код;
- дублирование кода. в большинстве случаев возникает, когда над одним проектом работает два или более разработчиков, которые в процессе написания имеют похожие задачи, но не знают, что их коллега уже написал такой код. Бывает, что разные разработчики решают одну и ту же задачу разными путями, такое дублирование выявить труднее. Другая причина дублирования — спешка (сроки сдачи, подгоняет ПМ или заказчик). Если дублирование происходит в одном классе — следует вынести код, который дублируется, в отдельный метод. Код дублируется в разных классах — вынести код в метод суперкласса; если же создание суперкласса невозможно, то следует вынести метод в отдельный класс;
- мертвый код. Часто случается, что добавление нового функционала целесообразнее начинать с нуля, чем редактировать уже написанную часть кода. В таких случаях, из-за спешки или недостатка опыта разработчик может добавить один или несколько классов, не удалив старые или более не нужные классы, оставив само удаления «на потом» и, как это всегда бывает, забыв о них. В масштабе одного класса эта ситуация может повторяться с переменными и функциями, которые больше не используются. Следует приучить себя всегда удалять ненужный код, который в дальнейшем достаточно сильно будет мешать пониманию и работе с кодом. Сегодня сэкономишь 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 (и их наследники).
Соntroller — посредник между View i Model. Координирует всю работу: реагирует на действия пользователя, выполненные на view, и обновляет view, используя model.
В идеале view полностью отделено от модели и ничего не знает о модели. Это позволит использовать view для отображения других данных.
Используя MVC, мы получаем такие преимущества:
- лучшее понимание классов;
- повторное использование классов, в том числе и в других проектах (в основном это касается Model и View);
- тестирование классов отдельно друг от друга;
- в сравнении с другими паттернами данный паттерн является самым простым и понятным в использовании, а также менее затратным в разрезе времени.
Недостатком MVC является то, что со временем, когда проект постепенно увеличивается и изменяется, Controller разрастается и в некоторых проектах может достигать отметки в 1000 строк кода и более, и чем дальше, тем больше это усложняет поддержку проекта: багфиксинг или добавления нового функционала. По этой причине некоторые разработчики «расшифровывают» аббревиатуру MVC как «Massive» ViewController :) Пока мы делаем что-то одно, мы боимся сломать что-то другое. В результате, работая с таким контроллером, разработчик не можем правильно оценить поставленную задачу, не укладывается в указанное время, теряет доверие и репутацию у заказчика. Здесь можно было бы обвинять клиента, потому что:
- клиент не хочет понимать, насколько трудно реализовать такое большое количество функционала на одном экране;
- клиент не понимает, что нельзя постоянно менять требования в ТЗ;
- клиенту многократно пытались объяснить, что сначала надо сосредоточиться на UX, а не на UI-дизайне;
- если бы клиент на старте проекта указал на этот функционал, то это заняло бы значительно меньше времени…
- ваш вариант :)
Но все это не отменяет того, что ситуация раз за разом будет повторяться.
Как можно «разгрузить» ViewController?
- Вынести dataSource-delegate функции в отдельные классы. В Swift мы имеем возможность выности UITableViewDataSourse & UITableViewDelegate функции в Extensions;
- Выносить логику в модель. В Obj-C можно часть логики вынести в категории, в Swift замечательным инструментом является Extensions и Protocols.
- Вынести код настройки объектов группы View в соответствующие классы;
- Вынести Web Service логику в модель, наприклад, в класс WebService;
- Вынести в отдельный класс логику работы с базой данных. В качестве примера CoreDataStack (или CoreDataManager);
- Не использовать повторно объекты группы Controller (наследники класса UIViewController). Увидев описание новой фичи, вы можете подумать: «У меня уже есть очень похожий viewController, я его использую, чтобы не дублировать код. А для разделения логики напишу протокол. Это сэкономит несколько часов ». Если вы уверены, что это финальная версия проекта — да, это сэкономит время. Но если, как часто бывает, будет меняться логика работы отдельных контроллеров или добавляться новый функционал, такой ViewController очень скоро превратится в «MassiveViewController» и работать с ним будет чрезвычайно трудно.
MVP
MVP-паттерн «эволюционировал» из MVC и состоит из таких трех компонентов:
- Presenter (независимый посредник UIKit);
- Passive View (UIView и/или UIViewController);
- Model.
Этот паттерн определяет View как получающий UI-события от пользователя и тогда вызывает соответствующий Presenter, если это нужно. Presenter же отвечает за обновление View с новыми данными, полученными из модели.
Преимущества: лучшее разделение кода, хорошо тестируется.
Недостатки: сравнительно с MVC имеет значительно больше кода, разработка и поддержка занимают больше времени.
MVVM
Этот паттерн удобен в проектах, где используются такие фреймворки, как ReactiveCocoa i 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://www.raywenderlich.com/46988/ios-design-patterns
https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52