Поднимаем читаемость кода в iOS разработке

Представьте себе книгу, в которой нет деления на главы, а все идет без логической и смысловой разбивки, книгу, где нет абзацев, нет точек и запятых, книгу, где в первой строке рассказывается про одно, во второй про другое, в третьей опять про первое.

Представили?

Смогли бы вы понять, о чем книга?

Насколько быстро вы смогли бы найти интересующий вас отрывок?

Ваш код, как и содержание книги, нуждается в структурировании, чтобы код был легко читаемым и передавал заложенный в нем смысл.

В данной статье я покажу примеры по организации кода, в которых классы будут иметь одинаковую последовательность основных блоков и их разбивку.

Для удобства я буду использовать слово класс (class), но подразумевать любой вид типа (class, struct, enum).

Благодаря применению этих советов ваш код станет читабельным, что в дальнейшем обеспечит удобство и скорость работы с ним.

Конечно, описанные советы можно модернизировать по своему вкусу, соблюдая основные принципы.

Для начала давайте сравним один и тот же код в двух вариантах.

Пример беспорядочного класса:



Данный код похож на свалку из методов, переменных и аутлетов, в котором все сливается воедино, сложно понять, что к чему относится и в каком месте что искать.

Пример чистого класса:



Пустая строка 38 — делайте отступ в одну строку от последнего метода, чтобы было видно где заканчивается последняя закрывающая скобка метода, а где заканчивается сам класс.

В обоих примерах приведен одинаковый функционал, но разница заключается в том, что второй вариант имеет четкую структуру, за счет чего логика более очевидна, код легко читается, в нем можно быстро найти искомое, а кроме того, на него просто приятно смотреть.

Основные принципы для формирования чистой структуры класса:


  1. Всегда используем // MARK: -
  2. Даем названия меток и устанавливаем их очередность
  3. Выносим логику из методов жизненного цикла в отдельные методы
  4. Используем extension для реализации протоколов
  5. Выделяем логически связанные элементы
  6. Убираем неиспользуемое
  7. Автоматизируем рутину

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


  1. Отмечайте класс ключевым словом final если данный класс не будет иметь наследников  -  проект быстрее компилируется, а код быстрее работает.
  2. Отмечайте свойства, аутлеты и методы ключевым словом private  —  они будут доступны только внутри класса и не будут в публичном списке свойств и методов, если они там не нужны.


Желаю всем успехов в разработке приложений и пусть ваш класс станет чище!

// MARK: — Помощь в написании статьи
Сергей Пчеляков
Алексей Плешков AlekseyPleshkov

// MARK: — Links
Ray Wenderlich code style
Поделиться публикацией

Комментарии 29

    0
    Всегда используем // MARK: -

    Вот это мне совершенно не понятно. Ну ладно, фиксированный порядок, хорошо. Но зачем еще дополнительно маркера-то вводить?


    Перечитал еще раз: "данная метка не только хорошо выделяется из всего кода, но и автоматически создает оглавление". Видимо, эта такая милая специфика языка/IDE. Моежт стоит тогда все-таки явно указывать, что эта рекомендация специфична?


    Хотя, впрочем, фиксированный порядок — тоже вопрос. Вот написано: сначала публичные методы, затем приватные методы. Почему так, а не "публичный метод и используемые им приватные методы"?

      +1
      На последний вопрос ответить легко. Публичные методы — интерфейс класса: то, как с ним взаимодействуют извне. Это то, как он выглядит «снаружи». Приватные методы/данные — детали реализации, знать которые надо только для написания самого класса, но не для его использования.
        +1
        Публичные методы — интерфейс класса: то, как с ним взаимодействуют извне. Это то, как он выглядит «снаружи».

        Во-первых, в таком случае они должны быть вместе с публичными свойствами, а они от них унесены неизвестно куда. Во-вторых… а почему, собственно, то, как класс выглядит "снаружи", должно влиять на то, как он расположен в файле?

          0
          Во-первых, в таком случае они должны быть вместе с публичными свойствами, а они от них унесены неизвестно куда.

          Не должны. Лучше иметь отдельный участок-секцию кода, где идут только свойства, упорядоченные по порядку паблик-readonly-приват. А потом уже идет конструктор, и лишь потом — публичные методы. Смешивать код по принципу модификатора доступа — не самая удачная идея, потому что их мало (часто используемых — всего два). Так ваш код разобьется всего на две огромных секции внутри файла. А разделение по смысловым элементам (typealias'ы, свойства, приватные свойства, конструктор/деструктор, методы, приватные методы, реализация протоколов) дает куда больше секций в коде, а значит, и повышает простоту восприятия его структуры. Аналогия: что проще, книга на тему «Программирование» из двух огромных общих глав, или из двадцати, но более конкретных?

          а почему, собственно, то, как класс выглядит «снаружи», должно влиять на то, как он расположен в файле?

          Потому что часто бывает задача изучить неизвестный класс, его функционал, и сделать это побыстрее, не отсматривая все тело файла. В первую очередь аккуратная «внешность» (создаваемая правильным порядком элементов) будет полезна вам, когда вы забудете, как устроен класс, в большом проекте. Во-вторых, она полезна новым людям, которые придут на проект и будут должны как можно быстрее изучить его. В большинстве случаев, в крупных проектах не нужно вникать во «внутренности» класса, потому что он и так нормально работает. Нужно лишь понять, какие возможности он предоставляет для пользования (те самые публичные элементы). И если все расположено правильно, то, увидев private, вы поймете, что публичные элементы уже закончились, дальше можно не смотреть, если нет задачи перетряхнуть внутренности. Если же это не так — то придется отсматривать весь файл.

          И да, есть вариант «отсматривать автоматически создаваемое подобие хедера файла» через спец. кнопку в Xcode, но это довольно тормозная штука, и поэтому, лично на мой взгляд, неудобная.
            0
            Не должны. Лучше иметь отдельный участок-секцию кода, где идут только свойства, упорядоченные по порядку паблик-readonly-приват.

            Но почему? Это же противоречит идее "сначала интерфейс класса, потом внутренности".


            Смешивать код по принципу модификатора доступа — не самая удачная идея

            Не "смешивать", а "объединять". И если "плохая идея" объединять публичные свойства и методы (особенно учитывая, что свойства — это, по большому счету, тоже методы), то непонятно, почему хорошая идея — объединять два публичных метода.


            Аналогия: что проще, книга на тему «Программирование» из двух огромных общих глав, или из двадцати, но более конкретных?

            Это зависит от того, что в этих главах содержится.


            Потому что часто бывает задача изучить неизвестный класс, его функционал, и сделать это побыстрее, не отсматривая все тело файла. [...] Нужно лишь понять, какие возможности он предоставляет для пользования

            Я для этого пользуюсь автодополнением, потому что тогда вообще не надо ходить в файл (и не надо пролистывать тела публичных методов).

          0
          Ctrl+Cmd+️
          0
          Почему так, а не «публичный метод и используемые им приватные методы»?

          Отвечу со своей колокольни. В варианте «сначала паблик, потом приват» при отсмотре файла вы точно знаете, что если видите приватный метод, то все, публичные — закончились. А если делать вперемешку, как вы предложили, то вы никогда не узнаете, где и когда в файле заканчиваются публичные методы. А учитывая, что свифтовый код пишется в одном файле (без хедеров, отделяющих публичную часть), то при изучении чужого кода на предмет «что там где находится, что умеет класс, какие свойства и методы отдает наружу и т.д.» вам будет на порядок сложнее разобрать каждый незнакомый вам класс. Ваш подход удобен лишь «для себя самого», пока на проекте все известно, и нет людей, не знакомых с кодом. А еще нет авралов.
            0
            Значит должно быть правило для swiftlint/swiftformat иначе уверенность в том что закончились может сыграть злую шутку.
            +1
            Перечитал еще раз: «данная метка не только хорошо выделяется из всего кода, но и автоматически создает оглавление». Видимо, эта такая милая специфика языка/IDE. Моежт стоит тогда все-таки явно указывать, что эта рекомендация специфична?

            Это же про ай-ос разработку, а там IDE — X code. Поэтому, как раз отклонение от нее — специфично.
              0
              Это же про ай-ос разработку, а там IDE — X code.

              Когда я писал свой комментарий, никаких упоминаний про iOS не было, и даже Swift был только в тегах.


              (ну и еще, я слышал, под iOS можно разрабатывать не только на в X code, говорят, даже кроссплатформенные инструменты есть)

                +1
                AppCode ещё есть. Очень удобно работать в ней.
            +1
            Отличная статья.

            Всегда стараюсь придерживаться тех же рекомендаций. Но узнал кое-что новое и для себя.

            Спасибо.
              +1
              Поддерживаю. Сам стараюсь в проектах, где работаю, добровольно-принудительно вводить такой же порядок. Очень помогает преобразить код из состояния «свалка» в состояние «книга».
                –1
                Данная метка

                Попробую догадаться: вы навязываете IDE?
                  +1
                  Не хочу показаться грубым, но видеть методы вроде setupNavigationBar во вьюконтроллере в примерах хорошего кода?
                  Можно сделать статический класс-гайдлайн с методами setupNavigationBar, setupSomeButton — так не придется в каждом вьюконтроллере дублировать одно и тоже. Как в одном из примеров showActivityIndicator(on viewController: UIViewController).
                    +1
                    А вставлю ка я свои пару центов.
                    IBAction private func cancelButtonPressed(_ sender: UIBarButtonItem) {}
                    Ежели почитать доку от яблочных, то мы увидим, что адептам данного течения настойчиво рекомендуется любое нажатие чач скрин клавиш называть TAP. Не PRESS, не CLICK, а именно TAP. То есть (со времен objective-c) было бы логичней метод назвать как нибудь так:
                    — didTapCancel(button: UIBarButtonItem)
                    — didTapCancelButton(_ sender: UIBarButtonItem)
                    — cancelAction(_ sender: UIBarButtonItem)
                    Хотя конечно же, это вкусовщина.

                    Идею разбивки классов на экстеншены по категориям как то UITableViewDataSource, UITableViewDelegate категорически поддерживаю.
                      0
                      Я привык называть функции didTouchButton(_ sender: UIButton), а коллеги просто touchButton(_ sender: UIButton) называют.
                        0
                        Плиз можете указать место где это яблочниками рекомендуется?
                        +1

                        Половина — просто вкусовщина.


                        1,2 — писать марки и давать им имена без смысловой нагрузки, которые просто констатируют капитанские факты (публичные, приватные) — странно, читаемость это точно не улучшит, стоит применять марки для разделения разных наборов методов внутри класса, например, работа с таблицей, работа с поиском и т.п.
                        3 — создавать множество одноразовых одно-двух-строчников также может ухудшить понимание происходящего в коде, часто придется скакать между использованием и реализацией.
                        4 — также ухудшает понимаемость класса, логичнее сразу в объявлении класса видеть все его наследования и интерфейсы, но с экстеншенами все интерфейсы будут разбросаны где-то далеко ниже по коду класса, придется весь класс просмотреть, чтобы понять его суть.


                        Также в статье про читаемость стоит привести в порядок примеры кода, в них нет единого консистентного кодстайла, выглядит как надерганные куски из гугла.


                        P.S. Интересно, когда таки айосники перестанут писать weak для аутлетов.

                          0
                          Т.е. вы утверждаете, что писать weak не нужно? Объяснитесь!
                          0
                          Интересно, когда таки айосники перестанут писать weak для аутлетов.
                          Хсоde weak вставляет сам если мышкой из Interface Builder тянуть.
                            0

                            В Xcode в появившемся окне при создании аутлета это выбирается.

                          +1
                          Выносите реализацию протоколов в extension

                          Что делать, если в протоколе объявлено свойство? Где его располагать?
                            0
                            Вычисляемое или переносить реализацию чисто в класс. А так этот совет касается в большей степени протоколов с методами (делегаты)
                            0
                            Любопытная статья, делаю все также, только вот вместе с Lifecycle я еще все override-ы сую.
                            Нормальная ли практика, если перекрывать все оконченые классы final?
                              0
                              Да, это хорошая практика отмечать класс как final если данный класс не будет иметь наследников.
                              0
                              Очень хорошо, хотя есть несколько мыслей на тему:

                              Чтобы ещё круче автоматизировать рутину — можно использовать готовый шаблон — тогда всем будет сразу видно куда вставлять IBOutlet а куда override

                              Имхо код в п.3 легче читается тот что Вами не рекомендован. И ещё я бы предпочёл
                              navigationController<b>!</b>.navigationBar.backgroundColor = .red
                              чтобы сразу упасть если кто-то решит использовать класс неожиданным образом. А читающему будет очевидно что раз не падает код то неожиданность в том что там был nil искать не следует.
                              Если ли же класс подразумевается для использования и под 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.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое