Представили?
Смогли бы вы понять, о чем книга?
Насколько быстро вы смогли бы найти интересующий вас отрывок?
Ваш код, как и содержание книги, нуждается в структурировании, чтобы код был легко читаемым и передавал заложенный в нем смысл.
В данной статье я покажу примеры по организации кода, в которых классы будут иметь одинаковую последовательность основных блоков и их разбивку.
Для удобства я буду использовать слово класс (class), но подразумевать любой вид типа (class, struct, enum).
Благодаря применению этих советов ваш код станет читабельным, что в дальнейшем обеспечит удобство и скорость работы с ним.
Конечно, описанные советы можно модернизировать по своему вкусу, соблюдая основные принципы.
Для начала давайте сравним один и тот же код в двух вариантах.
Пример беспорядочного класса:
Данный код похож на свалку из методов, переменных и аутлетов, в котором все сливается воедино, сложно понять, что к чему относится и в каком месте что искать.
Пример чистого класса:
Пустая строка 38 — делайте отступ в одну строку от последнего метода, чтобы было видно где заканчивается последняя закрывающая скобка метода, а где заканчивается сам класс.
В обоих примерах приведен одинаковый функционал, но разница заключается в том, что второй вариант имеет четкую структуру, за счет чего логика более очевидна, код легко читается, в нем можно быстро найти искомое, а кроме того, на него просто приятно смотреть.
Основные принципы для формирования чистой структуры класса:
- Всегда используем // MARK: -
- Даем названия меток и устанавливаем их очередность
- Выносим логику из методов жизненного цикла в отдельные методы
- Используем extension для реализации протоколов
- Выделяем логически связанные элементы
- Убираем неиспользуемое
- Автоматизируем рутину
1. Всегда используем // MARK: -
Для удобства чтения книга разбивается на главы, так и нам будет комфортней работать, если создать оглавление класса с помощью // MARK: -.
Данная метка не только хорошо выделяется из всего кода, но и автоматически создает оглавление? -? выделяет разделы в коде жирным шрифтом в списке элементов данного файла.
Посмотреть оглавление файла можно нажав на кнопку после стрелочки направо (>) в самом верху файла после названия данного файла или ctr + 6 (document items menu).
2. Даем названия меток и устанавливаем их очередность
Ниже приведены основные метки для разбиения кода на логически связанные блоки и их последовательность:
При использовании данного способа группировки, можно без труда ориентироваться в коде любого класса.
3. Выносим логику из методов жизненного цикла в отдельные методы
Логика внутри методов жизненного цикла ViewController'a должна быть вынесена в отдельные методы, даже если придется создавать метод с одной строчкой кода. Сегодня одна, а завтра десять.
За счет того, что детали реализации вынесены в сторонние методы, логика жизненного цикла становится яснее.
4. Используем extension для реализации протоколов
Выносите реализацию протоколов в extension с пометкой // MARK: — SomeProtocol:
По данной метке будет лежать все, что связанно с этим протоколом ?-? все что есть только тут и больше никуда ходить не надо, в противном случае методы и свойства протокола буду разбросаны по всему классу.
5. Выделяем логически связанные элементы
Для повышения наглядности необходимо выделять логически связанные элементы с использованием пустой строки:
6. Убираем неиспользуемое
Не оставляйте лишние комментарии (дефолтные), пустые методы или мертвый функционал ?- ?это засоряет код. Обратите внимание на класс AppDelegate, скорее всего вы обнаружите там пустые методы с комментариями внутри.
7. Автоматизируем рутину
Чтобы не писать вручную в каждом классе // MARK: — SomeMark, используйте Code Snippet.
Пишем метку, выделяем ее, далее Editor -> Create Code Snippet, даем ей название и вызываем по шорткату.
// MARK: — Bonus
- Отмечайте класс ключевым словом final если данный класс не будет иметь наследников? -? проект быстрее компилируется, а код быстрее работает.
- Отмечайте свойства, аутлеты и методы ключевым словом private? — ?они будут доступны только внутри класса и не будут в публичном списке свойств и методов, если они там не нужны.
Желаю всем успехов в разработке приложений и пусть ваш класс станет чище!
// MARK: — Помощь в написании статьи
Сергей Пчеляков
Алексей Плешков medium.com/@AlekseyPleshkov
// MARK: — Links
Ray Wenderlich code style
Комментарии (27)
RileyUsagi
25.05.2019 19:26+1Отличная статья.
Всегда стараюсь придерживаться тех же рекомендаций. Но узнал кое-что новое и для себя.
Спасибо.
GDXRepo
25.05.2019 19:44+1Поддерживаю. Сам стараюсь в проектах, где работаю, добровольно-принудительно вводить такой же порядок. Очень помогает преобразить код из состояния «свалка» в состояние «книга».
nubideus
26.05.2019 00:16Не хочу показаться грубым, но видеть методы вроде setupNavigationBar во вьюконтроллере в примерах хорошего кода?
Можно сделать статический класс-гайдлайн с методами setupNavigationBar, setupSomeButton — так не придется в каждом вьюконтроллере дублировать одно и тоже. Как в одном из примеров showActivityIndicator(on viewController: UIViewController).
Yoooriii
26.05.2019 01:47А вставлю ка я свои пару центов.
IBAction private func cancelButtonPressed(_ sender: UIBarButtonItem) {}
Ежели почитать доку от яблочных, то мы увидим, что адептам данного течения настойчиво рекомендуется любое нажатие чач скрин клавиш называть TAP. Не PRESS, не CLICK, а именно TAP. То есть (со времен objective-c) было бы логичней метод назвать как нибудь так:
— didTapCancel(button: UIBarButtonItem)
— didTapCancelButton(_ sender: UIBarButtonItem)
— cancelAction(_ sender: UIBarButtonItem)
Хотя конечно же, это вкусовщина.
Идею разбивки классов на экстеншены по категориям как то UITableViewDataSource, UITableViewDelegate категорически поддерживаю.RedBear961
27.05.2019 19:37Я привык называть функции didTouchButton(_ sender: UIButton), а коллеги просто touchButton(_ sender: UIButton) называют.
eugeneego
26.05.2019 10:47+3Половина — просто вкусовщина.
1,2 — писать марки и давать им имена без смысловой нагрузки, которые просто констатируют капитанские факты (публичные, приватные) — странно, читаемость это точно не улучшит, стоит применять марки для разделения разных наборов методов внутри класса, например, работа с таблицей, работа с поиском и т.п.
3 — создавать множество одноразовых одно-двух-строчников также может ухудшить понимание происходящего в коде, часто придется скакать между использованием и реализацией.
4 — также ухудшает понимаемость класса, логичнее сразу в объявлении класса видеть все его наследования и интерфейсы, но с экстеншенами все интерфейсы будут разбросаны где-то далеко ниже по коду класса, придется весь класс просмотреть, чтобы понять его суть.
Также в статье про читаемость стоит привести в порядок примеры кода, в них нет единого консистентного кодстайла, выглядит как надерганные куски из гугла.
P.S. Интересно, когда таки айосники перестанут писать weak для аутлетов.
Awaking
26.05.2019 15:06Выносите реализацию протоколов в extension
Что делать, если в протоколе объявлено свойство? Где его располагать?
ftp27
26.05.2019 16:48Любопытная статья, делаю все также, только вот вместе с Lifecycle я еще все override-ы сую.
Нормальная ли практика, если перекрывать все оконченые классы final?
Speakus
27.05.2019 21:41Очень хорошо, хотя есть несколько мыслей на тему:
Чтобы ещё круче автоматизировать рутину — можно использовать готовый шаблон — тогда всем будет сразу видно куда вставлять IBOutlet а куда override
Имхо код в п.3 легче читается тот что Вами не рекомендован. И ещё я бы предпочёл
чтобы сразу упасть если кто-то решит использовать класс неожиданным образом. А читающему будет очевидно что раз не падает код то неожиданность в том что там был nil искать не следует.navigationController<b>!</b>.navigationBar.backgroundColor = .red
Если ли же класс подразумевается для использования и под navigationController и без оного — то лучше как минимум вставить коммент об этом явным образом.
Ещё я предпочитаю вставлять internal чтобы было видно где свойство явно внешнее а где забыли прописать private.
А вообще рекомендую все var делать private + иметь
Или вообщеfunc configure(with delegate: SomeDelegate, userId: String)
static func start(from navCtrl: UINavigationController, with delegate: SomeDelegate, ...)
Кстати. Чтобы получить проверку на этапе компиляции лучше сделать класс UserId — тогда никто случайно FamilyId не отправит туда где должен быть UserId. Что легко возможно если используется String.
lair
Вот это мне совершенно не понятно. Ну ладно, фиксированный порядок, хорошо. Но зачем еще дополнительно маркера-то вводить?
Перечитал еще раз: "данная метка не только хорошо выделяется из всего кода, но и автоматически создает оглавление". Видимо, эта такая милая специфика языка/IDE. Моежт стоит тогда все-таки явно указывать, что эта рекомендация специфична?
Хотя, впрочем, фиксированный порядок — тоже вопрос. Вот написано: сначала публичные методы, затем приватные методы. Почему так, а не "публичный метод и используемые им приватные методы"?
OldFisher
На последний вопрос ответить легко. Публичные методы — интерфейс класса: то, как с ним взаимодействуют извне. Это то, как он выглядит «снаружи». Приватные методы/данные — детали реализации, знать которые надо только для написания самого класса, но не для его использования.
lair
Во-первых, в таком случае они должны быть вместе с публичными свойствами, а они от них унесены неизвестно куда. Во-вторых… а почему, собственно, то, как класс выглядит "снаружи", должно влиять на то, как он расположен в файле?
GDXRepo
Не должны. Лучше иметь отдельный участок-секцию кода, где идут только свойства, упорядоченные по порядку паблик-readonly-приват. А потом уже идет конструктор, и лишь потом — публичные методы. Смешивать код по принципу модификатора доступа — не самая удачная идея, потому что их мало (часто используемых — всего два). Так ваш код разобьется всего на две огромных секции внутри файла. А разделение по смысловым элементам (typealias'ы, свойства, приватные свойства, конструктор/деструктор, методы, приватные методы, реализация протоколов) дает куда больше секций в коде, а значит, и повышает простоту восприятия его структуры. Аналогия: что проще, книга на тему «Программирование» из двух огромных общих глав, или из двадцати, но более конкретных?
Потому что часто бывает задача изучить неизвестный класс, его функционал, и сделать это побыстрее, не отсматривая все тело файла. В первую очередь аккуратная «внешность» (создаваемая правильным порядком элементов) будет полезна вам, когда вы забудете, как устроен класс, в большом проекте. Во-вторых, она полезна новым людям, которые придут на проект и будут должны как можно быстрее изучить его. В большинстве случаев, в крупных проектах не нужно вникать во «внутренности» класса, потому что он и так нормально работает. Нужно лишь понять, какие возможности он предоставляет для пользования (те самые публичные элементы). И если все расположено правильно, то, увидев private, вы поймете, что публичные элементы уже закончились, дальше можно не смотреть, если нет задачи перетряхнуть внутренности. Если же это не так — то придется отсматривать весь файл.
И да, есть вариант «отсматривать автоматически создаваемое подобие хедера файла» через спец. кнопку в Xcode, но это довольно тормозная штука, и поэтому, лично на мой взгляд, неудобная.
lair
Но почему? Это же противоречит идее "сначала интерфейс класса, потом внутренности".
Не "смешивать", а "объединять". И если "плохая идея" объединять публичные свойства и методы (особенно учитывая, что свойства — это, по большому счету, тоже методы), то непонятно, почему хорошая идея — объединять два публичных метода.
Это зависит от того, что в этих главах содержится.
Я для этого пользуюсь автодополнением, потому что тогда вообще не надо ходить в файл (и не надо пролистывать тела публичных методов).
AlexRoch
Ctrl+Cmd+?
GDXRepo
Отвечу со своей колокольни. В варианте «сначала паблик, потом приват» при отсмотре файла вы точно знаете, что если видите приватный метод, то все, публичные — закончились. А если делать вперемешку, как вы предложили, то вы никогда не узнаете, где и когда в файле заканчиваются публичные методы. А учитывая, что свифтовый код пишется в одном файле (без хедеров, отделяющих публичную часть), то при изучении чужого кода на предмет «что там где находится, что умеет класс, какие свойства и методы отдает наружу и т.д.» вам будет на порядок сложнее разобрать каждый незнакомый вам класс. Ваш подход удобен лишь «для себя самого», пока на проекте все известно, и нет людей, не знакомых с кодом. А еще нет авралов.
Speakus
Значит должно быть правило для swiftlint/swiftformat иначе уверенность в том что закончились может сыграть злую шутку.
ukku
Это же про ай-ос разработку, а там IDE — X code. Поэтому, как раз отклонение от нее — специфично.
lair
Когда я писал свой комментарий, никаких упоминаний про iOS не было, и даже Swift был только в тегах.
(ну и еще, я слышал, под iOS можно разрабатывать не только на в X code, говорят, даже кроссплатформенные инструменты есть)
MeGaPk
AppCode ещё есть. Очень удобно работать в ней.