Сдерживая пороки императивности

    Объектно-ориентированная парадигма крайне удобна для бизнеса: она позволяет реализовывать практически любые идеи, обеспечивая приемлемую производительность продукта. В этом случае под продуктом мы понимаем iOS приложение, поэтому в умозаключениях будем отталкиваться от разработки конкретно на этой платформе.

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

    Принимая во внимание закон Мерфи, напрашивается вывод что, при отсутствии правильных архитектурных ограничений, все с большей вероятностью будет идти по пути хаоса, то есть качество кода будет стремительно уменьшаться, только потому что парадигма это позволяет. В данной статье пойдет речь об одном из возможных архитектурных ограничений, которое поможет сдержать баланс сил добра и зла, иными словами, динамику роста энтропии в кодовой базе проекта. Важно то, что эта динамика прямо коррелирует с количеством людей, задействованных в написании кода, поэтому для больших проектов особенно критична правильность подобранных ограничений. В чем суть?

    Суть проста. Оттолкнемся от следующей аксиомы — чем больше в объекте свойств, тем он «хуже». Такое утверждение можно объяснить следующим образом: с возрастанием количества внутреннего стейта, все положительные показатели объекта — расширяемость, модульность, прозрачность, тестируемость — снижаются. Можно возразить, сказав, что сложность объекта и его функционал понятия взаимосвязанные и что без первого второго не бывает. Все верно, но, следуя по «stateful» пути, рост сложности происходит в геометрической прогрессии, при том, что в идеале рост должен быть линейным, или, говоря проще, новая фича не должна усложнять существующие фичи.

    P.S. Здесь стоит уточнить, что под фичами понимаются семантически связанные слои бизнес-логики, поэтому часто принимается решение именно усложнить существующий класс, а не создать новый. Достаточно сложно найти в таких случаях противоречия первому принципу SOLID.

    В качестве примера приведен вполне стандартный экран со списком сущностей с возможностью выбора. Часть свойств можно было бы сделать немутабельными, но IB привязывает нас к дефолтному конструктору контроллера и этим вынуждает «делать грязь». Доступ к данным свойствам неявно получает каждый метод класса в пределах одного файла. А самый неприятный момент в том, что их мутабельность также ничем не прикрыта, и это так или иначе ведет к необратимым последствиям в виде сильной связанности и, следственно, к тому, что фикс одних дефектов вызывает собой появление новых:



    Можно сделать вывод, что линейный рост сложности объекта с общим стейтом практически недостижим. Функционал при таком подходе становится монолитным и сложно поддающимся какой-либо сегрегации. Хорошим примером является антипаттерн Massive-View-Controller, достаточно распространенный и наглядно демонстрирующий результат отсутствия каких-либо ограничений на проекте.



    На примере UIKit можно увидеть, как именно императивный стиль разработки влияет на сложность кода и в каких моментах фреймворк «принуждает» заводить в классах свойства.
    Самый простой кейс -обработка нажатия на кнопку — стандартно выполняется только определением «грязного метода», то есть функцией, которая на вход ничего полезного принимать не сможет, поэтому за данными придется обращаться наружу, к свойствам:



    Таким образом, «фича» кнопки тиранично усложнит весь остальной функционал объекта, так как к этим данным доступ получат все жители класса. На самом деле, почти любой UI-элемент управления в iOS работает подобным образом. Поэтому на ум приходит реализация некой обертки над UI элементами, например, принимающей замыкание, в качестве обработки «срабатывания» конкретного элемента, но сложность в том, чтобы придумать как такую обертку сделать лаконичной и надежной. Ведь дело не только в вводе, но и в выводе информации, например, вездесущая таблица с данными тоже работает «грязно» и понятия не имеет о данных которые показывает, поэтому приходится сохранять их в объект:



    Удобно? Удобно, у всех всегда есть доступ к данным. Гибко, быстро, императивно. Вот только превращается это все со временем, как правило, в бетонный обелиск на тысячу строк кода и в слезы тех, кому досталась задача на работу в этом классе.

    Возвращаясь к ограничениям, из вышеописанного можно сформировать следующий принцип — код без свойств в классах существенно чище, и более выгоден при поддержке и расширении. В идеале логика объекта должна заключаться в его «чистых» методах, которые на вход принимают все свои зависимости, а на выходе имеют результат своей деятельности.
    Идея в том, чтобы спустить приватный стейт объекта еще на уровень ниже. То есть, если private закрывает свойства от внешнего мира, то наша задача состоит в том, чтобы пойти еще дальше и усилить инкапсуляцию до уровня самих методов. На первый взгляд, при всей асинхронной природе платформы и императивности главного фреймворка, может показаться что вся эта затея не то чтобы невозможна, но как минимум ее реализация будет ощущаться неестественно. И, скорее всего так и будет, но это одна из тех проблем, которую можно решить введением дополнительного уровня абстракций.

    Если мы хотим уместить всю логику классов в их методах, не прибегая к свойствам, потребуется плотная работа с замыканиями. Для управления асинхронными операциями в iOS есть стандартные средства, такие как GCD и OperationQueue. Кажется, их бы хватило, но при попытке воплотить идею в жизнь, все окажется далеко не таким радужным как хотелось. При таком подходе, не считая того, что велик шанс получить callback-hell, сам код получится громоздким, в нем будет много логических дыр и он будет сильно связанным. Вполне возможно, что такой код будет даже сложнее чем то, от чего мы так стремительно стараемся убежать.

    Если осмотреться вокруг, можно увидеть что есть гораздо более красивые и функциональные пути достижения поставленной цели. Реактивное программирование достаточно давно применяется в коммерческой разработке ПО и идеально подходит для асинхронного мира фронтенда. В данном случае будет рассмотрена одна (достаточно удачная) реализация реактивной парадигмы для Swift — Rx.



    Он предлагает простую сущность называемую Observable. Это некая абстракция над потоком событий, на нее может быть произведена подписка, а после этого подписчик с течением времени будет получать эти события:



    Легче всего вообразить поток событий, представив обычную кнопку. Событие ее срабатывания и является здесь потоком, поэтому любой объект может подписаться и получать события ее нажатия, а самое главное, что кнопка ничего не знает о собственных подписчиках. Удобно то, что почти любое действие можно превратить в подобную последовательность значений и это важно, потому что Observable можно сочетать друг с другом, так, как никакой стандартный фреймворк возможности сделать не даст.

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



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

    Кроме компактности, в коде выше сложно допустить ошибку, так как каждое замыкание что-то возвращает и не содержит сайд-эффектов. Это та мощь, которую мы и искали.

    Можно заметить еще один важный момент: так как у класса нет свойств, то и не приходится писать [weak self], что положительно сказывается на читаемости кода. Все функции можно и лучше определять локально в методе где они используются, либо выносить в отдельные классы. Кстати, ссылки на зависимости (ViewModel, Presenter и т.п.) в данном случае могут передаваться в качестве аргумента к методу контроллера, в этом случае нет необходимости сохранять их в свойства. Это правильно.

    Ознакомившись, настало время применить Observable в светлых целях упрощения разработки. Как именно? Вернемся к идее о «чистых» методах и попробуем реализовать логику небольшого экрана в единственном его методе, для ясности выберем метод окончания загрузки представления (viewDidLoad). Естественно, что если экран сверстан в IB, то нам придется создавать свойства для аутлетов, но это не страшно, потому что сами элементы не представляют собой бизнес-логику, поэтому на сложность экрана они сильно не повлияют. Но если же экран сверстан из кода, то можно обойтись вовсе без свойств (кроме disposeBag), создавая элементы в нашем методе и тут же их используя. Как же быть с императивностью UIKit элементов о котором рассказывалось ранее? Rx, кроме самого подхода, предоставляет реактивные обертки для стандартных UI компонентов, поэтому в большинстве случаев можно на месте получить необходимую последовательность событий. Либо, наоборот, привязать существующий Observable к, например, таблице — подвести к ней запрос, чтобы она обновляла свой контент сразу после его завершения:



    Привязка к коллекциям достаточно гибкая, но по умолчанию, она работает только через reloadData. Для точечных обновлений есть замечательная отлаженная библиотека от того же автора — RxDataSources. С ней можно забыть про краши во время выполнения batchUpdates.

    Что произойдет дальше? Единый метод экрана будет расти и в достаточно сложном случае его станет трудно поддерживать. И когда это произойдет, вдруг станет ясно, что этот метод ни от чего кроме себя не зависит, а реактивный подход разделил код на логические блоки, которые без проблем можно вынести в отдельные объекты, проектируя их похожим образом. Но в этот раз методы будут уже принимать некоторые зависимости от экрана и возвращать некий результат назад. Положительный момент в том, что сигнатура в таком случае получается максимально содержательной, видно что функция требует для своей работы и в чем заключается ее результат. Выглядеть это может примерно так:



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

    Важно понимать, что метод необязательно должен быть единственным во всем объекте, суть в их независимости друг от друга. Благодаря Rx, их ввод и вывод могут быть асинхронными и представлять из себя один или несколько Observable, предоставляя еще одно измерение для манипулирования данными.

    Такой подход развязывает руки и позволяет реализовывать экраны практически любой сложности, сохраняя при этом их код явным и слабо связанным.
    SimbirSoft
    92,53
    Лидер в разработке современных ИТ-решений на заказ
    Поделиться публикацией

    Комментарии 4

      0
      Есть ли у вас пример проекта написанного подобным способом?
      Мы используем RxSwift во всём проекте, кроме работы с UI. Пока не можем для себя решить стоит ли его туда пускать.
        0
        Все зависит от степени вовлеченности разработчиков в проект и в Rx в частности. Если большая текучка, то стоит подумать о времени, которое новые люди потратят на преодоление порога вхождения, который у библиотеки достаточно высокий, особенно при работе с UI.
        А если с кадрами проблем нет, то полный вперед)
        0
        Я хорошо отношусь к Reactive, но ваш пример с использованием сохранения выделенных ячеек прямо во ViewControllere из UITableView.didSelectRow здесь не подходит. Так, как вы показываете, хранить данные в контроллере нельзя. Данное состояние нужно записывать в dataSource и брать его оттуда же. И ваш MVC не станет Massive.
          0
          Massive view controller упоминался в качестве примера того, что бывает если написание кода на проекте вообще ничем не ограничивать.
          Цель примера с UITableView иная — показать императивность UIKit'а, он упрощен для наглядности. Сырые данные или dataSource, в любом случае, придется заводить свойство, доступ к которому получают все единицы класса в пределах исходника.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое