Многие проекты делятся на модули / подпроекты с использованием системы сборки ( Maven , Gradle , SBT …); и написание модульного кода, как правило, хорошо. Разделение кода на модули сборки в основном используется для:
- разделительные части кода (уменьшение связи)
- API / Impl сплит
- добавление сторонней зависимости только к определенной части кода
- группировка кода со схожей функциональностью
- статическая проверка того, что код в одном модуле использует только код из зависимых модулей (межмодульные зависимости)
Хотя некоторые могут сказать, что это также полезно для отдельной компиляции, я не думаю, что это имеет большое значение (при рассмотрении одного проекта). Инструменты сборки в наше время достаточно умны, чтобы выяснить, что нужно перекомпилировать.
Проблемы со сборочными модулями
Я думаю, что есть несколько проблем с этим подходом. Прежде всего, довольно трудно решить, когда часть функциональности «достаточно велика», чтобы превратить ее в модуль сборки. Достаточно ли нескольких классов? Или тебе нужно больше? Должен ли он быть строго по одной функциональности на модуль? Но это может привести к взрыву модуля; и так далее. По крайней мере, в проектах, в которых я принимал участие, это была общая тема дискуссий о том, насколько грубыми должны быть модули сборки.
Во-вторых, модули сборки довольно «тяжелые». Maven наихудший, я полагаю, вам нужен большой кусок xml для создания модуля с множеством шаблонов (например, повторный идентификатор группы, номер версии, определение родителя); SBT и Gradle намного лучше, но все же, это значительное усилие. Необходимо создать отдельный каталог , обновить всю структуру каталогов ( src/main/...
, src/test/...
), обновить конфигурацию сборки и т. Д. В целом, это довольно сложно.
И затем довольно часто, когда мы разделяем наши прекрасные модули, оказывается, что для того, чтобы два из них могли взаимодействовать, нам нужна «общая» часть. Затем мы либо получим раздутый модуль foo-common , который содержит множество несвязанных классов, либо несколько небольших модулей foo-foomodule-common ; второе решение, конечно, хорошо, за исключением времени, потраченного на его настройку.
Наконец, модуль сборки — это еще одна вещь, которую вы должны назвать; скорее всего, уже имя пакета и имя класса отражают то, что делает код, теперь его также необходимо повторить в имени модуля сборки (нарушение DRY).
В общем, я думаю, что создание модулей сборки слишком сложно и отнимает много времени. Программисты ленивы (что, конечно, хорошо), и это приводит к тому, что проекты не так чисты, как могли бы быть. Время изменить это :).
(Смотрите также мой предыдущий блог о модулях.)
пакеты
Java, Scala и Groovy уже имеют систему для группировки кода: пакеты. Однако в настоящее время пакет представляет собой просто строковый идентификатор. За исключением некоторых очень ограниченных параметров видимости (приватность пакета в Java, область видимости пакета в Scala), пакеты не имеют семантического значения. Итак, у нас есть несколько уровней группировки кода:
- проект
- Модуль сборки
- пакет
- Класс
Что, если мы объединили 2. и 3. вместе; почему не следует использовать пакеты для создания модулей?
Пакеты как модули?
Давайте посмотрим, что потребуется для расширения пакетов до модулей. Очевидно, что первое, что нам нужно, это связать некоторые метаданные с каждым модулем. Есть уже некоторые механизмы для этого (например , с помощью аннотаций на package-info.java
), или это может быть расширение объектов пакета в Scala — некоторые черты смешивать в или вал с переопределять.
Что за метаданные? Конечно, мы не хотим переносить полное определение сборки в пакеты. Но давайте разделим вопросы — определение сборки должно определять, как строить проект, а не каковы зависимости модуля. Тогда первое, что нужно определить в метаданных модуля, — это зависимости от сторонних библиотек. Такие определения могут быть только символами, которые будут привязаны к конкретным версиям в определении сборки.
Например, мы бы указали, что пакет « foo.bar.dao
» зависит от « jpa
» библиотек. В этом случае определение сборки будет содержать отображение « jpa
» в список артефактов maven (например, hibernate-core, hibernate-entitymanager и т. Д.). Более того, это было бы наиболее целесообразно, если бы такие зависимости были транзитивными для подпакетов. Поэтому определение глобальной библиотеки будет означать добавление зависимости от корневого пакета.
Как примечание: с расширением объектов пакета Scala это можно сделать даже безопасным для типов. Объекты пакета могут реализовывать черту, где одно из значений для переопределения может быть списком сторонних символов зависимостей. Сами символы могут, например, содержаться в Enumeration
, определенном в корневом пакете; что может сделать такие вещи, как «найти все модули, зависящие от jpa» простым поиском использования в IDE.
Второй шаг — определить межмодульные зависимости, используя этот механизм. В метаданных пакета можно было бы определить список других пакетов, из которых виден код. Далее следует, как используются модули сборки: каждый содержит список модулей проекта, к которым можно получить доступ. (Еще одно замечание по Scala: поскольку объекты пакета будут реализовывать черту, это будет означать определение списка объектов с заданным типом.)
Пойдя дальше, мы могли бы указать api и impl type-packages. Типы Api по умолчанию будут доступны из других пакетов. С другой стороны, пакеты Impl- type недоступны без явного указания их как зависимости.
Как это может выглядеть на практике? Очень грубый набросок в Scala:
package foo.user // Even without definition, each package has an implicit package object // implementing a PackageModule trait ... package object dao { // ... which is used here. The type of the val below is // List[PackageModule]. override val moduleDependsOn = List(foo.security, foo.user.model) override val moduleType = ModuleType.API // FooLibs enum is defined in a top-level package or the build system override val moduleLibraries = List(FooLibs.JPA) }
Рефакторинг
Рефакторинг — это повседневная деятельность; однако рефакторинг модулей — это, как правило, огромная задача, к которой можно прибегать лишь время от времени. Так и должно быть? Если пакеты были расширены до модулей, рефакторинг модулей был бы таким же, как перемещение и переименование пакетов, с дополнительной потребностью в обновлении метаданных. Это было бы намного проще, чем в настоящее время, что, я думаю, приведет к улучшению общего дизайна.
Система сборки
Вышеуказанное очевидно означало бы больше работы для системы сборки — было бы сложнее выяснить список модулей, порядок сборки, список создаваемых артефактов и т. Д. (Кстати, если бы для пакета мог быть создан отдельный jar-файл, он мог бы также быть частью метаданных). Также потребуются некоторые проверки — для циклических зависимостей или для неправильного ограничения видимости.
Но тогда люди сделали более сложное программное обеспечение, чем это.
Jigsaw?
Вы, вероятно, сказали бы, что это частично совпадает с проектом Jigsaw , который появится в Java 9 (или нет). Тем не менее, я думаю, что Jigsaw стремится к другому масштабу: модули уровня проекта. Таким образом, один модуль Jigsaw будет вашим целым проектом, а у вас будет несколько (десятки) пакетов-модулей.
Название «модуль» здесь перегружено, возможно, название «мини-модули» было бы лучше, или очень скромно «пакеты сделаны правильно».
Нижняя линия
Я думаю, что в настоящее время способ определения модулей сборки слишком сложен и ограничен. С другой стороны, подъем пакетов в модули был бы очень легким. Определение нового модуля будет таким же, как создание нового пакета — не может быть намного проще. Сторонние библиотеки могут быть добавлены только там, где это необходимо. Там будет на одну вещь меньше, чтобы назвать. И будет один источник дерева для каждого проекта.
Также такой подход будет масштабируемым и приспосабливаемым к потребностям проекта. Можно было бы определить мелкозернистые или крупнозернистые модули без особых усилий. Или даже лучше, почему бы не создать оба — модули могут быть вложенными и построены один поверх другого.
Теперь … единственная проблема заключается в реализации и добавлении поддержки IDE;)