Optional وجافا الحديثة

أفضل ممارسات Optional والأنماط المضادة

15 دقيقة الدرس 5 من 13

أفضل ممارسات Optional والأنماط المضادة

أُضيفت Optional إلى Java 8 بغرض محدود وواضح: أن تكون نوع إرجاع للتوابع التي قد تُنتج قيمة أو لا تُنتجها. على الرغم من هذه المهمة الضيّقة، فإنها من أكثر واجهات برمجة التطبيقات سوء استخدامًا في عالم Java. يرسم هذا الدرس خريطة دقيقة لأماكن نفع Optional وأماكن ضررها مع الشرح الكامل لمنطق كل قاعدة.

القاعدة الأولى: Optional نوع إرجاع فقط — لا شيء آخر تقريبًا

كان مصمّمو Optional صريحين: صُمّمت لتُستخدم نوع إرجاع فقط. هذا التوجّه يشكّل كل إرشادٍ يلي.

توثيق Javadoc يقول حرفيًا: "Optional مخصّصة أساسًا للاستخدام كنوع إرجاع للتوابع حين تكون هناك حاجة واضحة لتمثيل «لا نتيجة»، وحيث قد يُفضي استخدام null إلى أخطاء."

النمط المضاد الأول: Optional كحقل في الكلاس

تخزين Optional في حقل داخل الكلاس يبدو بريئًا لكنه يُسبّب مشكلات حقيقية. فـOptional لا تُنفّذ واجهة Serializable، لذا حال ما تُسلسل كلاسًا يحتوي حقل Optional — سواء كان كيانًا في JPA أو كائنًا مُخزّنًا في ذاكرة التخزين المؤقت أو حمولة رسالة — ستحصل على فشل في وقت التشغيل. يُضاف إلى ذلك إهدار الذاكرة: كل تخصيص حقل يلفّ القيمة في كائن إضافي على الكومة.

// خطأ: Optional كحقل public class User { private Optional<String> middleName; // غير قابل للتسلسل، مُهدر للذاكرة public Optional<String> getMiddleName() { return middleName; } } // صحيح: حقل nullable، و Optional فقط على المُحصِّل public class User { private String middleName; // قد يكون null، مُخزَّن بنظافة public Optional<String> getMiddleName() { return Optional.ofNullable(middleName); // التغليف عند الإرجاع } }

القاعدة بسيطة: احتفظ بالحقل كمرجع nullable عادي؛ وأنتج Optional عند حدّ المُحصِّل ليتمكّن المستدعون من التسلسل دون فحوصات null.

النمط المضاد الثاني: Optional كمعامل للتابع

قبول Optional كمعامل يُجبر كل مستدعٍ على تغليف قيمته — بمن فيهم من لديهم القيمة دائمًا — دون أي فائدة. كما يُسرّب قرار التنفيذ (أن المعامل قد يكون غائبًا) إلى سطح واجهة برمجة التطبيقات العامة.

// خطأ: يُجبر المستدعين على التغليف حتى حين يملكون القيمة فعلًا public void sendEmail(String to, Optional<String> subject) { String s = subject.orElse("(no subject)"); // ... } // المستدعي مضطر للكتابة: sendEmail("alice@example.com", Optional.of("Hello")); sendEmail("bob@example.com", Optional.empty());
// صحيح: تحميل زائد مُركّز (أو معامل nullable مع حارس) public void sendEmail(String to) { sendEmail(to, "(no subject)"); } public void sendEmail(String to, String subject) { // subject مضمون غير null هنا }

التحميل الزائد أكثر قراءةً عند موقع الاستدعاء. إن كان عدد المعاملات الاختيارية كبيرًا، استخدم نمط البنّاء (Builder) أو كائن المعاملات (Parameter Object).

النمط المضاد الثالث: Optional داخل المجموعات

تخزين Optional داخل مجموعة — كـList<Optional<String>> أو Map<String, Optional<String>> — يكاد يكون دائمًا نموذجًا خاطئًا. المجموعات تُعبّر بالفعل عن الغياب من خلال أعرافها: العنصر إما موجود في القائمة أو ليس موجودًا؛ مفتاح الخريطة إما يُشير لقيمة أو لا يُشير.

// خطأ: ضوضاء وتخصيصات إضافية List<Optional<String>> results = queryMany(); for (Optional<String> r : results) { r.ifPresent(System.out::println); } // صحيح: صفّي nulls وتعامل مع القيم العادية مباشرةً List<String> results = queryMany() .stream() .filter(Objects::nonNull) .collect(Collectors.toList()); results.forEach(System.out::println);
Stream.flatMap مع Optional هو الاستثناء المشروع الوحيد. إن كان لديك Stream<Optional<T>> — ربما من واجهة برمجة لا تتحكم فيها — فسطّحه بنظافة:
Stream<Optional<String>> maybes = getMaybes(); List<String> values = maybes .flatMap(Optional::stream) // Java 9+: Optional.stream() تُعيد 0 أو عنصرًا واحدًا .collect(Collectors.toList());
هذه خطوة تحويل عند حدّ، لا قرار تصميم لتخزين Optionals داخل المجموعة.

النمط المضاد الرابع: استدعاء .get() بلا حارس

استدعاء optional.get() دون التحقق أولًا من isPresent() يُلقي NoSuchElementException حين تكون القيمة غائبة — وهو بالضبط الخطأ الذي أُنشئت Optional لمنعه. هذا النمط شائع بين المطوّرين الذين يتعاملون مع Optional كمرجع null منمّق.

Optional<String> name = findName(); // خطأ: نفس خطر إلغاء مرجعية null String value = name.get(); // يُلقي استثناءً إن كانت فارغة // صحيح: استخدم الواجهة التصريحية String value = name.orElse("default"); String value = name.orElseGet(() -> computeDefault()); String value = name.orElseThrow(() -> new EntityNotFoundException("الاسم غير موجود"));
لا تستدعِ get() أبدًا على Optional لم تتحقق من وجود قيمتها. إن كنت تكتب if (opt.isPresent()) { opt.get() }، أعد الكتابة باستخدام opt.ifPresent(...) أو opt.map(...) — النهج التصريحي أكثر أمانًا وأوضح قراءةً.

النمط المضاد الخامس: تغليف الاستثناءات بـ Optional

استخدام Optional لابتلاع استثناء مُتحقَّق منه بصمت يجعل الإخفاقات غير مرئية ولا يمكن تتبّعها. إرجاع Optional.empty() لا يُعطي المستدعي أي طريقة لتمييز "غير موجود" عن "انتهت مهلة قاعدة البيانات".

// خطأ: المستدعي لا يستطيع معرفة سبب الفراغ public Optional<User> loadUser(long id) { try { return Optional.of(repository.find(id)); } catch (Exception e) { return Optional.empty(); // الخطأ يُتجاهل بصمت } } // صحيح: دع الاستثناء ينتشر، أو ألقِ استثناء نطاق public Optional<User> loadUser(long id) throws DataAccessException { return Optional.ofNullable(repository.find(id)); }

متى تكون 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 لأن التدفقات الفارغة حالة حقيقية وليست خطأ.
// استخدام جيد: مستودع، تحويل متسلسل، مستهلك نهائي userRepository.findById(userId) // Optional<User> .map(User::getAddress) // Optional<Address> .map(Address::getCity) // Optional<String> .map(String::toUpperCase) // Optional<String> .ifPresent(city -> log.info("City: {}", city));

ملاحظة على الأداء

كل نسخة Optional كائن قصير العمر على الكومة. في الحلقات المضغوطة أو مسارات الكود عالية الإنتاجية يُضيف ذلك ضغطًا على مجمّع المهملات. يمكن للكلمة المفتاحية var في Java 10+ تقليل الإطناب، وتسعى الأنواع القيمية في Java 18+ (مشروع Valhalla، غير نهائي بعد) إلى إلغاء التخصيص كليًا. في الوقت الراهن، في المسارات الساخنة، لا يزال الإرجاع القابل للـnull هو الخيار الصحيح.

الخلاصة

استخدم Optional نوعًا للإرجاع لجعل غياب القيمة صريحًا في عقد واجهتك البرمجية. احتفظ بالحقول وعناصر المجموعات كمراجع nullable عادية. لا تستخدم Optional معاملًا — استخدم التحميل الزائد أو البنّاء عوضًا عن ذلك. لا تستدعِ get() بلا حارس؛ فضّل orElse وorElseGet وorElseThrow. هذه القيود ليست اعتباطية: تنبثق من افتقار Optional للـSerializability، وتكلفة تخصيصها، والإرباك الذي تُسبّبه في واجهة برمجة التطبيقات حين تُستخدم خارج دورها المقصود.