Software Architecture

اجرای مرزهای ماژول: استفاده از ArchUnit و بررسی‌های زمان ساخت برای جلوگیری از فرسودگی مونولیت ماژولار

در دنیای توسعه سازمانی جاوا، «مونولیت ماژولار» به عنوان جایگزینی عملی برای تجزیه زودهنگام به میکروسرویس‌ها دوباره سر برآورده است. با این حال، بدون نرده‌های محافظتی معماری سختگیرانه، این سیستم‌ها اغلب از «آنتروپی معماری» رنج می‌برند، جایی که ماژول‌ها به تدریج به یکدیگر نفوذ می‌کنند و وابستگی‌های تنگاتی ایجاد می‌کنند که استخراج آینده به میکروسرویس‌ها را دردناک یا غیرممکن می‌سازد. این فرسودگی نادرست عمدی است؛ بلکه از طریق راحتی اتفاق می‌افتد. برای مقابله با این مسئله، باید فراتر از عرف حرکت کنیم و مرزها را در سطح ساخت اجرا کنیم.

مسئله: وابستگی ضمنی در مونولیت‌های ماژولار

یک مونولیت ماژولار طراحی‌شده به‌خوبی، هر ماژول را به عنوان یک میکروسرویس بالقوه آینده در نظر می‌گیرد. این به این معنی است که ماژول A هرگز نباید مستقیماً به موجودیت‌های داخلی ماژول B دسترسی داشته باشد. به جای آن، باید از طریق یک API تعریف‌شده با ماژول B تعامل کند. در عمل، توسعه‌دهندگان اغلب زمانی که «آسان‌تر» است مستقیماً به یک repository یا service فراخوانی کنند، از این API‌ها عبور می‌کنند. با گذشت زمان، این میان‌برها انباشته می‌شوند و مونولیت را به یک «توده بزرگ گِل» تبدیل می‌کنند.

بازبینی دستی کد برای این هدف مقیاس‌پذیر نیست. یک توسعه‌دهنده ممکن است یک وابستگی ظریف را در یک بازسازی پیچیده از دست بدهد. ما به تأیید خودکار و پیوسته نیاز داریم.

معرفی ArchUnit: تست‌های واحد معماری

ArchUnit یک کتابخانه قدرتمند برای پلتفرم جاوا است که به شما امکان می‌دهد «تست‌های واحد معماری» بنویسید. این تست‌ها تست‌های معمولی JUnit هستند که ساختار کد شما را تأیید می‌کنند. اگر یک قانون نقض شود، ساخت شکست می‌خورد و از ادغام کد بد جلوگیری می‌کند.

پیاده‌سازی عملی: تعریف مرزها

فرض کنید یک مونولیت ماژولار با دو ماژول داریم: order-service و inventory-service. می‌خواهیم اطمینان حاصل کنیم که order-service فقط می‌تواند از طریق پکیج com.myapp.inventory.api به inventory-service دسترسی داشته باشد، نه از طریق کلاس‌های پیاده‌سازی داخلی آن.


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 این رویکرد را گسترش دهید تا کد قدیمی را مدیریت کنید. اگر نقض‌های موجود دارید، می‌توانید آن‌ها را «منجمد» کنید، که به ساخت اجازه می‌دهد عبور کند در حالی که از نقض‌های جدید جلوگیری می‌کند. این امر بازسازی پایگاه‌های کد بزرگ قدیمی را امکان‌پذیر می‌سازد.

گام‌های بعدی: اجرای وابستگی‌های لایه‌ای

فراتر از مرزهای ماژول، می‌توانید جداسازی لایه‌ها را درون ماژول‌ها اجرا کنید. برای مثال، اطمینان حاصل کنید که Controllerها مستقیماً به Repositoryها دسترسی ندارند و مجبور می‌شوند از طریق لایه‌های Service عبور کنند. ArchUnit این کار را ساده می‌کند:


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

نتیجه‌گیری

مونولیت‌های ماژولار بهترین‌های هر دو دنیای را ارائه می‌دهند: قابلیت استقرار یک تک‌آرتیفکت و وضوح ساختاری میکروسرویس‌ها. با این حال، این وضوح به‌طور خودکار حفظ نمی‌شود. با یکپارچه‌سازی ArchUnit در خط لوله ساخت خود، یک نگهبان خودکار ایجاد می‌کنید که نیت معماری شما را حفظ می‌کند. این امر معماری خوب را از یک هدفی که باید دنبال شود به یک محدودیتی که باید ارضا شود تبدیل می‌کند و اطمینان حاصل می‌کند که سیستم شما باقی می‌ماند، قابل تست است و برای تکامل آینده آماده است.

Share: