معمارية الخدمات المصغّرة وتصميمها

السياقات المحدودة وحدود الخدمات

18 دقيقة الدرس 3 من 12

السياقات المحدودة وحدود الخدمات

السؤال الأصعب في الميكروسيرفيسز ليس "كيف أنشر هذا؟" — بل "أين أقطع؟" إذا أخطأت في تحديد حدود الخدمات ستنتهي بـمونوليث موزّع: كل التكاليف التشغيلية للميكروسيرفيسز دون أي استقلالية. الأداة التي تُرشد تلك القطعة هي السياق المحدود (Bounded Context)، وهو مفهوم من كتاب Eric Evans حول Domain-Driven Design (DDD).

ما هو السياق المحدود؟

السياق المحدود هو منطقة في نطاق عملك تكون فيها نموذج معيّن محدّداً ومتسقاً. كلمة "محدود" هي الأساس: يمكن لنفس مصطلح الأعمال أن يعني شيئاً مختلفاً قليلاً في جزأين من المؤسسة. في منصة التجارة الإلكترونية، كلمة Customer تعني شخصاً يملك بيانات دفع لفريق الفوترة، وعنواناً لفريق الشحن، وسجل تصفح لفريق التوصيات. كل فريق يحتفظ بنموذجه الخاص — كلاس Customer خاص به بحقول وقواعد مختلفة.

رؤية DDD: نموذج واحد يحاول إرضاء المعاني الثلاثة ينتج كلاساً مترهلاً وغامضاً مليئاً بحقول قابلة للـ null ومنطق شرطي. تتيح السياقات المحدودة لكل فريق امتلاك نموذج دقيق وبسيط يناسب مسؤولياته تماماً.

يجب أن تُعيَّن الخدمة المصغرة لسياق محدود واحد أو تكون قسيمة منه. هذا يمنحك مبدأ عملياً موثوقاً: إذا كان التغيير في مفهوم أعمال واحد يتطلب تعديل كود في خدمتين، فالحدود على الأرجح خاطئة.

إيجاد الحدود: Event Storming عملياً

Event Storming هو تقنية ورشة تعاونية تسير فيها عبر النظام كسلسلة من أحداث النطاق — أشياء حدثت بصيغة الماضي: OrderPlaced وPaymentAuthorized وItemShipped. التجمّعات الطبيعية للأحداث والأوامر التي تُطلقها والتجمّعات (Aggregates) التي تمتلك الحالة تكشف السياقات المحدودة.

في نظام تجارة إلكترونية مبسّط، غالباً ما يكشف Event Storming عن هذه التجمّعات:

  • Catalog — ProductCreated, PriceUpdated, ProductDeactivated
  • Order — CartCreated, OrderPlaced, OrderCancelled
  • Payment — PaymentAuthorized, PaymentFailed, RefundIssued
  • Fulfillment — WarehousePickStarted, ItemShipped, DeliveryConfirmed
  • Identity — UserRegistered, PasswordChanged, AccountLocked

كل تجمّع هو مرشّح للسياق المحدود وبالتالي مرشّح لحدود الخدمة.

ترجمة السياق المحدود إلى خدمة Spring Boot

بعد تحديد الحدود، تمتلك الخدمة نموذج بياناتها باستقلالية كاملة. إليك جذر التجمّع الخاص بخدمة Order — لاحظ أنها لا تحمل كائن Product كاملاً ولا كائن User كاملاً. تخزّن فقط المعرّفات التي تحتاجها من السياقات المجاورة، إضافة إلى snapshot محلية لاسم المنتج وقت تقديم الطلب لتكون معزولة عن تغييرات Catalog.

// order-service/src/main/java/com/example/order/domain/Order.java @Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // مرجع إلى سياق Identity — ID فقط، لا join عبر الخدمات private Long customerId; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) private List<OrderLine> lines = new ArrayList<>(); @Enumerated(EnumType.STRING) private OrderStatus status; private Instant placedAt; // ... المنشئات وطرق النطاق والمحدّدات (getters) } @Embeddable public class OrderLine { private Long productId; // ID سياق Catalog فقط private String productNameSnapshot; // نسخة محلية — تتجنب الوصلات عبر السياقات private int quantity; private BigDecimal unitPriceSnapshot; }
خزّن المعرّفات لا الكائنات. الاحتفاظ بمفتاح خارجي لنطاق خدمة أخرى مقبول. لكن الاحتفاظ بـ @ManyToOne في JPA يُعيّن لجدول قاعدة بيانات خدمة أخرى غير مقبول — فهو يقرن مخططات قاعدة البيانات ويُبطل الغرض من النشر المنفصل.

خريطة السياق: العلاقات بين الخدمات

لا تعيش السياقات المحدودة في عزلة؛ فهي تتفاعل. يُسمّي DDD أنماط العلاقة بينها. الأكثر شيوعاً في الميكروسيرفيسز هما:

  • Customer/Supplier: سياق واحد (upstream) ينشر البيانات؛ والآخر (downstream) يستهلكها. الـ upstream يحدد العقد؛ الـ downstream يتكيّف. مثال: Catalog (upstream) ينشر أحداث المنتجات؛ Order (downstream) يشترك ويحتفظ بنسخته المحلية من المنتج.
  • Anti-Corruption Layer (ACL): الـ downstream يُغلّف نموذج الـ upstream في طبقة ترجمة حتى يبقى نموذجه الداخلي نظيفاً. في الكود عادةً ما يكون هذا كلاس مُعيّن للتحويل أو واجهة منفذ مخصصة.
// order-service: Anti-Corruption Layer لأحداث Catalog @Component public class CatalogEventAdapter { private final ProductSnapshotRepository snapshots; public CatalogEventAdapter(ProductSnapshotRepository snapshots) { this.snapshots = snapshots; } // يُترجم ProductUpdatedEvent الخاص بـ Catalog إلى النموذج الداخلي لـ Order @KafkaListener(topics = "catalog.product-updated") public void onProductUpdated(ProductUpdatedEvent event) { ProductSnapshot snap = snapshots .findById(event.getProductId()) .orElse(new ProductSnapshot(event.getProductId())); snap.setName(event.getName()); snap.setCurrentPrice(event.getPrice()); snapshots.save(snap); } }

مبادئ التماسك والاقتران

قاعدة عملية مفيدة عند تقييم الحدود: الكود الذي يتغير معاً ينتمي إلى المكان نفسه (تماسك عالٍ)؛ والكود الذي لا يحتاج لمعرفة الآخر يجب ألّا يكون مرتبطاً به (اقتران منخفض). طبّقها على مرشّحي الخدمات:

  • إذا كانت طلبات الميزات تمسّ دائماً خدمة واحدة فقط، فالحدود جيدة.
  • إذا كان كل سبرينت يتضمن نشراً منسّقاً لثلاث خدمات من أجل قصة واحدة، ادمجها.
  • إذا نمت خدمة لتصبح آلاف الأسطر وبات فريقها يتجادل حول الملكية، قسّمها.
تجنب تقسيم الخدمات بحسب كيان واحد. تقسيم كل كيان لخدمة ("UserService" و"ProductService" و"OrderService" كلٌّ يمتلك كياناً واحداً في JPA) يبدو أنيقاً لكنه غالباً ما يعكس مخطط قاعدة البيانات لا العمل التجاري. الخدمات المقطوعة بهذه الطريقة تنتهي كطبقات رفيعة يجب تنسيقها معاً لكل عملية — نمط مضاد يُعرف بالمونوليث الموزّع. دائماً اقطع حسب القدرة التجارية لا حسب جدول قاعدة البيانات.

الآثار الأمنية للحدود

حدود الخدمات هي أيضاً حدود أمنية. يجب أن تتحقق كل خدمة من الطلبات وتفوّضها باستقلالية — لا تثق ضمنياً أبداً بالبيانات القادمة من خدمة أخرى. في Spring Security 6، النمط الشائع هو تمرير JWT صادر من خدمة Identity والسماح لكل خدمة downstream بالتحقق منه باستقلالية:

// order-service/src/main/java/com/example/order/config/SecurityConfig.java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } }

تتحقق كل خدمة من توقيع JWT مقابل المفتاح العام لخدمة Identity (مستردّ من نقطة نهاية JWKS الخاصة بها). خدمة Order تستخرج مطالبة customerId — لا تستدعي قاعدة بيانات خدمة Identity مباشرة أبداً. هذا هو التعبير الأمني عن مبدأ السياق المحدود: كل خدمة تثق بالرمز المميّز، لا بجلسة مشتركة أو استدعاء قاعدة بيانات عبر الخدمات.

الخلاصة

السياق المحدود هو منطقة في نموذج نطاقك تكون فيها المصطلحات والقواعد متسقة. عيّن كل خدمة مرشحة لسياق محدود واحد، اجعلها تمتلك بياناتها باستقلالية (خزّن المعرّفات الخارجية لا الكائنات المشتركة)، نمذج العلاقات بين السياقات بخرائط السياق وطبقات Anti-Corruption، واستخدم تمرير JWT كجسر أمني. احصل على هذه الحدود صحيحة من البداية — إعادة هيكلتها لاحقاً هي إحدى أكلف العمليات في بنية الميكروسيرفيسز.