Понятно

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

  • Рассматриваем как подсистему. Android приложение - часть функциональности бизнеса.

  • Проектируем как систему. Android приложение - функциональность представления.

  • Разработку начинаем с центра. Выносим по краям только необходимый минимум и фиксируем контрактами.

  • Любые горизонтальные связи запрещены.

  • Запрещены сквозные зависимости.
    ScreenData-Api или Data-ImplFeature-Api

  • Модели контрактов (интерфейсы FeatureCase и Repository) всегда должны быть разными моделями. Выносить в общий my-common модуль запрещено.

  • UseCase является необязательной сущностью, как и модуль usecase.

    • Модуль usecase может проксировать зависимость одного feature-api.

    • feature модули могут поглотить в себя UseCase, если это не нарушает других правил

  • Модули usecasefeature-apifeature-impl и data-api строго являются котлин модулями.

  • ViewModel на одно действие пользователя может использовать не больше одного FeatureCase/UseCase.

  • Трансформация между внешней структурой данных (DTO) и внутренней для нашей системы (из интерфейса Repository) происходит в DataSource.

Чтобы стало Понятно, прочитайте блок Подробно. И только после вернитесь к Понятно.


Предисловие

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

Главная стратегия архитектуры в том, чтобы как можно дольше иметь как можно больше вариантов. (Роберт Мартин. Чистая архитектура с.145)

Кто-то говорит, что не бывает хорошей или плохой архитектуры, главное, чтобы задача была выполнена. Однако сложность проектирования, заключается в том, что "обратную связь" вы получите только через время, когда придется вносить изменения. Если мы сделаем систему, которая не будет работать, то это означает что мы вообще не выполнили нашу работу. По факту того, что система работает, нельзя сказать о том, хорошая это система или плохая.

Качество системы можно определить только после продолжительной эксплуатации.

Важно

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

Колесо

Итак вопрос: Как спроектировать качественную архитектуру системы?
Ответ: Начать с нуля и по этапам.

Дело в том, что построить "как-то", посмотреть что получилось и потом пытаться решить появившиеся проблемы, это хорошая тактика. Но не очень продуктивная, если периодически не делать переосмысление, чтобы учесть все полученные знания за это время.

- Может не стоит изобретать колесо?

Вот вам история:
Чемодан был придуман в середине 19 века (примерно 1850).
Колеса к чемодану прикрутили только в 1972 году. Это были 4 колеса внизу вертикально стоящего чемодана, который тянули за веревку. Из-за этого чемодан постоянно норовил уехать в сторону.
Но использовать это изобретение стали только через десятки лет после этого.
И только в 1989 создали привычный нам чемодан на 2 колесах с выдвижной ручкой.

Более 120 лет, на то, чтобы придумать колесо.


Подробно

Этапы проектирования системы:

  1. Определить этапы проектирования

  2. Определить терминологию и цели

  3. Высокоуровневая схема всей системы

  4. Определение проектируемого масштаба

  5. Установка границ проектируемой области

  6. Описание зон ответственности

  7. Определение направления зависимостей

  8. Формирование сущностей

  9. Выделение структур данных

  10. Разделение на модули

  11. Закрепление правил

1. Определить терминологию и цели

Цель

Код функциональностей должен не зависеть от платформы

Терминология

Функциональность - это конкретное действие для выполнения запроса пользователя.
Это не экран, не кнопка, не что-то на ui и не способ получения данных.

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

Платформа - конкретный способ представления функциональности, а так же, хранения и передачи информации.

Android SDK это только часть платформы, а сама платформа, это "Мобильное устройство с операционной системой Android".
Такое определение, приводит к выводу, что не только экраны, но и навигация по ним, это то, что зависит от платформы.
Пользователь, для блокировки карты, может воспользоваться звонком в поддержку, и функциональность (действие для выполнения запроса пользователя) не должна отличаться.

Запрос пользователя - любое действие пользователя, которое приводит к взаимодействию с нашей системой.

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

2. Высокоуровневая схема всей системы

Пользователь инициирует запрос (оранжевая стрелка) и он может быть обработан на любой из частей системы
Пользователь инициирует запрос (оранжевая стрелка)
и он может быть обработан на любой из частей системы

Это стандартная схема Клиент - Сервер - База данных
Каждая часть может быть реализована как угодно, и на масштабе ниже, должна проектироваться как самостоятельная система, а рассматриваться как часть (подсистема) общей системы.
Клиентом (Client) в такой схеме может быть что угодно: Android, Ios, Web, банкомат, оператор поддержки вместе с ПО для сотрудников поддержки. Все, с чем может взаимодействовать пользователь.

3. Определение проектируемого масштаба

Мы будем вести разговор только про часть Client и ее конкретную реализацию в виде Android приложения.

Android приложение - это совокупность экранов и компонентов, которые предоставляют доступ к функциональностям бизнеса.

Наше приложение может иметь:

  • Внешние вспомогательные части, предоставляемые другими разработчиками.
    Library level.

  • Внутренние вспомогательные части, такие как конфиги, тоглы, аналитика, утилиты.
    Common level.

  • Функциональности, которые наша система предоставляет пользователю.
    Feature level.

  • То, что сможет собрать все вместе в одну систему и запустить ее.
    Application level.

Client - это набор функциональностей, собранный в продукт, предоставляемый пользователю.Пунктирной стрелкой, отображается направление зависимостей.
Client - это набор функциональностей, собранный в продукт, предоставляемый пользователю.
Пунктирной стрелкой, отображается направление зависимостей.

4. Установка границ проектируемой области

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

Для пользователя важны только функциональности и то, как он может с ними взаимодействовать.

Поэтому сконцентрируемся именно на этом уровне: Feature level.

5. Описание зон ответственности

Зон ответственности, у любой системы, всегда будет ровно 3:

  • Предоставление пользователю способа взаимодействия - Presentation

  • Функциональности системы или логика предметной области - Domain

  • Предоставление нашим функциональностям доступа к внешним системам - Data

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

  • DataBase - отвечает за доступ к системе хранения данных (Data),

  • Server - отвечает за логику нашей предметной области (Domain),

  • Client - отвечает за представление пользователю функциональностей (Presentation).

Здесь важно сделать оговорку, что система не обязательно будет делиться именно на такие части.

6. Определение направления зависимостей

Вообще системой может быть практически все что угодно. Автомобиль, кафе, бизнес, дерево, город, компьютер, человек. По сути своей, все с чем можно взаимодействовать.

И любую такую систему можно разложить на эти 3 зоны ответственности (слоя).

Причем "главной" для нас, является Domain, потому что именно она определяет суть нашей системы.
Заменив domain слой, мы станем другой системой, тогда как заменив другие слои, мы остаемся для пользователя все еще той же самой системой.

Вопреки документации по Android, я утверждаю, что Domain слой является обязательным и не может быть proxy слоем.
Вопреки документации по Android, я утверждаю, что Domain слой является обязательным
и не может быть proxy слоем.

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

Например, система "автомобиль"

руль, педали и панель приборов - presentation;
(можно заменить на автопилот)

двигатель, трансмиссия, привода и колеса - domain;
(нельзя заменить на крылья, весла или рельсы)

системы воздухозабора и подачи топлива - data;
(можно заменить на электричество или пар)

дорога и воздух - api.

или система и точка

Любое кафе или ресторан.

официант/стойка/терминал/доставка - presentation;
кухня - domain;
склад/холодильник - data;
поставщики - api.

7. Формирование сущностей

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

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

Итак. Каждая зона ответственности, содержит еще 3 зоны. Определение количества сущностей, является определением масштаба углубления в зону ответственности.

Пояснение

Мы можем весь наш Android Client написать в одной Activity. То есть выделить в сущность всю System-Presentation зону (это весь наш Client, относительно всей системы целиком).

Или выделить для зоны Client-Data одну сущность, которая будет самостоятельно и в сеть ходить и в локальную базу.

А можем разделить этот слой на подзоны, выделив RepositoryImpl как Domain,
а различные DataSource, как Data.

Важно! В последнем случае, RepositoryImpl, будет отвечать за Domain зону, нашей Client-Data зоны, общей System-Presentation зоны.

Это может путать по началу, но пока сильно не акцентируйте на этом внимания.

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

Для начала, вспомним нашу цель.
Код функциональностей должен не зависеть от платформы

Отсюда мы определяем, что для Client, нам точно нужно выделить как минимум 3 зоны, чтобы Client-Domain не был зависим от платформы Android. Это масштаб привычных и понятных нам слоев из Clean Architecture.
KMP уже доказал нам, что мы сможем использовать такую Domain зону на других платформах.

Далее.
Экраны в Android это набор функциональностей, которые мы предоставляем пользователю и между этими наборами мы можем переключаться (навигироваться).
Платформа Android, также задает жизненный цикл экранов и их элементов.
А мы хотим, чтобы работа наших функциональностей, не зависела от платформы. Поэтому разделим нашу Presentation зону, еще на 3 зоны.
Главной задачей (Domain) здесь будет переживание жц и управление состоянием отображения.
В мире Android, для зоны Presentation существуют готовые шаблоны проектирования, такие как MVP, MVVM, MVI и прочие MV*. На текущий момент, стандартом принят MVVM, поэтому его мы и возьмем для нашей схемы. В этом шаблоне, ViewModel как раз отвечает за переживание жц и управлением состоянием отображения, то есть это и есть Client-Presentation-Domain ответственность.
View (Activity/Fragment/Compose) это Client-Presentation-Presentation ответственность.

ViewModel - отвязанная от жизненного цикла сущность, которая отвечает за реакцию на запрос пользователя.

Для зоны Data, мы применим ту же логику, отвязки от платформы. Только здесь, платформой Android, определяется реализация источников данных, таких как SQLite, REST, SharedPrefs и другие. Поэтому они у нас будут выделяться в отдельные сущности DataSourceImpl, которые будут отражать ответсвенность Client-Data-Data, которая максимально завязана на платформу. А вот в Client-Data-Domain, будет входить RepositoryImpl и интерфейс DataSource, чтобы сделать инверсию зависимости для этих зон ответственности. Потому что мы помним, что направление зависимостей, должно быть направленно на самую "главную" зону, внутрь системы.

Тем же принципом инверсии зависимостей мы определяем, что интерфейс Repository принадлежит Domain зоне, в качестве Client-Domain-Data ответственности.

И таким образом у нас осталась неопределенной только Domain зона. А она как раз и является сутью нашей системы. В ней содержатся конкретные функциональности. Или по другому кейсы функциональностей. Поэтому мы так и назовем эту сущность. FeatureCase.

FeatureCase - это конкретный независимый кейс функциональности.

В этом месте, настало время поговорить о такой вещи, как горизонтальные зависимости. Это когда FeatureCase вызывает другой FeatureCase, или вообще любая другая сущность, которая вызывает сущность того же типа. Можно долго спорить о том, можно так делать или нет, и для каждой уже работающей системы будет свой ответ, чаще всего потому, что "так исторически сложилось". Я хочу четко определить это для проектируемой нами системы, с помощью чего-то отдаленно похожего на доказательство.

Почему горизонтальные связи запрещены

Горизонтальная связь, подразумевает под собой связь class A на class B.

Мы можем упростить это до связи fun a на fun b.

Поскольку "горизонтальная" подразумевает сущности одного уровня, то мы не можем контролировать направление этих связей.

Что означает, что мы можем законно получить связь a > b > c > a

А это, ни что иное, как циклическая зависимость.

Циклическая зависимость функций может приводить к переполнению стека и крашу всей системы.

Поэтому циклическая зависимость - запрещена, а горизонтальные связи могут к ним приводить.

На этом основании, в нашей схеме, любые горизонтальные связи будут запрещены.

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

Такую ответственность можно смело назвать "кейс использования нескольких функциональностей", что отлично укладывается в название UseCase.
Не соотносите это с известным вам UseCase, который часто делают proxy.

UseCase - опциональная сущность, описывающая кейс использования нескольких функциональностей.

Важно подсветить, что DataSource, это интерфейс, а реализация будет иметь обратную стрелку зависимости.
Важно подсветить, что DataSource, это интерфейс,
а реализация будет иметь обратную стрелку зависимости.

Важно сразу отметить, что только эта сущность является не обязательной, и скорее всего, в функциональности, которую вы сейчас пишите, такой необходимости нет. Работая по полученной схеме, если вы столкнетесь с подходящей функциональностью, вы точно поймете что настало время использовать UseCase. Далее по статье, для наглядности, я буду показывать сразу 2 варианта схемы, с UseCase и без.

Мы определились с сущностями. Теперь давайте зафиксируем правила этих сущностей, чтобы вы точно понимали, как их реализовывать.

  • Горизонтальные связи запрещены.

  • ViewModel

    • количество публичных методов = количество всех возможных действий пользователя

    • на действие пользователя можно изменить стейт, навигироваться или вызвать функциональность

    • на одно действие можно вызвать строго не больше одной функциональности (FeatureCase или UseCase)

  • UseCase

    • опциональная сущность

    • вызывает 2 или более FeatureCase

  • FeatureCase

    • независимая функциональность

    • в случае сложной функциональности, можно расширять архитектуру этой сущности до нескольких вспомогательных классов, но только для чистоты и понятности кода

    • может использовать сколько угодно Repository для работы своей функциональности

  • Repository

    • описывает правила взаимодействия различных источников для получения/отправки данных

    • кеширование - это правила взаимодействия различных источников

  • DataSource

    • приводит протоколы взаимодействия с конкретными источниками данных к схожему виду, подходящему нашей системе

8. Выделение структур данных

В подходе DDD, проектирование начинается с определения структуры данных Domain части.

В книге Clean Architecture, в центре системы стоит Entity, что является описанием данных и их функций.

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

Мы начали проектирование схемы, для какой-то неопределенной системы, оставляя тем самым бесконечное количество вариантов реализации.

И только сейчас, определив структуру и сущности, мы можем описать то, как стоит работать со структурами данных и определять их.

Как я уже говорил ранее, каждую зону ответственности стоит проектировать как отдельную систему. Каждая система, должна выполнять некую трансформацию данных.

Примеры

Автомобиль трансформирует топливо в кинетическую энергию

Тормоза трансформируют кинетическую энергию в тепловую

Инжектор трансформирует воздух и топливо в горючую смесь

Кухня кафе трансформирует ингредиенты в блюда

Мойщик продуктов трансформирует воду и ингредиент в чистый ингредиент

Виды трансформации данных:

  • Прямая - из A получить B

  • Объединение - из A и A получить A

  • Разделение - из A получить A и A

  • Комбинация - из A и B получить С

Таким образом, мы можем уверенно говорить о том, что:

  • Presentation зона занимается трансформированием результата работы функциональности в данные пригодные для отображения;

  • Data зона, трансформирует данные из источников в данные пригодные для системы;

  • Domain зона занимается трансформацией данных пригодных для системы, в результат работы функциональности.

Итого, у нас получается как минимум 4 типа структур данных:

  1. Получаемые из других систем (DTO)

  2. Пригодные для работы в нашей системе

  3. Результат работы функциональности

  4. Данные отображения, которые увидит пользователь

А каждая сущность отвечает за конкретный тип трансформации:

  • DataSource - прямая

  • Repository - объединение

  • FeatureCase - комбинация

  • UseCase - комбинация

  • ViewModel - прямая

Каждая структура по итогу будет соответствовать интерфейсу какой-то сущности
Каждая структура по итогу будет соответствовать интерфейсу какой-то сущности
Структура данных для UseCase, так же не обязательна, как и сам UseCase
Структура данных для UseCase, так же не обязательна, как и сам UseCase

9. Разделение на модули

На этом этапе, наша схема уже готова и ее можно применять.

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

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

Принцип разделения на модули прост. Вынести в отдельные модули независимые части, то есть то, на что указываю стрелки зависимостей с обоих сторон. И все остальное, что осталось без модулей.

Сложное объяснение

Нам необходимо рассмотреть типы отношений между различными сущностями.

Экран - это всего лишь набор функциональностей. А любая функциональность может использоваться на разных экранах. ViewModel на FeatureCase имеет отношение многие-ко-многим.

Интерфейс FeatureCase и его реализация FeatureCaseImpl имеют отношение один-к-одному. В некоторых случаях, может быть отношение один-ко-многим.

То же самое справедливо для UseCase. А UseCaseImpl на FeatureCase, так же является отношением многие-ко-многим.

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

Отсюда делаем вывод, что FeatureCaseImpl и Repository имеют отношение многие-ко-многим.

Ровно теми же рассуждениями получаем связи, где отношение разных сущностей описывается как многие-ко-многим, а отношение интерфейса и реализации, как один-к-одному.

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

Из этих рассуждений, мы получаем правило выделения независимый частей.

Обратите внимание на модуль usecase
Обратите внимание на модуль usecase

Сущность UseCase встречается редко, является маленькой и изменяется совместно с FeatureCase, из-за чего накладные расходы на разделение api/impl модулей, оказываются не оправданными.

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

Или иметь транзитивную api зависимость только на один из feature-api модулей, чтобы не создавать собственную модель данных и маппинг, если этого не требуется.

Так же, не стоит путать горизонтальные стрелки на этой схеме с горизонтальными связями. Если рассматривать эту схему модулей в разрезе всего Client приложения, то мы получим вот такое представление:

Любые горизонтальные зависимости запрещены, как вправо/влево, так и в глубь схемы.Отдельно отмечу сквозные зависимости screen на data-api или data-impl на feature-api: они категорически запрещены.
Любые горизонтальные зависимости запрещены, как вправо/влево, так и в глубь схемы.
Отдельно отмечу сквозные зависимости screen на data-api или data-impl на feature-api:
они категорически запрещены.
Одиночная стрелка означает отношение один-к-одномуНесколько тонких стрелок означают отношение многие-ко-многим
Одиночная стрелка означает отношение один-к-одному
Несколько тонких стрелок означают отношение многие-ко-многим

Может показаться, что у нас получается слишком много модулей. По старой привычке, можно сказать, что "это же 6 модулей на фичу получается"! Однако стоит понимать, что количество сущностей разных типов не будут соответствовать. То есть модулей screen будет гораздо меньше, чем модулей feature.

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

10. Закрепление правил

  • Рассматриваем как подсистему. Android приложение - часть функциональности бизнеса.

  • Проектируем как систему. Android приложение - функциональность представления.

  • Разработку начинаем с центра. Выносим по краям только необходимый минимум и фиксируем контрактами.

  • Любые горизонтальные связи запрещены.

  • Запрещены сквозные зависимости.
    ScreenData-Api или Data-ImplFeature-Api

  • Модели контрактов (интерфейсы FeatureCase и Repository) всегда должны быть разными моделями. Выносить в общий my-common модуль запрещено.

  • UseCase является необязательной сущностью, как и модуль usecase.

    • Модуль usecase может проксировать зависимость одного feature-api.

    • feature модули могут поглотить в себя UseCase, если это не нарушает других правил

  • Модули usecasefeature-apifeature-impl и data-api строго являются котлин модулями.

  • ViewModel на одно действие пользователя может использовать не больше одного FeatureCase/UseCase.

  • Трансформация между внешней структурой данных (DTO) и внутренней для нашей системы (из интерфейса Repository) происходит в DataSource.


Примечание

Совершенно неважно какой паттерн выбрать для Presentation слоя. Это никак не отразится на Domain слое.

Мы можем беспрепятственно переписать Data слой и заставить его работать на websoket или graphql. Или заменить заглушкой для тестов.

При желании, центральная часть может быть без проблем извлечена и помещена в KMP проект. Что означает абсолютную ее независимость от других слоев и от платформы «Мобильного устройства с ОС Android».

В данном документе мы не рассматривали структуру Application и Common уровней.

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

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

  • Выбор di библиотеки, для нас совершенно не важен, потому что находится выше в уровне Application и любой вариант легко укладывается на схему.

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

Главная стратегия архитектуры в том, чтобы как можно дольше иметь как можно больше вариантов. (Роберт Мартин. Чистая архитектура с.145)

Common level

А вот для проектирования уровня Common, есть важный совет.

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

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

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


  1. quaer
    12.10.2023 06:43
    +1

    Как обратиться к ресурсам приложения из библиотеки? Например, надо выбросить исключение и показать сообщение об ошибке на языке пользователя. Или перечисления с текстовым представлением.


    1. Not_coolEd Автор
      12.10.2023 06:43

      Любая библиотека всегда находится на самом низком уровне зависимостей. То есть она априори не может быть зависима от нас. В таком случае у нас остается только вариант инверсии зависимостей. Когда библиотека выдает контракт в виде интерфейса, который мы должны реализовать и передать ей при инициализации. Это может также выглядеть как плагины, подключаемые к библиотеке или другие способы инверсии зависимостей.

      В рамках текущей статьи это не имеет значения, поскольку модули feature level имеют право зависеть от публичных модулей из уровней ниже. А управление зависимостями и инициализации происходят на Application уровне.


      1. quaer
        12.10.2023 06:43

        Звучит красиво. Давайте к практике. Есть enum. у него есть текстовое поле. Как реализовать эту самую инверсию зависимостей данном случае?


        1. Not_coolEd Автор
          12.10.2023 06:43

          А где этот enum лежит? Где и для чего используется? Я не очень понимаю условия вопроса. Можете, пожалуйста, предоставить более подробное описание проблемы?


          1. quaer
            12.10.2023 06:43

            У Андроид есть context, который нужен для доступа к ресурсам приложения. (Зачем они так сделали?) Из-за этого периодически возникают сложности. Как осуществляется доступ к context?


            1. Not_coolEd Автор
              12.10.2023 06:43

              Доступ к контексту есть только в 2 местах.
              AppContext - при создании графа зависимостей, чтобы создавать различные библиотечные зависимости.
              ActivityContext - на View, чтобы достать что-то из ресурсов. Можете почитать о том, как делаются обертки Text, которые получают значения из ресурсов при установке во View, или про то, как делают ResourceWrapper, для ViewModel.

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


              1. quaer
                12.10.2023 06:43
                +1

                То есть вы предлагаете хранить ID ресурса и далее выписывать везде обертки?

                Как быть в случае, когда библиотека не знает, будет ли она работть на мобильной платформе, или на обычной?


  1. MexanicD
    12.10.2023 06:43
    +1

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


  1. decoy9
    12.10.2023 06:43
    +1

    За несколько лет коммерческой разработки мне ни разу не приходилось сталкиваться с необходимостью в интерфейсах interface UseCase и interface FeatureCase
    Аргумент про тестирование? Любой сахарный фреймворк для тестов и так отлично замокает все наши классы.

    В общем, для меня неочевидно, зачем эта абстракция.


    1. Not_coolEd Автор
      12.10.2023 06:43

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

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


      1. maksonic
        12.10.2023 06:43

        Есть два экрана которым нужен один и тот же список данных. Пусть у нас будет какой-нибудь FetchItemsUseCase, юзкейс возвращает список данных доменного уровня, т.е модели не готовы к использованию на UI, чтоб их приготовить, мы должны во viewModel смапить домейн лист к ui листу. Правильно? Дак вот, мы должны в каждом модуле создавать свою модель и маппер для работы с домен уровнем? Горизонтальные зависимости запрещены и если я правильно понял, то и создание модуля содержащего общую ui модель и маппер тоже нельзя, так? Но тогда мы получаем копипасту. Как быть?


        1. Not_coolEd Автор
          12.10.2023 06:43

          В данном случае, мы можем вынести в отдельный модуль, на уровень common, view для нашего списка и модель этого списка. Они связаны 1к1. Маппинг мы не сможем сюда вынести, потому что это уровень common, а связи снизу вверх запрещены.
          В 2 разных screen модулях мы можем зависеть от одних и тех же feature-api и common-my-list-ui.

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

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

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


          1. maksonic
            12.10.2023 06:43

            Хорошо, теперь понял момент, спасибо. А вообще нет в планах проект написать по данной схеме или может уже есть такой, тогда где его можно посмотреть?


            1. Not_coolEd Автор
              12.10.2023 06:43
              +1

              Только начал, поэтому не судите строго) И пока особо нету времени активно продолжить, но обещаю, что не заброшу) https://github.com/NekrutovEd/EasyFinance


              1. maksonic
                12.10.2023 06:43

                Супер!????


  1. Antoxaxotna
    12.10.2023 06:43
    +2

    "И только в 1989 создали привычный нам чемодан на 2 колесах с выдвижной ручкой.

    Более 120 лет, на то, чтобы придумать колесо."

    Думается, привычный нам чемодан на 2 колесах 120 лет пришлось бы носить, ввиду отсутствия ровных полов в достаточном количестве.


    1. Not_coolEd Автор
      12.10.2023 06:43
      +1

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

      Эта история больше о том, что очевидные для нас вещи, не всегда являлись очевидными. Казалось бы, что может быть очевиднее коробки для бумаг или раскладывания документов по папкам и стопкам? Однако люди долгое время перевязывали стопки бумаг веревками и складывали их в кучу "как-то". На самом деле, существует достаточно много таких случаев, когда казалось бы очевидные для нас вещи, никому не приходили в голову.


  1. SchwarzerEngel
    12.10.2023 06:43
    +3

    Очень хочется посмотреть на пример приложения, которое построено по данным принципам. А вообще статья топовая! Жду продолжения. Очень познавательно! (Спойлеры с объяснением "для детей" вообще топ! Это очень грамотно.)


    1. Not_coolEd Автор
      12.10.2023 06:43

      Большое спасибо за обратную связь, я старался)


  1. DimaK21
    12.10.2023 06:43
    +2

    Я правильно понял, что если у нас, например, 10 feature-api, 5 data-api, то это 15 модулей, в каждом из которых лежит по одному интерфейсу? Или это 2 модуля, в одном из которых 10 интерфейсов и в другом 5?


    1. Not_coolEd Автор
      12.10.2023 06:43
      +1

      Отличный вопрос!) Чисто физически можно сделать и так и так. Однако, сложив все в один модуль, мы теряем преимущества модуляризации. Разбив модули по 1 интерфейсу, мы создаем излишний оверхед.

      Лучшим критерием объединения модулей является общий контекст. Например:
      data-api которые возвращают Chat и Message, лучше объединять, потому что это связанные сущности. Однако feature-api по работе с чатами, можно отделить от feature-api по работе с сообщениями, так как использоваться они будут в разных контекстах. При этом мы не отменяем того, что в контексте чата мы можем возвращать список чатов вместе с последним сообщением.