Pull to refresh

Получил 1.2K звезд на GitHub с ужасной архитектурой. Как?

Development for iOS *Objective C *Xcode *Swift *
Recovery mode
Хочу поделится довольно обычной, но показательной историей. Идея проекта появилась 3 месяца назад, за 1 месяц была реализована и вот уже два месяца как проект переодически висит в топе GitHub, попал в какие только можно профильные новостные ресурсы, и даже забрался в дайджест в статье “Топ 5 библиотек апреля”.

Вы могли подумать что я хвалюсь, но нет. Это предыстория нужна для более глубоко диссонанса. Я хочу поговорить об… архитектуре. Да, знаю-знаю, “сколько можно” и “что он себе позволяет”. Но я буду говорить не столько о паттернах, сколько о подходе к их использованию. Именно такие статьи я искал и люблю. Примеров синглтона и фабрики вы найдете больше, чем ошибок при выходе новой версии свифта, а мы поговорим об обобщенном подходе на примере моей библиотеки.

Перед погружением — прочтите инструкцию


Не сказал бы что я iOs-разработчик какого-то запредельного уровня, поэтому прошу отнестись к всему сказанному критично. Уверен есть люди опытнее меня, для них все очевидно. А вот для заблудшей души, вчера сортировавшей массивы, хорошо бы быть объективным. Я буду стараться.

Задраить люки! Погружаемся!


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

Основные требования к проекту:

  • Простое внедрение
  • Простое использование
  • Удобная кастомизация и расширение (если вдруг захочется добавить новых разрешений или визуалок)

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

А теперь давайте рассуждать. Это вообще штука полезная. Чтобы проект получился простым — нужно иметь простой интерфейс и не грузить программиста реализацией под капотом. В этом я вдохновлялся подходом от Appodeal. В общем, нужно иметь одну точку входа. Т.е. сконфигурировали объект 2-мя разрешениями, а далее запросили их. Должно быть так же просто, как это звучит!

Сразу дьявол на левом? плече шепчет: “Singleton…”. И первые дни мне казалось это прекрасным решением, в AppDelegate сконфигурировал, а показывай контролер где захочется.

Но проблем оказалось больше:


  • Крайне редко необходимо будет держать в памяти диалоговое окно
  • После получения всех разрешений точно не нужно держать его в памяти
  • Мало вероятно что будет нужно запрашивать одни и те же разрешения на разных экранах

В общем, паттерн имел больше минусов, чем плюсов. Тут я хочу обратить внимание на то что отталкивался именно от вариантов использования своего проекта. Уверен что и Singleton оправдан в использовании, но в других ситуациях. 


В переломный момент мою точку зрения подтвердил известный в определенных кругах iOS уже Android разработчик — Алексей Скутаренко, назвав паттерн “сомнительным”. То ли от нелюбви к этому паттерну, то ли от не лучший применимости его к моим потребностям — неизвестно. Но решение было принято — выбросить листов 20 макулатуры, достать новых. Маркер, собственно, тоже кончился.

Тогда было принято решение пойти от обратного. Как я бы хотел, чтобы использовали проект? Я это четко представлял:

class ViewController: UIViewController {

    var permissionAssistant = SPRequestPermissionAssistant.modules.dialog.interactive.create(with: [.Camera, .PhotoLibrary, .Notification])

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(true)
        self.permissionAssistant.present(on: self)
   }
}

Решение напросилось само собой: должен быть главный класс, его мы и назовем PermissionAssistant. А логику разделим на ключевые блоки, для удобства объединим их словом Manager. А что, логично, разные задачи — соответственно разные классы будут за них ответственные. 



Теперь давайте определимся с тем, какие функциональные части будут. Очевидно, одна из будет отвечать за запрос разрешений и получение информации о них. Назовем ее PermissionsManager. Так как подразумевается еще и визуальная часть, добавим PresenterManager (своруем именование у Viper, да будут в достатке его открыватели).


Презентер будет отвечать за презентацию контроллера, его конфигурирование… вообще за UI (если оно, конечно, будет). Кстати, обращаю внимание, что все части скрываем протоколами для большей гибкости в дальнейшем.

Гибкости?!

Да. Может не лучшее слово, но отражает суть. Представим что мы ремонтируем подводную лодку, крепление винта — 16-листовая резьба с 29 дюймами (только что выдумал). Не нужно каждый раз делать новую подводную лодку, достаточно будет сделать винт с известными требованиями и прикрутить его. Сделать и прикрутить.

Не мастер я алегорий, давайте к примеру с кодом. Разберём PermissionsManager, его сокрытие протоколом и реализацию. Для начала определим функционал менеджера. Нам хватит двух методов:

  • Запрос разрешения
  • Одобрено ли разрешение

По вкусу можно функционал расширить, но нам хватит. И так, протокол:

protocol PermissionManagerInterface {

   func requestPermission(_ type: PermissionType)

   func isAllowedPermission(_ type: PermissionType)
}

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

Теперь имплементируем протокол. Получим реальный объект (винт). Его и прикручиваем. А вот наш главный класс Assistant (подводная лодка) не будет знать какая конкретно реализация (из какого метала, сколько дней ее лили и сколько работа стоила). Главный класс знает только что есть две функции. Захотели сменить реализацию — пожалуйста) Особенно полезно это будет в кастомизации визуальной части и DataSource. Вот о нем сейчас и поговорим.

Очевидно что визуальная часть куда сложнее, чем просто Presenter. По хорошему ее нужно делить на модули. Собственно разделим на две части: Controller и DataSource

Presenter держит контроллер, он будет разбираться с жестами, экраном и прочим, чем занимаются контроллеры. Конечно, возникает вопрос как контроллер будет сообщать о действиях. У особо пытливых возникнет вопрос «а ARC часом не уничтожит все к чертям?».


Если с первым вопросом все понятно — делегаты, то второй — проблема, которая остановила мою работу на неделю. Но чтобы вы понимали, я дополнил схему ссылками между объектами (надеемся все знают про сильные и слабые ссылки, вопросов возникнуть не должно).

Проблема очевидна (надеюсь) — контроллер держит исключительно объект Assistant (конечно, Presenter, но он держит Assistant, так что опустим звено). Если проблема еще не понятна, разъясняю: 



Представим что объект Assistant вышел из своей области видимости, и соответственно, был выкинут за борт ARC. Если не был презентован контроллер, то умирает весь объект целиком. Это корректное поведение. Но если контролер был презентован…

то он теперь весит в стеке — и соответственно на него имеется ссылка вне объекта. А вот Assistant, так как выйдет из зоны видимости — умрет. Продемонстрировать можно на простом примере



if true {
	let permissionAssistant = SPRequestPermissionAssistant.modules.dialog.interactive.create(with: [.Camera, .PhotoLibrary, .Notification])
	
permissionAssistant.present(on: self)
}



Получаем ситуацию, когда контроллер будет жив, а вот все классы окружения — умрут. И даже Presenter, который и держал контроллер.

Грустно


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

Сколько разговоров было о проблеме, на уши поднял даже своих матросов сотрудников. Все в один голос твердили — “Что за ужасная архитектура?!”, “Автору — яду” и “Контроллер должен все держать”. 

Да, контроллер я выносил в центр. Но проблема в том, что если контроллер держит Assistant и небыл презентован сразу при инициализации — умирает весь объект. В общем, перевернуть связи не получилось, а это означало что…

контроллер выносить как главный объект! Писать логику внутри контроллера — ну уж нет. 



Решение пришло само собой за чашечкой чая и было чем то вроде прозрения:

— “А почему нет?” 



Просто инициализировать Assistant как проперти контроллера — и все! Пока жив родитель, жив и Assistant. А так как все диалоговые контролеры подразумевались модальными, решение отлично влилось. Такое решение мне показалось оптимальным, хоть и педантичность внутри взгрустнула. Что ж, продолжим. Дух был поднят, снова набираем скорость!



Теперь хорошо бы разделить UI и PermissionManager. Тут все тривиально — делаем протокол PermissionInterface, который выглядит так:


protocol PermissionInterface {
    
    func isAuthorized() -> Bool
    
    func request(withComlectionHandler complectionHandler: @escaping ()->()?)
}

И для каждого нового пермишина (Location, Notification, Camera…) реализовываем его. А в PermissionManager создаем необходимый класс Permission и дергаем нужные функции. 



Обновим схему:


Теперь мы видим всю картину. И как видно, любой кирпич мы можем заменить. Что лично я считаю — прекрасно. Чем ниже по лестнице блок — тем меньше придется переписывать. Для того, чтобы реализовать новый контроллер, нужно реализовать его интерфейс и внедрить в текущую систему (каким способом — ваше дело). Хотите поменять текста и цвета? Реализуйте протокол DataSource. Особенно мне нравится идея наличия нескольких PresenterManager. Сейчас вам хочется диалоговое окно, а на другом экране — всплывающий банер сверху (уже в разработке)


Время попсы


Прекрасно понимаю что кол-во звезд слабо коррелирует с качеством кода, надеюсь очевидно что текст совсем не про это.

Пока проект был в работе, я получил так много советов, что потратил больше времени на аргументацию (для себя) почему тот или иной паттерн / идея не подойдет. И это хорошо, я проделал много работы.

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

 когда я начинал только думать над проектом, мой хороший товарищ Геннадий (имя изменим) работал над одноэкранным приложением. Делал его на VIPER, и особо не вникал почему и зачем его использует. На мои аргументы:

— “Отпусти проблему, зачем тебе тяжелый паттерн на супер-простом приложении

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

Паттерны не таблетка от всех болезней. Не используйте его как показатель профессионализма или потому что "так делают гуру". Не отсохнут руки, если из MVC сделаете “не эпловский MVC”. Используйте паттерны, когда понимаете что это необходимо. 

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

Я не призываю отказаться от паттернов, писать все в контроллере и вообще от VIPER отстреливаться магнитной пушкой. Используйте, но обдумано! Ставьте конкретные требования к архитектуре и экспериментируйте — узнавайте что лучше решит поставленные задачи. Если идеально подходит VIPER, но вот PRESENTER вам кажется лишним — выбросьте его, почему нет? Именно эмпирическая работа с архитектурой даст лучший, пускай и не классический результат.

Я не знаю как называется мой паттерн (компот?) — но я остался доволен тем, как он решает поставленные перед ним задачи. Именно такой результат должно приносить использование паттерна.

— "Даже профессионалы используют сториборды" — неизвестный автор.



Всем успешных билдов и православного CTR!

UPD через год:
Сейчас проект собрал более 2К звезд и принято решение его обновить.



Предлагаю посмотреть ролик о поиске нового дизайна и небольшой туториал как использовать библиотеку в своем проекте:



UPD через 2 года:
На проекте теперь 4к+ звёзд, переписал на SPM. Завёл канал в телеграмме, подписывайтесь.
Tags:
Hubs:
Total votes 44: ↑33 and ↓11 +22
Views 78K
Comments Comments 41