- PVSM.RU - https://www.pvsm.ru -

Всем привет! Меня зовут Илья, я — iOS разработчик в Tinkoff.ru. В этой статье я хочу рассказать о том, как уменьшить дублирование кода в presentation слое при помощи протоколов.
По мере роста проекта, растет количество дублирования кода. Это становится заметно не сразу, и исправлять ошибки прошлого становится сложно. Мы заметили эту проблему у себя на проекте и решили ее с помощью одного подхода, назовем его, условно, traits.
Подход можно использовать с различными разными архитектурными решениями, но рассматривать его я буду на примере VIPER.
Рассмотрим самый распространенный метод в router — метод, закрывающий экран:
func close() {
self.transitionHandler.dismiss(animated: true, completion: nil)
}
Он присутствует во многих router, и лучше написать его только один раз.
Нам в этом помогло бы наследование, но в будущем, когда у нас в приложении будет появляться все больше классов с ненужными методами, или мы вовсе не сможем создать нужный нам класс из-за того, что нужные методы находятся в разных базовых классах, появятся большие проблемы.
В итоге, проект обрастет множеством базовых классов и классов-наследников с лишними методами. Наследование нам не поможет.
Что лучше наследования? Конечно же композиция.
Можно сделать для метода, закрывающего экран, отдельный класс, и добавлять его в каждый роутер, в котором он нужен:
struct CloseRouter {
let transitionHandler: UIViewController
func close() {
self.transitionHandler.dismiss(animated: true, completion: nil)
}
}
Нам все равно придется объявить этот метод в Input протоколе роутера и реализовать его в самом роутере:
protocol SomeRouterInput {
func close()
}
class SomeRouter: SomeRouterInput {
var transitionHandler: UIViewController!
lazy var closeRouter = { CloseRouter(transitionHandler: self. transitionHandler) }()
func close() {
self.closeRouter.close()
}
}
Получилось слишком много кода, который просто проксирует вызов метода close. Ленивый Хороший программист не оценит.
На помощь приходят протоколы. Это достаточно мощный инструмент, который позволяет реализовать композицию и может содержать реализации методов в extension. Так мы можем создать протокол, содержащий метод close, и реализовать его в extension.
Вот так это будет выглядеть:
protocol CloseRouterTrait {
var transitionHandler: UIViewController! { get }
func close()
}
extension CloseRouterTrait {
func close() {
self.transitionHandler.dismiss(animated: true, completion: nil)
}
}
Возникает вопрос, почему в названии протокола фигурирует слово trait? Это просто — так можно указать, что этот протокол реализует свои методы в extension и должен использоваться как примесь к другому типу для расширения его функциональности.
Теперь, посмотрим как будет выглядеть использование такого протокола:
class SomeRouter: CloseRouterTrait {
var transitionHandler: UIViewController!
}
Да, это все. Выглядит отлично :). Мы получили композицию, добавив протокол к классу роутера, не написали ни одной лишней строчки и получили возможность переиспользовать код.
Возможно, вы уже задались этим вопросом. Использование протоколов в качестве trait — вполне обыкновенное явление. Основное отличие в том, чтобы использовать этот подход как архитектурное решение в рамках presentation слоя. Как у любого архитектурного решения, тут должны быть свои правила и рекомендации.
Вот мой список:
Если полностью перейти на использование данного подхода с протоколами, то классы router и interactor будут выглядеть примерно так:
class SomeRouter: CloseRouterTrait, OtherRouterTrait {
var transitionHandler: UIViewController!
}
class SomeInteractor: SomeInteractorTrait {
var someService: SomeServiceInput!
}
Это относится не ко всем классам, в большинстве случаев в проекте останутся просто пустые routers и interactors. В таком случае, можно нарушить структуру VIPER модуля и плавно перейти к MVP при помощи добавления протоколов-примесей к presenter.
Примерно так:
class SomePresenter:
CloseRouterTrait, OtherRouterTrait,
SomeInteractorTrait, OtherInteractorTrait {
var transitionHandler: UIViewController!
var someService: SomeSericeInput!
}
Да, потеряна возможность внедрять router и interactor как зависимости, но в некоторых случаях это имеет место быть.
Единственный недостаток — transitionHandler = UIViewController. А по правилам VIPER Presenter ничего не должен знать о слое View и о том, с помощью каких технологий он реализован. Решается это в данном случае просто — методы переходов из UIViewController «закрываются» протоколом, например — TransitionHandler. Так Presenter будет взаимодействовать с абстракцией.
Посмотрим, как можно изменять поведение в таких протоколах. Это будет аналог подмены некоторых частей модуля, например, для тестов или временной заглушки.
В качестве примера возьмем простой интерактор с методом, который выполняет сетевой запрос:
protocol SomeInteractorTrait {
var someService: SomeServiceInput! { get }
func performRequest(completion: @escaping (Response) -> Void)
}
extension SomeInteractorTrait {
func performRequest(completion: @escaping (Response) -> Void) {
someService.performRequest(completion)
}
}
Это абстрактный код, для примера. Допустим, что нам не надо посылать запрос, а нужно просто вернуть какую-нибудь заглушку. Тут идем на хитрость — создадим пустой протокол под названием Mock и сделаем следующее:
protocol Mock {}
extension SomeInteractorTrait where Self: Mock {
func performRequest(completion: @escaping (Response) -> Void) {
completion(MockResponse())
}
}
Здесь реализация метода performRequest изменена для типов, которые реализуют протокол Mock. Теперь нужно реализовать протокол Mock у того класса, который будет реализовывать SomeInteractor:
class SomePresenter: SomeInteractorTrait, Mock {
// Implementation
}
Для класса SomePresenter будет вызвана реализация метода performRequest, находящаяся в extension, где Self удовлетворяет протоколу Mock. Стоит убрать протокол Mock и реализация метода performRequest будет взята из обычного extension к SomeInteractor.
Если использовать это только для тестов — лучше располагать весь код, связанный с подменой реализации, в тестовом таргете.
В заключении стоит отметить плюсы и минусы данного подхода и то, в каких случаях, по моему мнению, его стоит использовать.
Начнем с минусов:
Положительные стороны данного подхода следующие:
Считать это решение хорошим или нет — личное дело каждого. Наш опыт применения этого подхода был положительным и позволил решить проблемы.
На этом все!
Автор: NoFearJoe
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/297784
Ссылки в тексте:
[1] Источник: https://habr.com/post/413921/?utm_campaign=413921
Нажмите здесь для печати.