Software Architecture

فرض حدود الوحدات: استخدام ArchUnit وفحوصات وقت البناء لمنع تدهور الأحادية الوحدوية

في مجال تطوير المؤسسات بلغة جافا، شهدت "الأحادية الوحدوية" (Modular Monolith) إحياءً كبدائل عملية لتفكيك الخدمات المصغرة المبكر. ومع ذلك، دون حواجز معمارية صارمة، غالبًا ما تعاني هذه الأنظمة من "الإنتروبيا المعمارية"، حيث تتسرب الوحدات تدريجيًا إلى بعضها البعض، مما يخلق اقترانًا وثيقًا يجعل الاستخراج المستقبلي إلى خدمات مصغرة مؤلمًا أو مستحيلًا. نادرًا ما يكون هذا التدهور مقصودًا؛ بل يحدث من خلال السعي وراء الراحة. لمكافحة ذلك، يجب علينا تجاوز العادات المتعارف عليها وفرض الحدود على مستوى البناء.

المشكلة: الاقتران الضمني في الأحادية الوحدوية

تعامل الأحادية الوحدوية المصممة جيدًا مع كل وحدة كخدمة مصغرة محتملة في المستقبل. وهذا يعني أنه يجب ألا يصل وحدة A مباشرة إلى الكيانات الداخلية لوحدة B. بدلاً من ذلك، يجب أن تتفاعل مع وحدة B عبر واجهة برمجة تطبيقات (API) محددة جيدًا. في الممارسة العملية، غالبًا ما يتجاوز المطورون هذه الواجهات عندما يكون "أسهل" استدعاء مستودع أو خدمة مباشرة. مع مرور الوقت، تتراكم هذه الاختصارات، مما يحول الأحادية إلى "كرة طين كبيرة".

المراجعات اليدوية للكود ليست قابلة للتوسع لهذا الغرض. قد يفوت المطور اعتمادية خفية في إعادة هيكلة معقدة. نحن بحاجة إلى تحقق آلي ومستمر.

تقديم ArchUnit: اختبار الوحدات المعمارية

ArchUnit هو مكتبة قوية لمنصة جافا تتيح لك كتابة "اختبارات الوحدات المعمارية". هذه الاختبارات هي اختبارات JUnit عادية تحدد بنية الكود الخاص بك. إذا تم انتهاك قاعدة، يفشل البناء، مما يمنع دمج الكود السيئ.

تنفيذ عملي: تعريف الحدود

لنفترض أن لدينا أحادية وحدوية تحتوي على وحدتين: order-service و inventory-service. نريد التأكد من أن order-service يمكنه الوصول إلى inventory-service فقط عبر حزمة com.myapp.inventory.api، وليس عبر فئات التنفيذ الداخلية الخاصة به.


import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;

public class ArchitectureTest {

    private final JavaClasses classes = new ClassFileImporter().importPackages("com.myapp");

    @Test
    public void order_service_should_not_access_inventory_implementation() {
        ArchRule rule = noClasses().that().resideInAPackage("com.myapp.order..")
                .should().dependOnClassesThat().resideInAPackage("com.myapp.inventory.internal..");

        rule.check(classes);
    }

    @Test
    public void modules_should_only_interact_via_api_packages() {
        ArchRule rule = slices().matching("com.myapp.(*)..")
                .should().notDependOnEachOther()
                .allowAccessBetween("com.myapp.order..", "com.myapp.inventory.api..");

        rule.check(classes);
    }
}

التكامل مع CI/CD وأدوات البناء

لكي تكون هذه الفحوصات فعالة، يجب أن تكون جزءًا من عملية البناء القياسية. في مشروع Maven أو Gradle، يتم تنفيذ اختبارات ArchUnit أثناء مرحلة test. إذا حاول المطور إضافة اعتمادية مباشرة من فئة في خدمة الطلبات إلى فئة داخلية في المخزون، سيفشل البناء فورًا.

حلقة التغذية الراجعة الفورية هذه حاسمة. فهي تحول الامتثال المعماري من تدقيق بعد الحدث إلى قيد تطوير في الوقت الفعلي. علاوة على ذلك، يمكنك توسيع هذا النهج باستخدام FreezingArchRule من ArchUnit لإدارة الكود القديم. إذا كان لديك انتهاكات موجودة مسبقًا، يمكنك "تجميدها"، مما يسمح بمرور البناء مع منع الانتهاكات الجديدة. هذا يجعل إعادة هيكلة قواعد الكود الكبيرة القديمة ممكنة.

الذهاب أبعد: فرض اعتماديات الطبقات

إلى جانب حدود الوحدات، يمكنك فرض فصل الطبقات داخل الوحدات. على سبيل المثال، التأكد من أن المتحكمات (Controllers) لا تصل مباشرة إلى المستودعات (Repositories)، وإجبارها على المرور عبر طبقات الخدمة. يجعل ArchUnit هذا الأمر بسيطًا:


@Test
public void controllers_should_not_access_repositories() {
    ArchRule rule = noClasses().that().resideInAPackage("..controller..")
            .should().dependOnClassesThat().resideInAPackage("..repository..");
    rule.check(classes);
}

الخلاصة

تقدم الأحادية الوحدوية أفضل ما في العالمين: قابلية النشر كمنتج واحد والوضوح الهيكلي للخدمات المصغرة. ومع ذلك، هذا الوضوح لا يحافظ على نفسه. من خلال دمج ArchUnit في خط أنابيب البناء الخاص بك، تنشئ حارسًا آليًا يحافظ على نيتك المعمارية. إنه يحوّل البنية الجيدة من هدف يجب السعي إليه إلى قيد يجب الوفاء به، مما يضمن بقاء نظامك قويًا وقابلًا للاختبار وجاهزًا للتطور المستقبلي.

Share: