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

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

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

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

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

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

Для удобства я буду использовать слово класс (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: — Помощь в написании статьи
Сергей Пчеляков
Алексей Плешков medium.com/@AlekseyPleshkov

// MARK: — Links
Ray Wenderlich code style

Комментарии (27)


  1. lair
    25.05.2019 16:34
    +1

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

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


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


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


    1. OldFisher
      25.05.2019 18:54
      +1

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


      1. lair
        25.05.2019 19:00

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

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


        1. GDXRepo
          25.05.2019 19:51

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

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

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

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

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


          1. lair
            25.05.2019 20:05

            Не должны. Лучше иметь отдельный участок-секцию кода, где идут только свойства, упорядоченные по порядку паблик-readonly-приват.

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


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

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


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

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


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

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


      1. AlexRoch
        27.05.2019 12:01

        Ctrl+Cmd+?


    1. GDXRepo
      25.05.2019 19:47

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

      Отвечу со своей колокольни. В варианте «сначала паблик, потом приват» при отсмотре файла вы точно знаете, что если видите приватный метод, то все, публичные — закончились. А если делать вперемешку, как вы предложили, то вы никогда не узнаете, где и когда в файле заканчиваются публичные методы. А учитывая, что свифтовый код пишется в одном файле (без хедеров, отделяющих публичную часть), то при изучении чужого кода на предмет «что там где находится, что умеет класс, какие свойства и методы отдает наружу и т.д.» вам будет на порядок сложнее разобрать каждый незнакомый вам класс. Ваш подход удобен лишь «для себя самого», пока на проекте все известно, и нет людей, не знакомых с кодом. А еще нет авралов.


      1. Speakus
        27.05.2019 21:21

        Значит должно быть правило для swiftlint/swiftformat иначе уверенность в том что закончились может сыграть злую шутку.


    1. ukku
      25.05.2019 21:52

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

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


      1. lair
        25.05.2019 21:53

        Это же про ай-ос разработку, а там IDE — X code.

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


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


        1. MeGaPk
          26.05.2019 09:40

          AppCode ещё есть. Очень удобно работать в ней.


  1. RileyUsagi
    25.05.2019 19:26
    +1

    Отличная статья.

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

    Спасибо.


  1. GDXRepo
    25.05.2019 19:44
    +1

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


  1. kloppspb
    25.05.2019 23:13

    Данная метка

    Попробую догадаться: вы навязываете IDE?


  1. nubideus
    26.05.2019 00:16

    Не хочу показаться грубым, но видеть методы вроде setupNavigationBar во вьюконтроллере в примерах хорошего кода?
    Можно сделать статический класс-гайдлайн с методами setupNavigationBar, setupSomeButton — так не придется в каждом вьюконтроллере дублировать одно и тоже. Как в одном из примеров showActivityIndicator(on viewController: UIViewController).


  1. 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 категорически поддерживаю.


    1. RedBear961
      27.05.2019 19:37

      Я привык называть функции didTouchButton(_ sender: UIButton), а коллеги просто touchButton(_ sender: UIButton) называют.


    1. Speakus
      27.05.2019 21:43

      Плиз можете указать место где это яблочниками рекомендуется?


  1. eugeneego
    26.05.2019 10:47
    +3

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


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


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


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


    1. AlexRoch
      27.05.2019 12:46

      Т.е. вы утверждаете, что писать weak не нужно? Объяснитесь!



      1. eugeneego
        29.05.2019 01:57
        -1

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


    1. Speakus
      27.05.2019 21:46

      Интересно, когда таки айосники перестанут писать weak для аутлетов.
      Хсоde weak вставляет сам если мышкой из Interface Builder тянуть.


      1. eugeneego
        29.05.2019 01:59

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


  1. Awaking
    26.05.2019 15:06

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

    Что делать, если в протоколе объявлено свойство? Где его располагать?


  1. ftp27
    26.05.2019 16:48

    Любопытная статья, делаю все также, только вот вместе с Lifecycle я еще все override-ы сую.
    Нормальная ли практика, если перекрывать все оконченые классы final?


  1. Speakus
    27.05.2019 21:41

    Очень хорошо, хотя есть несколько мыслей на тему:

    Чтобы ещё круче автоматизировать рутину — можно использовать готовый шаблон — тогда всем будет сразу видно куда вставлять 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.