تركيب CompletableFuture
تركيب CompletableFuture
في الدرس السابق تعرّفت على أساسيات CompletableFuture: كيف تبدأ عملية حسابية غير متزامنة بـ supplyAsync وتحوّل نتيجتها بـ thenApply. غير أن البرامج الحقيقية نادرًا ما تحتوي على خطوة غير متزامنة واحدة معزولة. في أغلب الأحيان ستحتاج إلى ربط استدعاء غير متزامن بآخر، أو دمج نتيجتين من استدعاءين مستقلّين، أو انتظار مجموعة من الاستدعاءات قبل المتابعة. هذا هو ما تعنيه عملية التركيب — وإتقانها هو ما يميّز الكود غير المتزامن النظيف والصحيح عن الكود الذي يُعلِق الخيوط بصمت أو يبتلع الاستثناءات.
thenCompose — ربط الخطوات غير المتزامنة
thenCompose هو المقابل غير المتزامن لـ flatMap في التدفقات: يأخذ نتيجة CompletableFuture ويمرّرها إلى دالة تُعيد نسخة أخرى من CompletableFuture، ثم يُسطّح المرحلتين في مرحلة واحدة.
لماذا لا نستخدم thenApply؟ إذا أعادت دالة التحويل نفسها CompletableFuture فإن thenApply ستُعطيك CompletableFuture<CompletableFuture<T>> — مستقبل متداخل عديم الفائدة حتى تفكّه يدويًا. يقوم thenCompose بهذا الفكّ تلقائيًا.
thenCompose (بدون لاحقة Async) يعمل الكود التالي على نفس خيط المرحلة السابقة — غالبًا خيط من مجموعة fork-join المشتركة. استخدم thenComposeAsync مع Executor صريح حين تريد التحكم في مجموعة الخيوط التالية، مثلًا لإبقاء I/O المانع بعيدًا عن المجموعة المشتركة.
thenCombine — دمج مستقبلَين مستقلَّين
حين لا تعتمد عمليتان غير متزامنتان على بعضهما يجب تشغيلهما معًا ودمج نتيجتيهما فقط بعد اكتمالهما. هذا بالضبط ما يفعله thenCombine: يأخذ CompletableFuture ثانيًا وBiFunction، وينتظر اكتمال كليهما، ثم يدمج النتيجتين.
fetchUsdRate().thenCompose(_ -> fetchQuantity()) لتسلسلت استدعاءين مستقلّين وأهدرت وقت المعالج. استخدم thenCombine أو allOf ليبدأ كلاهما فورًا وتدفع فقط تكلفة الأبطأ منهما.
allOf — انتظار عدّة مستقبلات
يُعيد CompletableFuture.allOf(futures...) نسخة من CompletableFuture<Void> تكتمل حين تكتمل كل المستقبلات المُمرَّرة. ولأن نوع النتيجة Void، عادةً ما تجلب النتائج الفردية بنفسك بعد اكتمال allOf.
allOf نفسه بشكل استثنائي، لكن استدعاء join() على المستقبل الفاشل هو الطريقة للحصول على السبب الفعلي. عالج الاستثناءات على كل مستقبل فردي إذا احتجت تقريرًا مفصّلًا بالأخطاء.
معالجة الاستثناءات في خطوط الأنابيب المركّبة
تسري الاستثناءات عبر سلسلة CompletableFuture كـمرحلة فاشلة. كل مرحلة تالية لم تُسجّل معالجًا للأخطاء تُتجاوز، ويظهر الاستثناء عند استدعاء get() أو join() النهائي مُغلَّفًا في CompletionException. لديك ثلاث استراتيجيات للتعافي:
exceptionally(fn)— يعمل فقط عند فشل المرحلة؛ يستبدل الاستثناء بقيمة افتراضية.handle(BiFunction)— يعمل سواء نجحت المرحلة أم فشلت؛ يتيح فحص النتيجة والاستثناء معًا في مكان واحد.whenComplete(BiConsumer)— مثلhandleلكنه لا يحوّل النتيجة؛ مفيد للتسجيل أو التنظيف.
thenCompose وإحدى الخطوات قد تفشل، ألصق exceptionally أو handle عند النقطة التي يمكنك فيها التعافي بمعنى — عادةً مباشرةً بعد المرحلة التي قد تفشل، وليس فقط في النهاية. المعالج في النهاية يلتقط كل شيء لكنه يمنحك سياقًا أقل عن أي خطوة فشلت.
الجمع في نموذج واقعي
المثال التالي يجمع thenCompose وthenCombine وexceptionally في خط أنابيب واحد يُحضر معرّف المنتج بشكل غير متزامن، ثم يحمّل التفاصيل والسعر الحي بالتوازي، ويتعافى بأناقة عند أي فشل:
الخلاصة
- استخدم
thenComposeحين تكون الخطوة التالية بحد ذاتها غير متزامنة (لتجنّب التداخل المزدوج). - استخدم
thenCombineلدمج مستقبلَين مستقلَّين دون تسلسلهما. - استخدم
allOfللتوزيع على مجموعة ديناميكية وجمع كل النتائج بعد ذلك. - ألصق
exceptionallyأوhandleقريبًا من المرحلة التي قد تفشل لتعافٍ دقيق. - اختر دومًا الصيغة المتزامنة (Async) مع executor صريح حين يجب أن يبقى I/O المانع بعيدًا عن المجموعة المشتركة.