Лучшая практика для API Gradle по сравнению с реализацией в многомодульном проекте

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

Допустим, у меня есть следующие модули в приложении

  • library
  • base
  • feature1
  • feature2
  • app

Теперь отношения между модулями:

base обертывает library

feature1 и feature2 используют (зависит) от base

app объединяет feature1 и feature2

Все в этой многомодульной структуре должно иметь возможность работать с зависимостями Gradle implementation, и нигде не нужно использовать предложение api.

Теперь предположим, что feature1 должен получить доступ к деталям реализации base, включенным в library.

Насколько я могу судить, чтобы сделать library доступным для feature1, у нас есть два варианта:

  1. Измените implementation на api в base, чтобы передать зависимость модулям, которые зависят от base.

  2. Добавьте library в качестве зависимости implementation к feature1 без утечки зависимости base от library.

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

Мы могли бы создать промежуточный модуль base-feature, который может обернуть base и предоставить feature1 другой уровень абстракции для использования без утечки library, но давайте оставим это решение за рамками этой проблемы, чтобы сосредоточиться на настройке зависимостей.


Некоторые компромиссы, который я обнаружил в приведенных выше параметрах:

Вариант 1) плюсы

  • Меньшие файлы build.gradle, так как нет необходимости повторять пункты implementation.
  • Более быстрое редактирование скриптов сборки. Просто внесите одно изменение в предложение api и посмотрите, как изменения распространяются на все потребительские модули.

Вариант 1) минусы

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

Вариант 2) плюсы

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

Вариант 2) минусы

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

Теперь вопросы:

  • Есть ли какие-либо компромиссы с точки зрения компиляции этого многомодульного сценария?

  • Является ли модуль, пропускающий зависимость, «быстрее» для компиляции потребительских модулей?

  • Сильно ли это влияет на время сборки?

  • Какие еще побочные эффекты, плюсы/минусы мне не хватает?

Спасибо за ваше время.

11
0
3 081
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Репост из темы Форум Gradle.

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

С моей точки зрения, если модуль должен разбить слои, я бы смоделировал структуру проекта, чтобы показать это наиболее прямым и наглядным образом. В данном случае это означает добавление library в качестве зависимости реализации от feature1. Да, это делает диаграмму более уродливой, да, это заставляет вас трогать еще несколько файлов при обновлении, и в этом суть - в вашем дизайне есть недостаток, и теперь он виден.

Если несколько модулей должны разорвать инкапсуляцию уровня таким же образом, я могу рассмотреть возможность добавления отдельного базового модуля, раскрывающего эту функциональность, с таким именем, как base-xyz. Добавление нового модуля — это большое дело не из-за технической работы, а потому, что наш мозг может обрабатывать только определенное количество «вещей» за раз (фрагментирование). Я считаю, что то же самое будет справедливо и для «вариантов» Gradle, когда они станут доступны, но я пока не могу этого утверждать, поскольку я не пробовал их на практике.

Если всем клиентам модуля base требуется доступ к library (т. е. потому что вы используете классы или исключения из library в своих общедоступных подписях), вам следует предоставить library как зависимость API от base. Недостатком этого является то, что library становится частью общедоступного API base, и он, вероятно, больше, чем вам хотелось бы, и не находится под вашим контролем. Публичный API — это то, за что вы несете ответственность, и вы хотите, чтобы он был небольшим, документированным и обратно совместимым.

В этот момент вы можете думать о модулях jigsaw (хорошо), osgi (эээ… не стоит) или об обертывании частей lib, которые вам нужно выставить в ваших собственных классах (может быть?)

Обертывание только ради разрыва зависимостей — не всегда хорошая идея. Во-первых, это увеличивает объем кода, который вы поддерживаете и (надеюсь) документируете. Если вы начнете делать небольшие адаптации на уровне base, а library является хорошо известной библиотекой, вы вносите несоответствия (с добавленной стоимостью) - нужно всегда быть начеку, остаются ли в силе их предположения для lib. Наконец, часто тонкие оболочки в конечном итоге приводят к утечке дизайна библиотеки, поэтому, даже если они обертывают API, это все равно заставляет вас касаться клиентского кода при замене/обновлении lib, и в этот момент вам, возможно, лучше использовать lib напрямую.

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

Не зацикливайтесь на лучшем (как в «Что бы сделал дядя Боб») дизайне, когда сработает любой хороший дизайн. Количество дополнительной сложности, которое оправдано для введения порядка, является нечеткой величиной, и это то, что вы должны решить. Лучше позвони и не бойся завтра поменять :-)

О какой утечке идет речь? Если я использую implementation в библиотеках, то в хост-приложении я буду вынужден писать что-то вроде implementation library implementation wraps implementation base, иначе приложение будет падать во время выполнения. Т.е. утечка wraps и base все равно будет. Если я использую api в библиотеках, то в хост-приложении буду вынужден писать что-то типа implementation library и все. и здесь тоже будет ваша "утечка"

tim4dev 25.10.2019 09:39

Извините, но я не понимаю вашего вопроса. Какую часть моего ответа вы имеете в виду? Что вы подразумеваете под форматированием кода: база, обертки, библиотека, реализация? Небольшое цитирование и контекст помогут, хотя я понимаю, что это сложно сделать в комментарии SO. Хотите перенести обсуждение в связанную ветку на форуме Gradle?

ddimitrov 26.10.2019 11:51

Просто пройдя несколько лет спустя, «платформы» Gradle — это хороший способ связать версии библиотек, которые всегда используются вместе. Однажды я могу обновить ответ выше, но пока обязательно прочитайте документы :-)

ddimitrov 06.11.2021 05:13

Другие вопросы по теме