- Свифт Лаб
- 16 января, 2019
Улучшении структуры 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
- View Controllers
- Сеть
- Модель
- Request.swift
- Endpoint.swift
- Логика
- DataLoader.swift
- RequestFactory.swift
- Расширения
- URL+Endpoint.swift
- URLSession+RequestFactory.swift
- Модель
Самое полезное в разделении функций для структурирования и организации нашего проекта, как в примере выше, это создание дополнительного уровня иерархии. В ином случае, если мы расположим все на верхнем уровне, например в View Controllers, мы рискуем превратить наш код в свалку.
Если вы хотите поддерживать надежную структуру проекта, нужно определиться с ключевыми принципами, которыми будет пользоваться вся команда, например, правило трех или организация по функционалу. А затем вам потребуется непрерывно уделять время на выполнение этого правила. Один из способов добиться успеха в этом — придерживаться правила бойскаута: после работы с кодовой базой вы должны оставить код чище, чем он был до вашего прихода.
Как и большинство вещей в программировании, в организации и структурировании проекта нет универсального решения. Каждый проект уникален, поэтому единственный правильный путь для всех — это придерживаться общепринятых правил стиля для имён переменных, идентификаторов и прочего, а также соблюдать интуитивно понятную иерархию.
👉Хочешь больше новостей из мира Swift и iOS разработки?
❤️ Лайк и подписка на @swiftlab приветствуются.