Share on facebook
Facebook
Share on twitter
Twitter
Share on linkedin
LinkedIn
Share on pinterest
Pinterest
Share on vk
VK
Share on odnoklassniki
OK
Share on telegram
Telegram
Share on whatsapp
WhatsApp

Улучшении структуры Swift проектов

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

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

Давайте рассмотрим несколько способов улучшения структуры проектов в Swift. 

Свалка кода

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

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

Свалку легко обнаружить, поискав типы, файлы и папки, которые содержат в себе множество несвязанных функций. Например, часто разработка iOS-приложения начинается с создания BaseViewController, который содержит в себе общие функции для всех контроллеров представления (view controllers). Базовый класс может показаться действительно удобным решением, и в начале в нем может быть не так много функций. Но затем такой способ начинает использоваться по умолчанию для размещения любого функционала и быстро превращается в свалку разного кода, который никак не связан между собой.

Еще один распространенный источник свалки кода — это папки, которые называются Library, Utilities или Helpers. Подобная папка также может начинаться с небольшого количества расширений и вспомогательных методов, но в итоге она рискует превратиться в хранилище кода, который некуда деть.

 

Разбивка кода

Основная проблема, связанная со свалкой кода, заключается в том, что мы создаем папки, файлы или типы со слишком общим назначением. Мы можем назвать именами Helpers или Utility практический все, а название с приставкой base не говорит ни о чем. Если использовать более четкие названия и задавать различным частям кодовой базы более узкую направленность, будет проще поддерживать четкую структуру.

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

Также, если у нас есть огромный файл String+Utilities.swift, который содержит в себе множество различных расширений String, мы можем разбить его на несколько файлов, каждый из которых будет состоять из подмножества этих расширений. Мы можем создать один файл для разделения строк (String+Splitting.swift), один для определения API констант (String+APIConstants.swift) и так далее. Приучая себя определять, к чему относится то или иное расширение, мы в итоге получаем структуру, в которой легко найти нужное и с которой удобно работать.

 

Правило трех

Главное в разбивке кода — это баланс. Если мы перестараемся, то не улучшим структуру нашего проекта, а получим множество отдельных файлов и типов, в которых будет трудно разобраться. Один из способов достигнуть баланса — следовать «правилу трех»:

 

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

 

Например, представьте, что мы создаем приложение «Адресная книга», которая включает в себя ContactViewController для отображение одного из контактов. Его UI состоит из трех компонентов: заголовок, табличное представление, которое отображает всю информацию о контакте, и табличное представление, содержащее в себе различные действия. На данный момент они все установлены внутри ContactViewController и выглядят так:

 

class ContactViewController: UIViewController {
    private lazy var headerImageView = UIImage()
    private lazy var headerLabel = UILabel()
    private lazy var headerButton = UIButton()
    private lazy var infoTableView = UITableView()
    private lazy var actionsTitleLabel = UILabel()
    private lazy var actionsSubtitleLabel = UILabel()
    private lazy var actionsStackView = UIStackView()
}

 

Применяя правило трех к вышеуказанному классу, мы сможем увидеть, насколько полезна будет разбивка. У нас есть три свойства, которые используют префикс header, тоже самое и с префиксом actions. Исходя из этого, мы можем извлечь эти свойства в свои собственные выделенные типы, например c header:

 

class ContactHeaderView: UIView {
    let imageView = UIImageView()
    let label = UILabel()
    let button = UIButton()
}

 

Сделав тоже самое с action, мы улучшим структуру ContactViewController и сделаем его проще в управлении:

 

class ContactViewController: UIViewController {
    private lazy var headerView = ContactHeaderView()
    private lazy var infoTableView = UITableView()
    private lazy var actionsView = ContactActionsView()
}

 

Теперь гораздо проще понять, за что отвечает ContactViewController: код имеет более прочную структуру и менее вероятно, что он превратится в свалку.

Другой способ, с помощью которого можно достичь той же цели, это включить ContactViewController в пользовательский контейнер контроллера.

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

 

func saveDocument() {
    guard let titleText = titleLabel.text else {
        return
    }

    guard !titleText.isEmpty else {
        return
    }

    let titleFontIndex = titleFontPicker.selectedSegmentIndex
    let titleFont = fonts[titleFontIndex]
    let titleAlignmentIndex = titleTextAligmentPicker.selectedSegmentIndex
    let titleAlignment = textAlignments[titleAlignmentIndex]

    ...
}

 

Попробуем применить правило трех для этой функции, поскольку в ней используется более трех ссылок связанных с title, давайте создадим для него отдельную функцию:

 

func makeTitle() -> Article.Title? {
    guard let text = titleLabel.text else {
        return nil
    }

    guard !text.isEmpty else {
        return nil
    }

    let fontIndex = titleFontPicker.selectedSegmentIndex
    let font = fonts[fontIndex]
    let alignmentIndex = titleTextAligmentPicker.selectedSegmentIndex
    let alignment = textAlignments[alignmentIndex]

    return .init(text: text, font: font, alignment: alignment)
}

 

Теперь после рефакторинга код стало легче читать, а структура кода стала надежнее. Теперь мы можем отбросить префикс title и сделать наш код более чистым.

Любое правило существует, чтобы его нарушать (по крайней мере в программировании). Правило трех не исключение — его не нужно заучивать, просто нужно понимать, в каких обстоятельствах оно применимо, а в каких его можно нарушать.

 

Функционал

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

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

Вот как можно организовать структуру в Xcode для одной пользовательской функции (поиск) и одной функции системного уровня (сеть):

Функционал

  • Поиск
    • View Controllers
      • SearchResultsViewController.swift
      • SearchViewController.swift
    • Модель
      • SearchNetworkResponse.swift
      • SearchResult.swift
    • Отображение
      • SearchBar.swift
      • SearchTableViewCell.swift
    • Логика
      • SearchLogicController.swift
      • SearchResultsLoader.swift
  • Сеть
    • Модель
      • Request.swift
      • Endpoint.swift
    • Логика
      • DataLoader.swift
      • RequestFactory.swift
    • Расширения
      • URL+Endpoint.swift
      • URLSession+RequestFactory.swift

 

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

Если вы хотите поддерживать надежную структуру проекта, нужно определиться с ключевыми принципами, которыми будет пользоваться вся команда, например, правило трех или организация по функционалу. А затем вам потребуется непрерывно уделять время на выполнение этого правила. Один из способов добиться успеха в этом — придерживаться правила бойскаута: после работы с кодовой базой вы должны оставить код чище, чем он был до вашего прихода.

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

 

👉Хочешь больше новостей из мира Swift и iOS разработки?
❤️ Лайк и подписка на @swiftlab приветствуются.

Пролистать наверх