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

Хочу поделится довольно обычной, но показательной историей. Идея проекта появилась 3 месяца назад, за 1 месяц была реализована и вот уже два месяца как проект переодически висит в топе GitHub, попал в какие только можно профильные новостные ресурсы, и даже забрался в дайджест в статье «Топ 5 библиотек апреля».

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

2701d00370d74987ae03348cc59d24b0.gif

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


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

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


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

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

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

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

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

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


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

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


В переломный момент мою точку зрения подтвердил известный в определенных кругах 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. А что, логично, разные задачи — соответственно разные классы будут за них ответственные. 


817c8315245241c8958429cc2a370bd4.png

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

93439c2ab2ca4a928d251a1254563e2c.png

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

Гибкости?!

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

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

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

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

protocol PermissionManagerInterface {

   func requestPermission(_ type: PermissionType)

   func isAllowedPermission(_ type: PermissionType)
}

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

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

7db3bc9830dd462e9ac3122aa233f4ef.png

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

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

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



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

756613c0e59f4388a06d1942ee60126f.png

то он теперь весит в стеке — и соответственно на него имеется ссылка вне объекта. А вот 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 и дергаем нужные функции. 


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


420dd8ac44a8491eaca3910f5cae4cee.png

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


Время попсы


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

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


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

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

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



Я призываю использовать паттерны не как таблетку от всех болезней, не как показатель профессионализма или »только так делают профи». Не отсохнут руки, если из MVC сделаете «не эпловский MVC». Используйте паттерны, когда понимаете что это нужно. 

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

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



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

Комментарии (8)

  • 16 апреля 2017 в 20:24 (комментарий был изменён)

    +2

    Используйте паттерны, когда понимаете что это нужно.

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


    Скажем если мы хотим чтобы код легко находился в проекте, повышаем coheasion. Что бы было проще определить насколько модуль кохизив, смотрим на него с точки зрения SRP. Хотим увеличить гибкость и избежать лавинообразных изменений между модулями — нужно снижать coupling, применять dependency inversion и т.д.


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


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

    • 16 апреля 2017 в 20:26

      +1

      Вам нужно было писать эту статью) Но насколько я понял — вы со мной солидарны
      • 16 апреля 2017 в 20:33 (комментарий был изменён)

        +1

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


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


        • Паттерны офигенны! Как я раньше без них жил! Куда бы еще запихнуть…
        • От этих паттернов только одни проблемы… Как мне теперь код мэйнтейнить…

        Как по мне тратить на подобное целый год слишком долго. И грустно от того что я сам проходил через подобные этапы. Грустно от того что про концепции вроде связанности или принципы/паттерны GRASP я узнал через года 3 после того как начал путь в коммерческой разработке. А многие и через 5 лет не знают об этом. Люди через какое-то время просто находят свою зону комфорта. Кто-то просто не парится и не повышает сложность проекта. Кто-то раз в год меняет работу чтобы сбежать от ужаса поддержки своих решений…


        Что до проектов с кривой архитектурой и кучей звезд — увы и ах… так везде. Многие хорошие разработчики которые выкладывают неплохие решения просто не парятся о продвижении своих библиотек.

        • 16 апреля 2017 в 20:37

          +1

          Свою не продвигал, считаю что визуалка вытащила. Хотя для громкого заглавия использовал «плохую архитектуру». Проблемы она решает, я ей доволен — считаю что хороший выбор сделал.

          История скорее о том, «сидя в зонах комфорта» юзают один паттерн под все случаи жизни.

          Грустно когда смотрю код подчиненых, и вижу VIPER где только можно и нельзя. Тренды…

          • 16 апреля 2017 в 21:00

            +1

            юзают один паттерн под все случаи жизни.

            тут скорее культ карго нежели зона комфорта. Ну мол кто-то рассказал что если будете применять viper то железные птицы вернутся все будет всегда хорошо. И скорее всего либо было лень разбираться что это такое либо авторитет шамана главного разработчика на проекте высок и ему лучше знать.


            вижу VIPER где только можно и нельзя.

            Я могу сказать так, из моего опыта общения с нашими мобильщиками которые практиковали viper на нескольких проектах, чаще проблема кроется еще и в неверной интерпретации этого паттерна. Поскольку паттерн этот основан на идеях Дяди Боба, нужно хорошо понимать такие принципы как SRP (который кажется обманчиво простым, но на деле его весьма сложно соблюдать).


            Так же у нас там есть ограничение в духе «сущности никогда не должны покидать интерактор и попадать в presenter» что есть просто соблюдение закона деметры и инкапсуляции. Опять же за последние 6 лет мне попадались только 2–3 проекта где на клиенте было достаточно логики что бы можно было выстраивать интеракторы. И да, те самые интеракторы подразумевается использовать только для операций записи, операции чтения можно делать проще. Более того, использование одних и тех же сущностей как для записи так и для чтения может приводить к нарушению SRP ради которого весь viper и задуман.


            Ну то есть как по мне опять же проблема не в viper, а в том что во всех описаниях этого паттерна которые мне удалось нагуглить вообще не говорится о том какие проблемы он решает. Как правило в качестве аргумента используют «юзают на больших проектах» и людям этого хватает.

            • 16 апреля 2017 в 21:04

              +1

              Именно! Проблем с Viper нет никаких, есть вопросы «зачем», на которые часто ответ «ну так делают»

              А то что поддержка сложным паттернов обходится в 2x-5x времени, или что есть другие, лучшие варианты (или вообще плюнуть и написать это одностраничное приложение все в контроллере) — боятся будто нечесть серебряных пуль)

  • 16 апреля 2017 в 20:48

    +1

    Для чего используется вот эта конструкция?
    let permissionAssistant = SPRequestPermissionAssistant.modules.dialog.interactive.create(with: [.Camera, .PhotoLibrary, .Notification]
    

    Никому не нужны подробности реализации, должно быть просто:

    let permissionAssistant = SPRequestPermissionAssistant.create(with: [.Camera, .PhotoLibrary, .Notification]
    

    То что вы выставляете наружу кишки — это феил.

    • 16 апреля 2017 в 20:56 (комментарий был изменён)

      +1

      Для чего используется вот эта конструкция?

      Это сборка модуля. Если не ошибаюсь, прием называется Dependency Injection и очень близко идет с протокольным подходом.
      Никому не нужны подробности реализации, должно быть просто:

      Никаких подробностей реализации. Это просто выбор одного из модулей. Хотите банер сверху? Будет:

      SPRequestPermissionAssistant.modules.banner

      Хотите диалоговое окно, но не интерактивное, а с блюром:

      SPRequestPermissionAssistant.modules.dialog.blur

© Habrahabr.ru