أدوات البناء والوحدات

مشروع: بناء متعدد الوحدات

15 دقيقة الدرس 10 من 13

مشروع: بناء متعدد الوحدات

نادرًا ما تكون تطبيقات Java الحقيقية شجرة مصدر مسطّحة واحدة. تُقسّم الفِرق قواعد الشفرة الكبيرة إلى وحدات — مشاريع فرعية مستقلة تمتلك كل منها شريحة متماسكة من النطاق، وتعرض واجهة برمجية مقصودة لجيرانها، وتُجمَّع وتُختبر وتُصدر بصورة مستقلة. يرشدك هذا الدرس عبر بناء مشروع صغير لكنه واقعي متعدد الوحدات باستخدام Maven (مع ملاحظات حول المكافئ في Gradle)، موضحًا المقايضات في كل خطوة.

لماذا نقسم إلى وحدات؟

قبل كتابة سطر XML أو Kotlin DSL، يستحق السؤال: هل تحتاج فعلًا إلى بناء متعدد الوحدات؟

  • التغليف على نطاق واسع. لكل وحدة منفصلة ملف pom.xml خاص بها؛ الشفرة في الوحدات الأخرى لا تستطيع استخدام إلا ما تُعلنه اعتمادًا. هذا يمنع تشابك "كل شيء يعتمد على كل شيء" الذي يُصيب الأحجية الكبيرة المتراصة.
  • البناء التدريجي. يستطيع كلٌّ من Maven وGradle تجاوز إعادة تجميع وحدة لم تتغير مدخلاتها، مما يُقلّص أوقات البناء في المشاريع الكبيرة.
  • إيقاع نشر مستقل. يمكن نشر وحدة domain ذات العقود المستقرة في سجل داخلي مرة واحدة، ثم تستهلكها خدمات متعددة دون إعادة تجميع المستودع بأكمله.
  • حدود واضحة للفرق. الفريق أ يمتلك وحدة payments، والفريق ب يمتلك notifications. الملكية لا لبس فيها.
تكلفة التقسيم المبكّر. تُضيف البنى متعددة الوحدات طقوسًا إضافية — ملفات POM أكثر، واعتماديات صريحة بين الوحدات، وملف بناء جذري. طبّق هذا النمط حين يكون حدّ الوحدة مستقرًا فعلًا وتكلفة الاقتران حقيقية. تقسيم تطبيق من 2000 سطر إلى ست وحدات غالبًا تبكير مفرط.

المشروع: خدمة الطلبات الإلكترونية

سنبني خدمة طلبات مُبسَّطة بثلاث وحدات:

  • domain — نموذج النطاق بلغة Java الخالصة (الكيانات، كائنات القيمة، واجهات المستودع). بلا اعتماديات إطار؛ مصممة لتكون مستقرة وقابلة للاختبار بشكل مستقل.
  • application — طبقة حالات الاستخدام. تعتمد على domain. تحتوي على فئات خدمة تنسّق كائنات النطاق.
  • web — طبقة التسليم (محوّل HTTP رفيع). تعتمد على application. هذه هي نقطة الدخول التي تحتوي على main().

هذه بنية طبقية مقصودة؛ سهم الاعتماد يشير دائمًا نحو الداخل.

Maven: ملف POM الجذري

أنشئ المجلد الجذري order-service/. ملف pom.xml الجذري هو فقط واصف بناء — يُعلن التغليف كـ pom، ويُدرج كل الوحدات الفرعية، ويُمركز إصدارات الاعتماديات عبر <dependencyManagement>.

<!-- order-service/pom.xml --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>order-service</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>domain</module> <module>application</module> <module>web</module> </modules> <properties> <java.version>17</java.version> <maven.compiler.release>17</maven.compiler.release> <junit.version>5.10.2</junit.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <!-- الوحدات الداخلية: تُدار هنا أيضًا حتى تلتقط الوحدات الفرعية إصدار الأب دون تكرار --> <dependency> <groupId>com.example</groupId> <artifactId>domain</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>application</artifactId> <version>${project.version}</version> </dependency> </dependencies> </dependencyManagement> </project>
ضع أرقام الإصدارات دائمًا في كتلة <dependencyManagement> الجذرية. ترث الوحدات الفرعية من الأب، لذا تُعلن الاعتمادية بلا إصدار — يحلّه Maven من المجموعة المُدارة. هذه أكثر الطرق فاعلية لمنع انجراف الإصدارات عبر مشروع كبير.

وحدة domain

يُعلن ملف domain/pom.xml الأب ولا شيء آخر — لا اعتماديات تشغيل من جهة ثالثة، بتصميم مقصود.

<!-- order-service/domain/pom.xml --> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>order-service</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>domain</artifactId> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <!-- الإصدار محلول من dependencyManagement الأب --> </dependency> </dependencies> </project>

تستخدم شفرة النطاق سجلات Java 17 — كائنات قيمة غير قابلة للتعديل بلا كود نمطي:

// domain/src/main/java/com/example/domain/model/OrderId.java package com.example.domain.model; public record OrderId(String value) { public OrderId { if (value == null || value.isBlank()) throw new IllegalArgumentException("OrderId must not be blank"); } } // domain/src/main/java/com/example/domain/model/Order.java package com.example.domain.model; import java.math.BigDecimal; import java.time.Instant; import java.util.List; public record Order( OrderId id, String customerId, List<OrderLine> lines, Instant placedAt ) { public BigDecimal total() { return lines.stream() .map(OrderLine::subtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } } // domain/src/main/java/com/example/domain/model/OrderLine.java package com.example.domain.model; import java.math.BigDecimal; public record OrderLine(String sku, int quantity, BigDecimal unitPrice) { public BigDecimal subtotal() { return unitPrice.multiply(BigDecimal.valueOf(quantity)); } } // domain/src/main/java/com/example/domain/port/OrderRepository.java package com.example.domain.port; import com.example.domain.model.Order; import com.example.domain.model.OrderId; import java.util.Optional; public interface OrderRepository { void save(Order order); Optional<Order> findById(OrderId id); }

وحدة application

تعتمد وحدة application على domain وتربط حالات الاستخدام. لا تعرف شيئًا عن HTTP أو تفاصيل الاستمرارية.

<!-- order-service/application/pom.xml --> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>order-service</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>application</artifactId> <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>domain</artifactId> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> </dependency> </dependencies> </project>
// application/src/main/java/com/example/application/PlaceOrderCommand.java package com.example.application; import com.example.domain.model.OrderLine; import java.util.List; public record PlaceOrderCommand(String customerId, List<OrderLine> lines) {} // application/src/main/java/com/example/application/OrderService.java package com.example.application; import com.example.domain.model.Order; import com.example.domain.model.OrderId; import com.example.domain.port.OrderRepository; import java.time.Instant; import java.util.UUID; public class OrderService { private final OrderRepository repository; public OrderService(OrderRepository repository) { this.repository = repository; } public OrderId place(PlaceOrderCommand cmd) { var id = new OrderId(UUID.randomUUID().toString()); var order = new Order(id, cmd.customerId(), cmd.lines(), Instant.now()); repository.save(order); return id; } }
حقن المُنشئ، لا حقن الحقل. تستقبل OrderService الواجهة OrderRepository عبر مُنشئها. هذا يجعل الاعتمادية صريحة، ويُبقي الفئة محايدة من الإطار، ويجعل اختبارات الوحدة بسيطة — مرّر كائنًا وهميًا أو تطبيقًا داخليًا في الذاكرة.

وحدة web

وحدة web هي طبقة التسليم. تعتمد على application (وبشكل متعدٍّ على domain)، وتحتوي على main()، وتربط البنية التحتية الفعلية (كمستودع داخلي في الذاكرة لهذا المثال).

// web/src/main/java/com/example/web/InMemoryOrderRepository.java package com.example.web; import com.example.domain.model.Order; import com.example.domain.model.OrderId; import com.example.domain.port.OrderRepository; import java.util.HashMap; import java.util.Map; import java.util.Optional; public class InMemoryOrderRepository implements OrderRepository { private final Map<String, Order> store = new HashMap<>(); @Override public void save(Order order) { store.put(order.id().value(), order); } @Override public Optional<Order> findById(OrderId id) { return Optional.ofNullable(store.get(id.value())); } } // web/src/main/java/com/example/web/App.java package com.example.web; import com.example.application.OrderService; import com.example.application.PlaceOrderCommand; import com.example.domain.model.OrderLine; import java.math.BigDecimal; import java.util.List; public class App { public static void main(String[] args) { var repo = new InMemoryOrderRepository(); var service = new OrderService(repo); var cmd = new PlaceOrderCommand( "customer-42", List.of(new OrderLine("SKU-001", 2, new BigDecimal("29.99"))) ); var id = service.place(cmd); System.out.println("Order placed: " + id.value()); repo.findById(id) .ifPresent(o -> System.out.println("Total: " + o.total())); } }

بناء المشروع بالكامل

من المجلد الجذري، يبني أمر واحد كل وحدة بترتيب الاعتماد الصحيح:

# تثبيت كل الوحدات في مستودع Maven المحلي mvn install # تشغيل الاختبارات فقط عبر كل الوحدات mvn test # بناء وحدة application واعتمادياتها فقط (مفاعل Maven) mvn install -pl application -am # تخطّي الاختبارات للتكرار السريع (لا تفعل هذا في CI أبدًا) mvn package -DskipTests

المكافئ في Gradle بإيجاز

تستخدم نفس البنية في Gradle ملف settings.gradle.kts في الجذر لتعريف المشاريع الفرعية، ولكل مشروع فرعي ملف build.gradle.kts خاص به. يُطبّق ملف build.gradle.kts الجذري إضافات مشتركة وكتلة subprojects { ... } للإعداد المشترك. تستخدم الاعتماديات بين الوحدات نفس التدوين: implementation(project(":domain")).

// settings.gradle.kts (الجذر) rootProject.name = "order-service" include("domain", "application", "web") // application/build.gradle.kts plugins { java } dependencies { implementation(project(":domain")) testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") }
الاعتماديات الدائرية بين الوحدات خطأ في البناء، لا في وقت التشغيل. إذا استورد domain من application، يرفض Maven وGradle البناء. استخدم هذا كقوة دفع: الاعتماديات الدائرية تكشف مشكلة تصميم يجب حلّها في النموذج، لا التحايل عليها في أداة البناء.

أفضل الممارسات الاحترافية

  • ملف module-info.java واحد لكل وحدة (اختياري لكن موصى به). أعلن exports بشكل صريح لفرض التغليف على مستوى JVM لا بالاتفاقية فقط.
  • أبقِ وحدة domain خالية من الاعتماديات. إذا وجدت نفسك تضيف تعليقًا توضيحيًا لإطار ما لفئة في النطاق، فذلك انتهاك للحدود يستحق الإصلاح.
  • رقّم كل الوحدات الداخلية معًا. استخدم إصدار POM الأب لكل وحدة فرعية؛ يُحدّث أمر واحد mvn versions:set -DnewVersion=2.0.0 المفاعل بأكمله.
  • استخدم Maven Wrapper (mvnw) أو Gradle Wrapper (gradlew). أضفهما إلى التحكم في المصدر حتى يستخدم كل مطوّر وكل عامل CI نفس إصدار أداة البناء بالضبط دون تثبيت محلي.
  • انشر الوحدات المستقرة في سجل حزم (Nexus أو Artifactory أو GitHub Packages). يتعامل معها المستهلكون الداخليون كأي مكتبة خارجية، مما يتيح دورات إصدار مستقلة.

الخلاصة

يمنحك البناء متعدد الوحدات حدودًا مفروضة، وتجميعًا تدريجيًا، وفصلًا واضحًا للمخاوف يتوسّع مع حجم الفريق. النمط — POM الجذري كمفاعل، والوحدات الفرعية ذات الاعتماديات الصريحة بينها، وإصدارات الاعتماديات مُدارة مركزيًا — هو ذاته سواء اخترت Maven أو Gradle. وحدة domain هي القلب: أبقِها نقية، وسيتركّب باقي النظام حولها.