Problem: framework-ə qaynaq olunmuş biznes qaydaları
Endirim qaydası @RestController metodunun içindədir; həmin metod həm də JpaRepository çağırır, HttpServletRequest oxuyur və ResponseStatusException atır. Qaydanı test etmək üçün Spring kontekstinə və verilənlər bazasına ehtiyac var. Eyni qaydanı message listener-ə köçürmək üçün onu kopyalayırsınız. JPA provider-i yeniləmək üçün biznes koduna toxunursunuz. Framework arxitekturaya çevrilib.
Clean architecture, hexagonal stil və ports and adapters yaxın qohumdur və ortaq ideyaları var: biznes qaydaları çatdırılma və ya infrastruktur təfərrüatlarından asılı olmamalıdır. Ağılla tətbiq edildikdə bu, millisaniyələrlə işləyən testlər və framework dəyişikliyindən sağ çıxan nüvə verir. Mexaniki tətbiqdə isə bir sətirlik interfeyslərlə dolu qovluqlar yaranır. Bu məqalə birinci nəticəni hədəfləyir.
Dependency rule
Mənbə kodu asılılıqları yalnız içəriyə doğru yönəlir.
+--------------------------------------------------+
| Framework / driver: web, JPA, Kafka, HTTP |
| +--------------------------------------------+ |
| | Adapter: controller, repository impl | |
| | +--------------------------------------+ | |
| | | Application: use case, port | | |
| | | +--------------------------------+ | | |
| | | | Domain: entity, qaydalar | | | |
| | | +--------------------------------+ | | |
| | +--------------------------------------+ | |
| +--------------------------------------------+ |
+--------------------------------------------------+
asılılıqlar mərkəzə doğru yönəlirDomain Spring, JPA və ya HTTP haqqında heç nə bilmir. Application layı (use case-lər) domain-i tanıyır və xaricdən ehtiyacını interfeys kimi, yəni port kimi elan edir. Xarici halqadakı adapter-lər həmin port-ları implementasiya edir və use case-ləri çağırır. İdarəetmə axını xaricə gedə bilər (use case verilənlər bazasını çağırır), amma mənbə asılılığı içəriyə yönəlir, çünki interfeys daxili layə məxsusdur. Bu çevrilmə dependency inversion-dır.
Konkret struktur
Əvvəlcə feature üzrə paketləmə, hər feature daxilində layer-lər:
ordering/
domain/ Order, Money, OrderLine, OrderPolicy
application/ PlaceOrderUseCase, port/out/OrderRepository,
port/out/PaymentGateway
adapter/in/web/ OrderController, PlaceOrderRequest
adapter/out/ JpaOrderRepository, PaymentHttpClientPort istifadə edildiyi yerdə, application layında elan olunur:
// application/port/outpublic interface OrderRepository { Optional<Order> findById(OrderId id); void save(Order order);}Use case JPA-dan yox, port-dan asılıdır:
@Service@Transactionalpublic class PlaceOrderUseCase { private final OrderRepository orders; private final PaymentGateway payments; public PlaceOrderUseCase(OrderRepository o, PaymentGateway p) { this.orders = o; this.payments = p; } public OrderId place(PlaceOrderCommand cmd) { Order order = Order.create(cmd.customerId(), cmd.lines()); payments.authorize(order.id(), order.total()); orders.save(order); return order.id(); }}Adapter tərcüməni edir və persistence modeli domain modelindən fərqli ola bilər:
@Repositoryclass JpaOrderRepository implements OrderRepository { private final OrderJpaRepo jpa; // Spring Data interfeysi public void save(Order o) { jpa.save(OrderEntity.from(o)); } public Optional<Order> findById(OrderId id) { return jpa.findById(id.value()).map(OrderEntity::toDomain); }}Ayrıca JPA entity saxlamaq mapping kodu bahasına başa gəlir. Model sadədirsə, bir çox komanda domain sinfini bir neçə JPA annotasiyası ilə işarələməyi qəbul edir; bu, təmizlik qaydasını bilərəkdən əyir. Bu seçimi ADR-də yazın ki, təsadüf yox, qərar olsun.
Testdə nə qazanırsınız
Use case in-memory saxta obyektlərlə, Spring konteksti və container olmadan test edilə bilər:
class PlaceOrderUseCaseTest { @Test void savesOrderAfterAuthorization() { var repo = new InMemoryOrderRepository(); var payments = new FakePaymentGateway(); var useCase = new PlaceOrderUseCase(repo, payments); OrderId id = useCase.place(sampleCommand()); assertThat(repo.findById(id)).isPresent(); assertThat(payments.authorized()).containsKey(id); }}Domain qaydaları adi unit testlər alır. Adapter-lər məqsədyönlü slice və ya integrasiya testləri alır (controller üçün @WebMvcTest, repository üçün @DataJpaTest və ya real verilənlər bazası). Nəticə: testlərin sürətli çoxluğu və məhz inteqrasiya riskini hədəfləyən az sayda yavaş test. Test strategiyası məqaləsinə baxın.
Nə vaxt özünü doğruldur
- Dəyişən və sürətli test tələb edən real biznes qaydaları: qiymətləndirmə, uyğunluq, state machine.
- Eyni davranış üçün bir neçə giriş nöqtəsi (HTTP, message listener, planlı job).
- Dəyişmə və ya əvəzlənmə ehtimalı real olan infrastruktur: ödəniş provayderi, message broker, xarici API.
- Domain-in framework-lərdən uzun yaşadığı uzunömürlü sistemlər.
Nə vaxt mərasimdir
- Az məntiqi olan CRUD servis: controller, service və repository interfeysi artıq hər şeyi deyir. "Bu sətri saxla" üçün port, command, mapper və use-case siniflərinin əlavəsi heç nəyi qorumadan faylları çoxaldır.
- Yalnız bir implementasiyası olan və test double ehtiyacı olmayan interfeyslər. Spring Data repository-lər artıq abstraksiyadır.
- Hər obyekti default olaraq dörd layer arasında map etmək.
- Prototiplər və qısaömürlü alətlər.
Faydalı mövqe: sadə başlayın və port-u ağrını hiss etdiyiniz anda əlavə edin: test edilməsi çətin məntiq, ikinci giriş nöqtəsi və ya təcrid etmək istədiyiniz infrastruktur. Strukturu əsas domain-ə tətbiq edin, dəstəkləyici hissələri yüngül saxlayın. Eyni tətbiqin fərqli modulları qanuni olaraq fərqli dərəcədə mərasim istifadə edə bilər.
Tez-tez edilən səhvlər
- Domain-i layer pirogunun arxasına qoymaq, amma məntiqi yalnız data ötürən service-lərdə saxlamaq (anemik model); daxili halqa boş qalır.
- Framework tiplərinin, məsələn
Pageablevə ya JPA entity-lərin domain və ya use-case imzalarına sızması. - Məqsədyönlü use case-lər əvəzinə entity üzrə bir nəhəng
Service. - Ehtiyacla (
OrderEventPublisher) yox, texnologiya ilə (KafkaPublisher) adlandırılmış port-lar. - Web DTO-larının domain-ə çatmasına icazə vermək.
- Qaydanı yalnız razılaşma ilə tətbiq etmək. Arxitektura testi (məsələn ArchUnit ilə)
domainorg.springframeworkimport edəndə build-i uğursuz edə bilər.
Kompromislər
Daha çox tip və mapping verib testlənə bilənlik, əvəzlənə bilən infrastruktur və görünən nüvə alırsınız. Qiymət realdır: adaptasiya vaxtı, kodda naviqasiya zamanı dolayılıq və həddən artıq abstraksiya vasvasısı. Fayda domain mürəkkəbliyi və sistem ömrü ilə artır, nazik data-in/data-out servislər üçün azalır.
Dependency rule-u testlə tətbiq etmək
Yalnız wiki-də yazılmış qayda aşınır. Arxitektura testi onu icra olunan edir:
@AnalyzeClasses(packages = "com.example.ordering")class ArchitectureTest { @ArchTest static final ArchRule domain_is_framework_free = noClasses().that().resideInAPackage("..domain..") .should().dependOnClassesThat() .resideInAnyPackage("org.springframework..", "jakarta.persistence..");}Ən çox dəyəri qoruyan bir-iki qaydadan başlayın: domain-in framework-süz qalması və adapter-lərin bir-birini çağırmaması. Çox qayda test paketini bürokratik əngələ çevirir.
Mövcud kod bazasına tədricən gətirmək
Yenidən yazmayın. Real biznes məntiqi və artan ağrısı olan bir feature seçin. Qaydaları unit testli sadə domain siniflərinə çıxarın, ağrıdan infrastruktur asılılığının ətrafına port qoyun, qalanına toxunmayın. Eyni ağrı görünən yerdə təkrarlayın. Beləliklə struktur birdəfəlik miqrasiya kimi yox, real ehtiyac xətləri boyunca yayılır.
Spring-də bağlama və nüvəni təmiz saxlamaq
Spring kənarlarda qalır. Domain sinifləri sadə Java-dır və bean kimi işarələnmir. Use case praqmatik kompromis kimi @Service və @Transactional daşıya bilər, ya da application paketi annotasiyasız qalsın deyə onu adapter layındakı konfiqurasiya sinfindən qeydiyyata ala bilərsiniz. İkisi də ardıcıl olduqda məqbuldur. Əsas məsələ container-in təfərrüat olmasıdır: use case-i testdə new ilə yaratmaq mümkündür və o, eyni davranır. Xəta idarəsi də eyni istiqamətdə gedir: domain öz exception-larını atır, web adapter onları HTTP cavablarına çevirir, beləliklə nüvə status kodunun mövcudluğunu bilmir.
Sürətli qərar qaydası
Port əlavə etməzdən əvvəl üç sual verin. Onun arxasındakı məntiq biznes səbəbləri ilə dəyişirmi? Çağıranı real asılılıq olmadan test etməliyəmmi? İkinci implementasiya və ya giriş nöqtəsi real şəkildə yarana bilərmi? Üç cavab da xeyrdirsə, asılılığı birbaşa çağırın və davam edin. Portu sonra dəqiqələr içində əlavə etmək olar: dependency rule işləyən kodun üstünə asan tətbiq olunur, hər yerə yayıldıqdan sonra isə çətin geri alınır.
Yoxlama siyahısı
- Domain paketində framework import-u yoxdur; bu test ilə yoxlanır.
- Port-lar biznes ehtiyacı ilə adlanır və istifadəçilərinin yanında yerləşir.
- Use case-lər məqsədyönlüdür, hər biri bir niyyət.
- Adapter-lər biznes qaydası yox, tərcümə daşıyır.
- Əsas məntiq Spring və verilənlər bazası olmadan test olunur.
- Entity mapping qərarı yazılıb.
- Mərasimin dərəcəsi hər modulun mürəkkəbliyinə uyğundur.
Növbəti addım
- DDD aggregate-lər: mərkəzdəki domain-in formalaşdırılması.
- Spring Boot layering: daha sadə başlanğıc nöqtəsi.
- Senior backend developer roadmap: bu səviyyədə dizayn bacarıqları.