Недавно я ушел после трех лет работы в компании, использующей The Composable Architecture (TCA) от PointFree. Я хотел написать о своем опыте работы с TCA и о некоторых проблемах, которые я вижу в ней.
Я считаю Брэндона Уильямса и Стивена Селлиса, создателей TCA, абсолютно гениальными, и то, что им удалось сделать при создании TCA, просто потрясающе. Однако их только двое, и никто и ничто не может быть идеальным.
Начало
В феврале 2021 года я узнал, что одна местная компания использует TCA для написания своего программного обеспечения для iOS. Я большой поклонник функционального программирования и смотрел видео PointFree, поэтому меня очень заинтересовала TCA и возможность ее использовать. Я также являюсь поклонником языка программирования Elm, The Elm Architecture (TEA), и я рассматривал TCA как потенциальную замену этого языка для приложений на iOS. Этот вид программирования называется функциональным реактивным программированием, и другие фреймворки, о которых вы могли слышать — React и RxSwift.
Многие iOS-разработчики в Юте состоят в Slack-группе, где мы регулярно обсуждаем вопросы, связанные с индустрией. Я связался с одним знакомым, который работал в компании, и вскоре мне назначили собеседование. Я проходил собеседование с тремя ведущими разработчиками. Видимо, я хорошо справился, потому что в начале марта 2021 года я начал работать в компании. Я работал в команде, которая отвечала за одно приложение для iOS из трех, представленных компанией в App Store. Вскоре я столкнулся с первой проблемой, связанной с TCA.
Это сложно
TCA имеет крутую кривую обучения. Вам нужно знать о редукторах (reducer), хранилищах (store), области действия состояния (state) и действий (action). В начале вам также нужно было знать о хранилищах представлений и окружения (теперь они не нужны, см. ниже). Вы не можете просто использовать SwiftUI так, как вас научили в Apple. Каждое представление нуждается в хранилище, а если вы заботитесь о производительности, то и в хранилище представлений. Вам нужно научиться использовать первоклассные возможности TCA по тестированию и использовать хранилище тестов. Вы не можете просто использовать XCTest сам по себе.
В марте 2021 года TCA был в версии 0.16.0. С тех пор все значительно улучшилось с выходом последней версии (1.9.x), но на самом деле главные улучшения — это эргономика использования фреймворка и его производительность. Хранилища представлений больше не нужны благодаря новому фреймворку Observation и макросу @Observable
, представленному Apple в iOS 17, но вам необходимо знать о макросе TCA @ObservableState
. Если вы работаете на iOS 16 или более ранних версиях, вам необходимо знать о withPerceptionTracking
из TCA, макросе @Perceptible
и фреймворке Perception. Окружения (Environments) были заменены фреймворком swift-dependencies и макросом @Dependency
, и вам нужно знать, как их использовать. Макросы улучшили пути кейсов и то, как ссылаться на scoped действия.
Даже с учетом этих улучшений вам все равно придется многому научиться, чтобы использовать TCA. Кроме того, вам придется постоянно переучиваться, поскольку фреймворк обновляется и меняется. Все изменения — это улучшения, но на их изучение требуется время, особенно когда вы пытаетесь делать свою работу.
Перемены
TCA меняется часто и значительно. Больше, чем сам iOS SDK. Когда я говорю «значительно», я имею в виду, что при сохранении обратной совместимости новая функциональность имела значительные изменения, которые вы хотели использовать и которые улучшали код, но требовали значительных изменений в использовании.
За 3 года работы фреймворк прошел путь от версии 0.16.0 до 1.9.2. За эти 3 года было выпущено 95 релизов, большинство из которых были незначительными (по сравнению с другими), которые просто добавляли функциональность и сохраняли обратную совместимость. Итак, мы использовали фреймворк до версии 1.0, поэтому нельзя винить TCA или PointFree, но если посмотреть только на релизы между 1.0 (июль 2023 года) и 1.9.2 (март 2024 года), то менее чем за год вышло 24 релиза. Опять же, большинство из них — минорные релизы, но с существенными дополнениями.
Когда я уходил, мы все еще использовали 1.6, потому что, когда мы попытались перейти на 1.7 с новой поддержкой Observation, мы столкнулись с некоторыми проблемами, которые потребовали бы большого количества изменений и других сложных вопросов для решения, потому что мы все еще поддерживали iOS 15. Это означало, что мы не могли использовать фреймворк Observation от Apple. Вместо этого нам нужно было использовать фреймворк Perception от PointFree и обернуть многие представления в withPerceptionTracking
. Это само по себе непросто, поскольку вам нужно начать с корневого представления и работать вниз по дереву. В противном случае вы получите предупреждения и множество проблем во время выполнения. Использование макроса @ObservableState
создало еще одну проблему, поскольку макросы не очень хорошо работают с обертками свойств, которые мы использовали в нашем состоянии (см. раздел «Проблемы производительности»).
Подробнее о структуре нашей команды мы расскажем позже, но когда у вас 8 команд, каждая со своим кодом, который нужно обновлять одновременно с другими командами, переход на новую версию не всегда прост, особенно если команды находятся под давлением, связанным с предоставлением функций.
Архитектурные проблемы
Я уже говорил о том, насколько сложным является TCA. Вы можете быть гораздо продуктивнее, используя другую архитектуру, например MVVM (с дополнительными элементами), и получить те же преимущества, такие как однонаправленный поток данных, более простое модульное тестирование и модульный код. См. некоторые из моих других постов об этом, но я намерен углубиться в эту тему в будущих постах.
Другая проблема заключается в том, что TCA построен на функциональном программировании, а это противоречит объектно-ориентированным корням iOS SDK. Да, Swift и SwiftUI более функциональны, но они построены на базе Objective-C и Cocoa, объектно-ориентированного языка и фреймворка. Все разработано и оптимизировано с учетом этого. TCA функционален и не может воспользоваться этим преимуществом, что приводит к проблемам с производительностью и несоответствию импеданса с платформой.
Возьмем, к примеру, редукторы и действия. Для меня это просто объектно-ориентированная передача сообщений. TCA, по сути, реализует свою собственную передачу и обработку сообщений. При использовании TCA я просто хотел вызвать метод, что было бы эквивалентно отправке действия. В MVVM или другой архитектуре я мог бы сделать именно это.
Но, по сути, инкапсуляция отсутствует. Код может перехватить действие, отправленное из любой точки иерархии редукторов, а все состояние для иерархии редукторов должно существовать в корневом состоянии. Вы не можете скрыть часть состояния от других областей, равно как и действия.
Еще одна проблема — массивные редукторы. Так же как MVC может привести к массивным контроллерам представлений, я видел, как TCA приводит к массивным редукторам. Редуктор нашего приложения был настолько большим, что в Xcode возникали проблемы с его прокруткой, и он не выдавал корректных ошибок компилятора. В итоге я разделил его примерно на 20 отдельных файлов, содержащих расширения для редуктора приложения. Каждое расширение предназначалось для обработки связанной группы действий. Таким образом, app reducer выполнял по меньшей мере 20 различных обязанностей.
Редукторы в основном представляют собой просто оператор switch с регистром для каждого действия. Как правило, люди просто помещают туда все действия, не обращаясь к отдельным функциям редуктора. Составление редукторов может быть сложной задачей (см.”Это сложно”), поэтому обычно под давлением разработчик просто вставляет все в этот оператор switch.
Можно возразить, что это проблема навыков (или лени), и я склонен с вами согласиться. Мы также все знаем, насколько распространена такая проблема, как массивные контроллеры представлений.
Проблемы с производительностью
С нашим большим приложением мы столкнулись с некоторыми проблемами производительности.
Во-первых, TCA использует большой стек. Если вы попытаетесь отладить TCA-приложение, то удачи в поиске места, откуда вызывается ваш код. Стек очень большой, а вызовов функций фреймворка TCA для обработки действий очень много. С нашим большим приложением и большим состоянием приложения мы получали ошибки переполнения стека, и нам приходилось увеличивать пространство стека, выделенное для нашего кода. Мы также прибегли к созданию обертки свойств для размещения ссылок в состоянии приложения, которое обычно представляет собой просто struct с типами значений.
Далее, как уже говорилось выше, обработка действий может быть довольно медленной. Каждое действие должно пройти через все дерево редукторов. В большом приложении это может быть долгим. Аналогично тому, как медленной может быть отправка сообщений в Objective-C.
Кроме того, из-за отсутствия инкапсуляции (т. е. свободного доступа к состоянию и действиям) и композиции редукторов изменение кода в одной области часто требует перекомпиляции большого количества кода, что приводит к медленному времени сборки.
Организационные проблемы компании
Когда я начал работать в компании, я был в команде, которая полностью контролировала одно приложение. Это приложение было снято с производства, и когда я уходил, то был в команде платформы, которая поддерживала 7 других команд, работавших над одним приложением. Это еще больше усложнило ситуацию. Все 8 команд должны были использовать одну и ту же версию TCA, поэтому, когда мы хотели обновиться, обновляться приходилось всем, так как менеджер пакетов Swift не позволяет использовать несколько версий одного и того же фреймворка.
Поскольку в TCA нет инкапсуляции, и любой родительский редуктор может получить доступ к состоянию или действиям любого дочернего, возникают проблемы, когда команды лезут в другие хранилища и полагаются на код, который не следовало бы. Даже если ваши команды дисциплинированы, это может произойти, потому что состояние родителя содержит все состояния детей. Этот код может измениться, и тогда вызывающий код сломается. Такое случалось часто, и нам практически приходилось собирать все приложение во время PR-проверки, чтобы убедиться, что ничего не сломалось.
При таком количестве команд необходимо принимать какие-то меры для решения этих проблем. Одним из решений является создание отдельных хранилищ для каждой команды. Таким образом, код каждой команды будет изолирован и выполняться в своем собственном хранилище. Гениальный Кшиштоф Заблоцки писал об этом, и я рекомендую его сайт для других способов решения проблем масштабирования TCA, таких как разделение ваших действий на делегатные и внутренние действия. Если вы используете отдельные хранилища, вам нужно будет придумать, как взаимодействовать между ними. Один из подходов заключается в том, чтобы у каждой команды (или хранилища) был клиент TCA (т. е. зависимость), который могла бы использовать другая команда. Этот клиент, по сути, создаст чистую границу или интерфейс между двумя командами.
Риски для компании
Давайте поговорим о рисках, которые берет на себя компания, выбирая фреймворк, подобный TCA.
Во-первых, TCA — это сторонний фреймворк. Это означает, что Apple не поддерживает его и не заботится о нем. Что-то, что делает Apple, может сломать его (например, новый релиз iOS), и вам придется ждать обновления фреймворка. Фреймворк не может использовать все преимущества платформы, как это делает Apple, потому что он не может использовать внутренние компоненты, как это делает Apple. Это действительно проблема TCA из-за проблем с производительностью, о которых я говорил.
Далее, TCA — это в основном работа двух парней. Двух блестящих и плодовитых парней, но все же двух. Если им станет скучно, они получат травму, сменят профессию и т. д., у вас могут возникнуть проблемы. Это несколько смягчается тем, что TCA является открытым и, предположительно, компания и/или сообщество могут взять его на себя, но это все равно проблема.
И, наконец, такая крупная компания, как та, в которой я работал, не должна использовать незрелый фреймворк, каким был TCA. Я думаю, что сейчас, в версии 1.9.x, TCA уже довольно зрелый. Но не стоило начинать с версии 0.16.0. Вот что происходит, когда неопытные разработчики (большинство разработчиков программного обеспечения имеют опыт работы менее 5 лет, по словам дяди Боба) хотят использовать крутые технологии, а менеджеры, которые на самом деле должны знать лучше, слушают их. Некоторые из этих менеджеров даже имели технический опыт.
Я спросил, почему была выбрана TCA, и мне ответили, что они выбрали ее, потому что им нужна была архитектура, чтобы не дать всем разработчикам делать все, что они хотят. Главной проблемой для них были все подрядчики в Восточной Европе. По-моему, это не очень хорошая причина для выбора фреймворка. Есть и другие архитектуры, такие как MVVM (или MVVM+), которые проще в использовании и служат той же цели. Если вас это беспокоит, может быть, вам просто стоит нанять лучших разработчиков и обеспечить лучшее руководство? Справедливости ради стоит отметить, что разработчики, работавшие в Восточной Европе, были довольно хороши, и им не нужна была архитектура вроде TCA, чтобы держать их в узде.
Заключение
TCA — это невероятное достижение, но у нее есть некоторые проблемы при использовании в большом приложении, состоящем из нескольких команд. У вас может быть другая ситуация, и архитектура может сработать, особенно если у вас одна команда, относительно небольшое приложение или несколько очень дисциплинированных разработчиков. В случае большого приложения и нескольких команд вам придется столкнуться с отсутствием инкапсуляции и другими проблемами масштабирования.
TCA — это сторонний фреймворк, не поддерживаемый Apple, и он зависит всего от двух гениальных парней, на которых вы, по сути, ставите всё — всю свою кодовую базу. Это фреймворк, основанный на функциональном программировании, который противоречит объектно-ориентированному наследию и влиянию SwiftUI. Возможно, вы будете более продуктивны в работе с новыми разработчиками и добавлении функций с другой архитектурой и все же сможете достичь желаемой дисциплины с помощью MVVM или Чистой архитектуры.
В следующих постах я расскажу о том, как можно использовать более простую архитектуру для достижения тех же целей, что и с помощью TCA, и как установить чистые границы между различными областями вашего кода или между командами. Некоторые из моих предыдущих постов уже говорили об этом, но новые посты будут более специфичны для SwiftUI и будут посвящены чистым границам.
Я готов предоставить консультации, так что если вам нужна помощь в решении этих вопросов, я могу дать рекомендации и советы.