أفضل ممارسات Optional والأنماط المضادة
أفضل ممارسات Optional والأنماط المضادة
أُضيفت Optional إلى Java 8 بغرض محدود وواضح: أن تكون نوع إرجاع للتوابع التي قد تُنتج قيمة أو لا تُنتجها. على الرغم من هذه المهمة الضيّقة، فإنها من أكثر واجهات برمجة التطبيقات سوء استخدامًا في عالم Java. يرسم هذا الدرس خريطة دقيقة لأماكن نفع Optional وأماكن ضررها مع الشرح الكامل لمنطق كل قاعدة.
القاعدة الأولى: Optional نوع إرجاع فقط — لا شيء آخر تقريبًا
كان مصمّمو Optional صريحين: صُمّمت لتُستخدم نوع إرجاع فقط. هذا التوجّه يشكّل كل إرشادٍ يلي.
النمط المضاد الأول: Optional كحقل في الكلاس
تخزين Optional في حقل داخل الكلاس يبدو بريئًا لكنه يُسبّب مشكلات حقيقية. فـOptional لا تُنفّذ واجهة Serializable، لذا حال ما تُسلسل كلاسًا يحتوي حقل Optional — سواء كان كيانًا في JPA أو كائنًا مُخزّنًا في ذاكرة التخزين المؤقت أو حمولة رسالة — ستحصل على فشل في وقت التشغيل. يُضاف إلى ذلك إهدار الذاكرة: كل تخصيص حقل يلفّ القيمة في كائن إضافي على الكومة.
القاعدة بسيطة: احتفظ بالحقل كمرجع nullable عادي؛ وأنتج Optional عند حدّ المُحصِّل ليتمكّن المستدعون من التسلسل دون فحوصات null.
النمط المضاد الثاني: Optional كمعامل للتابع
قبول Optional كمعامل يُجبر كل مستدعٍ على تغليف قيمته — بمن فيهم من لديهم القيمة دائمًا — دون أي فائدة. كما يُسرّب قرار التنفيذ (أن المعامل قد يكون غائبًا) إلى سطح واجهة برمجة التطبيقات العامة.
التحميل الزائد أكثر قراءةً عند موقع الاستدعاء. إن كان عدد المعاملات الاختيارية كبيرًا، استخدم نمط البنّاء (Builder) أو كائن المعاملات (Parameter Object).
النمط المضاد الثالث: Optional داخل المجموعات
تخزين Optional داخل مجموعة — كـList<Optional<String>> أو Map<String, Optional<String>> — يكاد يكون دائمًا نموذجًا خاطئًا. المجموعات تُعبّر بالفعل عن الغياب من خلال أعرافها: العنصر إما موجود في القائمة أو ليس موجودًا؛ مفتاح الخريطة إما يُشير لقيمة أو لا يُشير.
Stream<Optional<T>> — ربما من واجهة برمجة لا تتحكم فيها — فسطّحه بنظافة:
النمط المضاد الرابع: استدعاء .get() بلا حارس
استدعاء optional.get() دون التحقق أولًا من isPresent() يُلقي NoSuchElementException حين تكون القيمة غائبة — وهو بالضبط الخطأ الذي أُنشئت Optional لمنعه. هذا النمط شائع بين المطوّرين الذين يتعاملون مع Optional كمرجع null منمّق.
get() أبدًا على Optional لم تتحقق من وجود قيمتها. إن كنت تكتب if (opt.isPresent()) { opt.get() }، أعد الكتابة باستخدام opt.ifPresent(...) أو opt.map(...) — النهج التصريحي أكثر أمانًا وأوضح قراءةً.
النمط المضاد الخامس: تغليف الاستثناءات بـ Optional
استخدام Optional لابتلاع استثناء مُتحقَّق منه بصمت يجعل الإخفاقات غير مرئية ولا يمكن تتبّعها. إرجاع Optional.empty() لا يُعطي المستدعي أي طريقة لتمييز "غير موجود" عن "انتهت مهلة قاعدة البيانات".
متى تكون Optional الأداة الصحيحة فعلًا
لكل قاعدة مكانها. إليك أين تكون Optional الخيار الصحيح حقًا:
- توابع بحث المستودعات —
Optional<User> findById(long id)اصطلاحي؛ يُشير صراحةً إلى أن الصف قد لا يوجد. - مُحصِّلات الإعداد —
Optional<String> getProperty(String key)أوضح من إرجاعnullلمفتاح غير موجود. - تسلسل التحويلات — حين تريد حساب شيء فقط إن نجحت خطوة سابقة، يقرأ تسلسل
map/flatMap/filterأكثر وضوحًا بكثير من كتل if-null-else المتداخلة. - خطوات نهاية المسار — توابع Stream كـ
findFirst()وreduce()وmin()/max()تُعيدOptionalلأن التدفقات الفارغة حالة حقيقية وليست خطأ.
ملاحظة على الأداء
كل نسخة Optional كائن قصير العمر على الكومة. في الحلقات المضغوطة أو مسارات الكود عالية الإنتاجية يُضيف ذلك ضغطًا على مجمّع المهملات. يمكن للكلمة المفتاحية var في Java 10+ تقليل الإطناب، وتسعى الأنواع القيمية في Java 18+ (مشروع Valhalla، غير نهائي بعد) إلى إلغاء التخصيص كليًا. في الوقت الراهن، في المسارات الساخنة، لا يزال الإرجاع القابل للـnull هو الخيار الصحيح.
الخلاصة
استخدم Optional نوعًا للإرجاع لجعل غياب القيمة صريحًا في عقد واجهتك البرمجية. احتفظ بالحقول وعناصر المجموعات كمراجع nullable عادية. لا تستخدم Optional معاملًا — استخدم التحميل الزائد أو البنّاء عوضًا عن ذلك. لا تستدعِ get() بلا حارس؛ فضّل orElse وorElseGet وorElseThrow. هذه القيود ليست اعتباطية: تنبثق من افتقار Optional للـSerializability، وتكلفة تخصيصها، والإرباك الذي تُسبّبه في واجهة برمجة التطبيقات حين تُستخدم خارج دورها المقصود.