البرمجة الجانبية في Spring

ما هو البرمجة الموجهة بالجوانب؟

18 دقيقة الدرس 1 من 13

ما هو البرمجة الموجهة بالجوانب؟

تُصاب كل تطبيقات Spring Boot غير البسيطة في نهاية المطاف بالمشكلة ذاتها: تظهر المخاوف المتكررة ذاتها — التسجيل (Logging) والتحقق الأمني ومراقبة الأداء وإدارة المعاملات وسجلات التدقيق — مبعثرةً عبر عشرات الفئات المختلفة. تتضخم كل دالة خدمة بطبقة من الكود المكرور الذي لا علاقة له بمنطق العمل، لكنه يجب أن يكون حاضرًا في كل مكان. البرمجة الموجهة بالجوانب (AOP) هي المنهجية التي تُشخّص هذه المشكلة وتقدم لها حلًا مبدئيًا.

المخاوف العابرة للقطاعات: جذر المشكلة

المخاوف العابرة للقطاعات (Cross-Cutting Concerns) هي السلوكيات التي تؤثر في وحدات عديدة لكنها لا تنتمي إلى أي وحدة بعينها. الأمثلة الكلاسيكية في تطبيقات Spring هي:

  • التسجيل (Logging) — تسجيل الدخول والخروج من الدوال، والمعطيات وقيم الإرجاع والاستثناءات.
  • الأمان — التحقق من أن المُستدعي يمتلك الدور المطلوب قبل تنفيذ الدالة.
  • إدارة المعاملات — فتح معاملة قبل استدعاء الخدمة وإتمامها أو التراجع عنها بعده.
  • مراقبة الأداء — قياس المدة التي تستغرقها الدالة والتنبيه عند تجاوز حد معين.
  • التدقيق (Auditing) — كتابة سجل بمن فعل ماذا ومتى في جدول تدقيق مستقل.
  • التخزين المؤقت (Caching) — التحقق من الكاش قبل تنفيذ الدالة وتخزين النتيجة بعدها.

لا يُعدّ أي من هذه المخاوف منطق العمل الخاص بأي فئة معينة. يجب أن تُعنى خدمة PaymentService بمعالجة المدفوعات، لا ببدء مؤقتات والتحقق من الأدوار وفتح معاملات وكتابة سجلات. لكن بدون AOP، تنتهي كل دالة بحمل هذا الثقل كله.

مشكلة التكرار في الواقع العملي

انظر إلى طبقة خدمة واقعية قبل AOP. ثلاث دوال خدمة تؤدي مهامًا مختلفة تمامًا لكنها تتشارك في هيكل مكرر شبه متطابق:

@Service public class OrderService { private static final Logger log = LoggerFactory.getLogger(OrderService.class); public Order placeOrder(Cart cart) { log.info("placeOrder called with cart id={}", cart.getId()); long start = System.currentTimeMillis(); try { // منطق العمل الفعلي Order order = buildOrderFromCart(cart); orderRepository.save(order); log.info("placeOrder completed in {}ms", System.currentTimeMillis() - start); return order; } catch (Exception ex) { log.error("placeOrder failed", ex); throw ex; } } public void cancelOrder(Long orderId) { log.info("cancelOrder called with orderId={}", orderId); long start = System.currentTimeMillis(); try { // منطق العمل الفعلي Order order = orderRepository.findById(orderId).orElseThrow(); order.setStatus(OrderStatus.CANCELLED); orderRepository.save(order); log.info("cancelOrder completed in {}ms", System.currentTimeMillis() - start); } catch (Exception ex) { log.error("cancelOrder failed", ex); throw ex; } } public List<Order> getOrdersForUser(Long userId) { log.info("getOrdersForUser called with userId={}", userId); long start = System.currentTimeMillis(); try { List<Order> orders = orderRepository.findByUserId(userId); log.info("getOrdersForUser completed in {}ms", System.currentTimeMillis() - start); return orders; } catch (Exception ex) { log.error("getOrdersForUser failed", ex); throw ex; } } }

يستحوذ هيكل التسجيل والتوقيت على ما يقارب نصف أسطر كل دالة. الآن اضرب هذا عبر PaymentService وInventoryService وShippingService وعشرين خدمة أخرى — ثم تخيّل أن تنسيق التسجيل تغيّر، أو أنك تحتاج إلى إضافة التدقيق فوق التسجيل الموجود. ستضطر إلى فتح كل دالة وتعديلها يدويًا.

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

لماذا لا تكفي البرمجة الكائنية وحدها لحل المشكلة

قد تتساءل: هل يمكن لفئة أساسية أو دالة مساعدة أن تحل هذا؟ الجواب: جزئيًا فحسب، وبتكلفتها الخاصة.

  • الوراثة تجبر كل فئات الخدمة على مشاركة فئة أساسية تخلط مخاوف التسجيل بالتسلسل الهرمي للأنواع. تدعم Java الوراثة الأحادية فحسب، لذا لا يمكن لأي فئة تمتد شيئًا آخر أن تمتد فئة التسجيل الأساسية أيضًا.
  • التفويض والدوال المساعدة تقلل التكرار لكن نقاط الاستدعاء لا تزال تحتاج إلى كتابتها يدويًا في كل دالة. لا تُغني عن ضرورة لمس كل دالة بشكل منفرد.
  • نمط المُزيِّن (Decorator) — تغليف كل خدمة في مُزيِّن يتولى التسجيل — هو أقرب ما تصله البرمجة الكائنية من AOP. يعمل هذا، لكنه يستلزم كتابة فئة مُزيِّن منفصلة لكل واجهة خدمة وتوصيلها وإبقائها متزامنة مع الأصل. Spring AOP يفعل كل هذا تلقائيًا.

تُهيكل البرمجة الكائنية الكود حول الأسماء — الكائنات ومسؤولياتها. المخاوف العابرة للقطاعات تقطع هذه الأسماء بطريقة لا تملك البرمجة الكائنية آلية نظيفة للتعبير عنها. يُقدم AOP آلية هيكلة تكاملية تتمحور حول متى وأين يجب أن يحدث شيء ما في تدفق التنفيذ.

ما يفعله AOP بدلًا من ذلك

يتيح لك AOP كتابة السلوك العابر للقطاعات مرة واحدة، في فئة مخصصة تُسمى الجانب (Aspect)، ثم الإعلان — باستخدام تعبير موجز — عن أين ينبغي تطبيق هذا السلوك في التطبيق. يقوم Spring AOP بعد ذلك بنسج الجانب في تدفق التنفيذ تلقائيًا دون لمس الفئات الأصلية.

تبدو خدمة OrderService ذاتها مع AOP الذي يتولى التسجيل والتوقيت هكذا:

@Service public class OrderService { public Order placeOrder(Cart cart) { Order order = buildOrderFromCart(cart); orderRepository.save(order); return order; } public void cancelOrder(Long orderId) { Order order = orderRepository.findById(orderId).orElseThrow(); order.setStatus(OrderStatus.CANCELLED); orderRepository.save(order); } public List<Order> getOrdersForUser(Long userId) { return orderRepository.findByUserId(userId); } }

تحتوي كل دالة الآن على منطق العمل فحسب. يُعبَّر عن التسجيل والتوقيت مرة واحدة في جانب ويُطبَّق على جميع دوال الخدمة بتعبير نقطة قطع واحد — والذي ستكتبه في الدرس الخامس.

AOP ليس بديلًا سحريًا عن التصميم الجيد. إنه أداة لفصل المخاوف البنية التحتية (التسجيل والأمان والمعاملات) عن منطق العمل فصلًا نظيفًا. يستخدمه Spring داخليًا: فـ@Transactional و@Cacheable وأمان المستوى الدالي في Spring Security كلها مُنفَّذة كجوانب Spring AOP.

AOP كامتداد طبيعي لـ Spring IoC

أنت تعرف مسبقًا كيف يعمل Spring IoC وحقن التبعيات. يبني AOP على هذا الأساس مباشرةً. يعمل Spring AOP بإنشاء وكيل (Proxy) حول الـ Bean الخاصة بك — فعندما يطلب Bean آخر من Spring كائن OrderService، يمنحه Spring وكيلًا يعترض استدعاءات الدوال وينفذ أي نصيحة (Advice) مطبّقة قبل التفويض إلى الكائن الحقيقي. لهذا السبب لا يستطيع Spring AOP اعتراض سوى استدعاءات الدوال على الـ Beans المُدارة بواسطة Spring (الاستدعاءات الداخلية لا تُعترض لأنها تتجاوز الوكيل). ستستكشف هذا بعمق في الدرس التاسع؛ أما الآن فالفهرة المهمة هي أن AOP يندمج مع حاوية IoC بشكل طبيعي — لا يلزم سوى تبعية واحدة وتعليقة توضيحية.

الأثر في الواقع: لماذا يهمنا هذا

في بيئات Spring Boot الإنتاجية، يقف AOP وراء العديد من مزايا المنصة التي تستخدمها يوميًا:

  • @Transactional على دالة خدمة — يُدار بواسطة TransactionInterceptor، وهو نصيحة Spring AOP.
  • @Cacheable على دالة مستودع — يُدار بواسطة CacheInterceptor.
  • @PreAuthorize("hasRole('ADMIN')") — يُدار بواسطة MethodSecurityInterceptor في Spring Security.
  • @Async — يُعترض الدالة وتُرسل محتواها إلى تجمع خيوط.

إن فهم AOP يتيح لك التوقف عن التعامل مع هذه الميزات كصناديق سوداء والبدء في كتابة سلوكياتك العابرة للقطاعات بالأناقة والقوة ذاتها.

الخلاصة

المخاوف العابرة للقطاعات — كالتسجيل والأمان والمعاملات والتدقيق والتخزين المؤقت — هي سلوكيات يجب أن تظهر في كل أنحاء التطبيق لكنها لا تنتمي إلى أي فئة عمل بعينها. عندما تُعالج بالنسخ واللصق تُنشئ تكرارًا هائلًا وعبء صيانة هش. تقلل الأساليب الكائنية كالوراثة والتفويض المشكلة دون حلها. تحلها البرمجة الموجهة بالجوانب بكتابة السلوك مرة واحدة في جانب والإعلان عن أماكن تطبيقه بتعبير نقطة قطع، مما يُبقي فئات العمل نقية. يدمج Spring AOP ذلك في حاوية IoC من خلال إنشاء وكيل تلقائي، وهو الآلية الكامنة وراء @Transactional و@Cacheable وأمان المستوى الدالي في Spring Security. في الدرس القادم ستتعلم المفردات الدقيقة — نقطة الوصل والنقطة القاطعة والنصيحة والجانب والنسج — التي تجعل مفاهيم AOP ملموسة وقابلة للتطبيق.