- PVSM.RU - https://www.pvsm.ru -
За 9 лет работы фич в проектах роботов становилось все больше, запутаться в коде становилось все проще.
Когда разработчиков стало больше десятка, появилась еще одна проблема – болезненная ротация людей между проектами. Аутсорс-разработка славится жесткими дедлайнами, и у разработчиков нет месяцев или недель на погружение в особенности нового проекта, в то же время работа над разными проектами нужна для развития специалистов.
Главная проблема, которая возникает при долгосрочном развитии приложения – масштабируемость. Решить ее может переход на новую архитектуру или рефакторинг кодовой базы и добавление новых сущностей, которые разгружают объекты с большим количеством обязанностей.
В нашем понимании не существует такого понятия как «универсальная архитектура». Каждый делает выбор в пользу той, с которой эффективнее и удобнее всего работать на протяжении всего проекта. То, что эффективно используется у нас, может стать бесполезным overhead’ом для других команд.
Началось все с популярного MVC и его приятеля network manager.
Основные проблемы MVC: вызов сетевых запросов и запросы к базе данных, реализация бизнес-логики и навигация расположены в контроллере. Из-за этого объекты сильно взаимосвязаны, а уровень переиспользования и тестирования низок.
Network manager в серьезных проектах превращается в “божественный объект” [1], который становится невозможно поддерживать из-за его размера.
Представим, что у нас приложение для сети магазинов, в котором есть экраны акций, профиля и настроек. На первом экране отображается список действующих акций, в профиле – текущий бонусный баланс, имя и номер телефона пользователя, в настройках, например, возможность включить push-уведомления о новых акциях. Для входа в приложение необходимо авторизоваться с помощью логина и пароля.
Получается, что в таком случае все запросы к серверу – авторизация, получения списка акций, получение информации по профилю, изменение настроек – находятся в network manager, в том числе логика по созданию модельных объектов из JSON.
На слое взаимодействия с сетью было принято решение придерживаться подхода SOA – разделения сервисного слоя на множество сервисов в зависимости от типа сущности.
В качестве ConcreteService
в нашем примере будут выступать – AuthService, UserService, DealService, SettingsService
. Каждый сервис занимается своим делом – сервис авторизации работает с авторизацией, сервис пользователя – с данными пользователя и так далее. Хорошим правилом является разделение со стороны сервера на разные path: /auth, /user, /deals, /settings
, но необязательно.
Более подробное описание сервисного слоя есть в нашей прошлой статье [2].
Сериализацию/десериализацию объектов выделяем в отдельные сущности: parser [3] и serializer. Операции взаимно обратные: parser преобразует объект типа данных, который принимает от сервера, в модельный объект, serializer – из модельного объекта в объект данных для передачи по сети. Внутри этих классов реализована проверка обязательности полей и логирование ошибок.
class UserParser: JSONParser<User> {
func parseObject(_ data: JSON) -> User?
}
class UserSerializer: JSONSerializer<User> {
func serializeObject(_ object: User) -> Data?
}
Для каждой сущности у нас отдельные парсеры AuthParser, UserParser, DealParser
и SettingsParser
. С сериализаторами – ровно так же.
В своей архитектуре мы придерживаемся разделения на слои, верхний слой знает только о существование нижнего.
Выше по порядку: слой пользовательского интерфейса, слой бизнес-логики, слой сервисов и слой данных.
Этот слой мы чаще всего реализуем через паттерн DAO [4], абстрагируясь от реализации и особенностей базы данных на всех остальных слоях. У нас есть готовые решения для Realm и CoreData, чаще всего используем Realm. Пример реализации тут [5].
Представим, что в приложении мы хотим кэшировать скидки. Поэтому при использовании DAO у нас появятся следующие классы:
DBDeal
– сущность скидки в БД Realm.DealTranslator
– транслятор сущностей.DealDAO
– DAO для скидок.В разделе «Кодогенерация» я приведу примеры реализации данных классов.
Здесь у нас было несколько итераций, в ходе которых мы анализировали существующие решения: MVVM и VIPER, но без практического применения было сложно оценить их объективно. VIPER для наших проектов показался избыточным: большое количество сущностей для одного модуля (во многих случаях являются только посредниками в цепочке вызовов), сложная реализация роутинга с использованием storyboard’ов, отдаление от UIKit. Конечно, тестирование модулей можно отнести к плюсам.
Использование MVVM, по нашему мнению, было проще для понимания со знанием MVC, биндинги решали проблему явных вызовов для обновления данных, стало возможно писать тестируемый код. Проблем с использованием реактивного программирования не было – мы использовали его в связке с MVC.
Эта архитектура разобрана [6] в деталях, и вряд ли стоит пытаться делать это n+1 раз. Какое преимущество перед MVC мы здесь увидели? В большинстве случаев информация, отображаемая пользователю, является преобразованием моделей от сервера. Поэтому логика по преобразованию этой информации инкапсулируется внутрь view model, или, если есть зависимость между объектами, частично в фабрике view model’ей. Пример того, как номер телефона пользователя преобразуется для дальнейшего отображения на экране:
Через какое-то время мы поняли, что одним MVVM не обойдемся. Класс view controller постепенно «распухал», особенно это было заметно, если на экране вызывается несколько запросов. Следующим шагом выделили обращение к сервисам в отдельную сущность – presentation model и view controller перестал знать об их существовании.
Использование навигации (с segue или без них) на множество экранов так же приводило к разрастанию view controller. Замечу, что сам по себе вызов для показа экрана займет у вас 2-3 строчки кода, тогда как конфигурация и передача нужных данных на другой экран может занять, скажем, 10 строк. Поэтому router был выделен в отдельную сущность (да-да, еще чуть-чуть и VIPER). Использование роутера оказалось удобным в том числе, когда мы отказались от stroyboard'ов в пользу xib. Читать класс router'а, безусловно, тяжелее, чем визуально воспринимать карту экранов с переходами. Но еще менее удобно, если ваш код навигации разбросан повсюду.
Router в этой схеме – отдельное свойство на view controller'е, который создается в методе viewDidLoad
. На каких-то проектах мы создавали его непосредственно в момент совершения навигации.
Здесь важно понимать, что мы не требуем соблюдение разделения на presentation model и router для относительно простых экранов, скажем, где все помещается в 200 строк.
К примеру, у роутера в нашем приложении будут методы
showProfile()
– показать профиль пользователя.showDeal(_ deal: Deal)
– показать подробное описание скидки.showSettings()
– показать настройки.Так как настройки и пользователь в приложении существует в единственном экземпляре, нет необходимости передавать его в роутер для конфигурации нового экрана. Напротив, имея множество скидок, presentation model экрана подробной информации о скидке должен быть создан с параметром сущности скидки (или view model’и, если ее достаточно).
Изначально мы создавали реализацию data source и delegate отдельным классом, который хранили на view controller'е. Этот класс в свою очередь забирал данные (view model'и) из presentation model.
Cell mapper в данной схеме – замыкание, которое приводит в соответствие класс ячейки классу view model. Это сделано для того, чтобы не регистрировать вручную классы ячеек на каждом экране.
Таким образом, мы выделили большую часть кода data source и delegate в отдельную сущность.
Попробовали, оказалось, что делегирование в отдельном классе неудобно, а при выделении одного data source выигрыш не столь существенен.
Поэтому следующей итерацией перешли к использованию table presentation model в качестве data source, view controller стал delegate'ом.
Схема упростилась, ушли ненужные сущности data source и cell mapper. Проще – лучше.
Схема упростилась, ушли ненужные сущности data source и cell mapper. Проще – лучше.
Реализация роутинга, которая была описана выше, плоха тем, что все переходы жестко прописаны во view controller'е. Для реализации слабой связанности между навигацией и внутренним устройством отдельного view controller'а мы делаем следующее:
Итого, view controller перестал обладать знанием о router'е.
Отдельно стоит упомянуть еще одну особенность разработки в Redmadrobot – это использование кодогенерации. На основе модельных сущностей с помощью консольной утилиты генерируются parser'ы [7], translator'ы [8] для DAO [5].
/*
Скидка
@model
*/
class Deal: Entity {
/*
Заголовок
@json
*/
let title: String
/*
Подзаголовок
@json
*/
let subtitle: String?
/*
Дата окончания
@json end_date
*/
let endDateString: String
init(title: String, subtitle: String?, endDateString: String) {
self.title = title
self.subtitle = subtitle
self.endDateString = endDateString
super.init()
}
}
Имеем написанный собственноручно класс скидки Deal
. На основе его и вспомогательных аннотаций (@model, @json
) утилита кодогенерации создает класс парсера DealParser
, класс сущности БД DBDeal
и класс транслятора DealTranslator
.
class DealParser: JSONParser<Deal> {
override func parseObject(_ data: JSON) -> Deal? {
guard
let title: String = data["title"]?.string,
let endDateString: String = data["end_date"]?.string
else { return nil }
let subtitle: String? = data["subtitle"]?.string
let object = Deal(
title: title,
subtitle: subtitle,
endDateString: endDateString
)
return object
}
}
class DBDeal: RLMEntry {
@objc dynamic var title = ""
@objc dynamic var subtitle: String? = nil
@objc dynamic var endDateString = ""
}
class DealTranslator: RealmTranslator<Deal, DBDeal> {
override func fill(_ entity: Deal, fromEntry: DBDeal) {
entity.entityId = fromEntry.entryId
entity.title = fromEntry.title
entity.subtitle = fromEntry.subtitle
entity.endDateString = fromEntry.endDateString
}
override func fill(_ entry: DBDeal, fromEntity: Deal) {
if entry.entryId != fromEntity.entityId {
entry.entryId = fromEntity.entityId
}
entry.title = fromEntity.title
entry.subtitle = fromEntity.subtitle
entry.endDateString = fromEntity.endDateString
}
}
С недавних пор мы научились генерировать еще и service на основе документированного protocol (про это стоит написать отдельную статью). До момента, когда начали использовать zeplin, генерировали стили цветов и шрифтов на основе текстового файла с их описанием.
Для написания утилит для генерации мы используем свою библиотеку Model Compiler [9], но для этой задачи вполне может подойти и Sourcery [10].
Развивая архитектуру мы, прежде всего, задумывались над возможностью расширяемости наших проектов, явным разделением обязанностей и низким порогом входа для новых разработчиков. Безусловно, мы так же сталкиваемся со сложными сценариями, где какие-то из элементов нашей архитектуры «проседают», и мы придумываем, как выйти из этой ситуации, как разнести ответственности на вспомогательные сущности и сделать код более понятным. Очевидно, что ни одна архитектура не решает абсолютно все проблемы. На нескольких проектах, которые мы разрабатываем уже не первый год, наши подходы оказались удобными, и редко с этим возникают какие-то проблемы.
Мы не проповедуем MVC, MVVM, VIPER, Riblets и другие архитектуры. Мы постоянно пробуем что-то новое не в ущерб эффективности. При этом стараемся не изобретать велосипедов. Потом проверяем, насколько удобно работать с тем или иным подходом, насколько новые разработчики быстро могут схватить эти изменения.
Автор: vani2
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios-development/271229
Ссылки в тексте:
[1] “божественный объект”: https://ru.wikipedia.org/wiki/%D0%91%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82
[2] нашей прошлой статье: https://habrahabr.ru/company/redmadrobot/blog/246551/
[3] parser: https://github.com/RedMadRobot/core-parser
[4] DAO: http://www.oracle.com/technetwork/java/dataaccessobject-138824.html
[5] тут: https://github.com/RedMadRobot/DAO
[6] разобрана: https://www.objc.io/issues/13-architecture/mvvm/
[7] parser'ы: https://github.com/RedMadRobot/core-parser-generator
[8] translator'ы: https://github.com/RedMadRobot/DAO-generator
[9] Model Compiler: https://github.com/RedMadRobot/model-compiler
[10] Sourcery: https://github.com/krzysztofzablocki/Sourcery
[11] Источник: https://habrahabr.ru/post/340600/?utm_campaign=340600
Нажмите здесь для печати.