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

مشروع: إعادة هيكلة الكود إلى Java الحديثة

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

مشروع: إعادة هيكلة الكود إلى Java الحديثة

طوال هذا الدليل تعلّمت كل ميزة حديثة بمعزل عن غيرها. الآن حان وقت جمعها معًا. في هذا الدرس المشروع ستبدأ بقطعة كود واقعية مطوّلة من حقبة ما قبل Java 17، وستعيد هيكلتها تدريجيًا باستخدام Optional وvar وتعبيرات switch وعمليات instanceof المحسّنة. الهدف ليس استخدام كل ميزة في كل مكان، بل تحديد الأداة المناسبة لكل نقطة ألم وفهم المقايضات قبل الالتزام بها.

الكود الابتدائي

فيما يلي مقطع صغير لمعالجة الطلبات كما قد تكتبه بأسلوب Java 8. صحيح من الناحية الوظيفية، لكنه مزدحم: فحوصات null في كل مكان، وتحويلات نوع صريحة بعد instanceof، وسلسلة طويلة من if/else if لتعيين الحالات.

// --- قبل: أسلوب ما قبل Java 17 --- import java.util.HashMap; import java.util.Map; public class OrderProcessor { private Map<Integer, String> orderDb = new HashMap<>(); public OrderProcessor() { orderDb.put(1, "Alice:PENDING:99.99"); orderDb.put(2, "Bob:SHIPPED:149.50"); orderDb.put(3, "Carol:DELIVERED:220.00"); } // تُعيد null عندما لا يُوجد الطلب public String findRaw(int id) { return orderDb.get(id); } public String getCustomerName(int id) { String raw = findRaw(id); if (raw == null) { return "UNKNOWN"; } String[] parts = raw.split(":"); return parts[0]; } public String mapStatus(Object statusObj) { if (statusObj instanceof String) { String status = (String) statusObj; if (status.equals("PENDING")) { return "Awaiting processing"; } else if (status.equals("SHIPPED")) { return "On its way"; } else if (status.equals("DELIVERED")) { return "Delivered"; } else { return "Unknown status"; } } return "Invalid input"; } public static void main(String[] args) { OrderProcessor processor = new OrderProcessor(); System.out.println(processor.getCustomerName(1)); // Alice System.out.println(processor.getCustomerName(99)); // UNKNOWN System.out.println(processor.mapStatus("SHIPPED")); // On its way System.out.println(processor.mapStatus(42)); // Invalid input } }

هذا الكود يُجمَّع ويعمل. لكن توجد أربع نقاط ألم تستحق المعالجة:

  1. تُعيد findRaw قيمة null — يجب على المستدعين تذكّر الفحص، ولا شيء في توقيع النوع يُحذّرهم.
  2. المتغيرات المحلية مثل String raw وString[] parts تكرّر معلومات النوع التي يعرفها المُجمِّع بالفعل.
  3. دالة mapStatus تستخدم تحويل نوع قديم (String) statusObj بعد فحص instanceof — ازدواجية غير ضرورية.
  4. سلسلة if/else if لتعيين الحالات مطوّلة ولا تستفيد من التحقق من الشمولية.

الخطوة 1 — استبدال إرجاع null بـ Optional

غيّر findRaw لتُعيد Optional<String>. هذا يجعل الغياب صريحًا في النوع ويُجبر المستدعين على التعامل معه:

import java.util.Optional; public Optional<String> findRaw(int id) { return Optional.ofNullable(orderDb.get(id)); } public String getCustomerName(int id) { return findRaw(id) .map(raw -> raw.split(":")[0]) .orElse("UNKNOWN"); }

اختفى فحص null. تُقرأ السلسلة هكذا: إذا وُجدت قيمة فقسّمها وخذ الفهرس 0؛ وإلّا استخدم "UNKNOWN". التحويل والقيمة الافتراضية في مكان واحد، مما يجعل القراءة أسهل من فحص if (raw == null) المتناثر.

لماذا لا نستخدم isPresent() فقط؟ كتابة if (opt.isPresent()) { … opt.get() … } يُعيد النمط الأمري نفسه الذي كنّا نستخدمه مع null. يُفضَّل استخدام map وflatMap وorElse — فهي تحافظ على النية وظيفية وتتركّب بشكل نظيف.

الخطوة 2 — تقليل ضجيج الأنواع بـ var

داخل الدوال ذات السلاسل الأطول، تُضيف تعليقات الأنواع المتكررة ضخامة دون فائدة. استخدم var للمتغيرات المحلية عندما يكون النوع واضحًا من الجانب الأيمن للإسناد:

public void printOrderSummary(int id) { var rawOpt = findRaw(id); // Optional<String> var display = rawOpt.orElse("order not found"); // String var parts = display.split(":"); // String[] System.out.println("Customer : " + parts[0]); if (parts.length > 1) { System.out.println("Status : " + parts[1]); } }
لا تُطبّق var بإفراط. هي مثالية عندما يكون النوع واضحًا من السياق (استدعاءات المُنشئ، طرق المصنع، سلاسل الدفق). تجنّب استخدامها عندما يكون المُهيِّئ استدعاءً لدالة غامضة لا يظهر نوع إرجاعها من اسمها — ستفقد القيمة التوثيقية للنوع الصريح.

الخطوة 3 — تحديث instanceof بمتغيرات النمط

التحويل القديم (String) statusObj بعد instanceof String مجرد شيفرة نمطية. يسمح Java 16+ بربط نتيجة التحويل بمتغير في خطوة واحدة:

// قبل if (statusObj instanceof String) { String status = (String) statusObj; // ... استخدام status } // بعد if (statusObj instanceof String status) { // status هي بالفعل String — لا حاجة للتحويل System.out.println(status.toLowerCase()); }

متغير النمط status نطاقه الفرع الصحيح true للشرط if. إذا لم يكن statusObj من نوع String، فالمتغير ببساطة غير موجود — لا خطر من ClassCastException.

الخطوة 4 — استبدال سلاسل if/else بتعبيرات switch

دالة mapStatus هي المرشّح الأمثل لتعبير switch. إنها تعيّن قيمة لأخرى، ولا تُحدث آثارًا جانبية، وكل فرع يُعيد نتيجة:

public String mapStatus(Object statusObj) { if (!(statusObj instanceof String status)) { return "Invalid input"; } return switch (status) { case "PENDING" -> "Awaiting processing"; case "SHIPPED" -> "On its way"; case "DELIVERED" -> "Delivered"; default -> "Unknown status"; }; }

تجمع هذه النسخة الخطوتين 3 و4: الحرس في الأعلى يتعامل مع حالة غير-String مرة واحدة، وتعبير switch يتعامل مع حالات String دون أي عبارات break أو خطر التسقيط. كل ذراع عبارة واحدة — نظيف وشامل.

تعبيرات switch يجب أن تكون شاملة. يفرض المُجمِّع تغطية جميع القيم الممكنة (بذراع default أو بسرد جميع ثوابت enum). هذا شبكة أمان في وقت الترجمة لا تستطيع سلاسل if/else if توفيرها.

الكلاس المُعاد هيكلته بالكامل

بجمع الخطوات الأربع معًا:

import java.util.HashMap; import java.util.Map; import java.util.Optional; public class OrderProcessor { private final Map<Integer, String> orderDb = new HashMap<>(); public OrderProcessor() { orderDb.put(1, "Alice:PENDING:99.99"); orderDb.put(2, "Bob:SHIPPED:149.50"); orderDb.put(3, "Carol:DELIVERED:220.00"); } // الخطوة 1 — الغياب الصريح عبر Optional public Optional<String> findRaw(int id) { return Optional.ofNullable(orderDb.get(id)); } // الخطوتان 1 + 2 — سلسلة Optional، var للمتغيرات المحلية public String getCustomerName(int id) { return findRaw(id) .map(raw -> raw.split(":")[0]) .orElse("UNKNOWN"); } public void printOrderSummary(int id) { var rawOpt = findRaw(id); var display = rawOpt.orElse("order not found"); var parts = display.split(":"); System.out.println("Customer : " + parts[0]); if (parts.length > 1) System.out.println("Status : " + parts[1]); } // الخطوتان 3 + 4 — instanceof بالنمط + تعبير switch public String mapStatus(Object statusObj) { if (!(statusObj instanceof String status)) return "Invalid input"; return switch (status) { case "PENDING" -> "Awaiting processing"; case "SHIPPED" -> "On its way"; case "DELIVERED" -> "Delivered"; default -> "Unknown status"; }; } public static void main(String[] args) { var processor = new OrderProcessor(); // var: النوع واضح من المُنشئ System.out.println(processor.getCustomerName(1)); // Alice System.out.println(processor.getCustomerName(99)); // UNKNOWN System.out.println(processor.mapStatus("SHIPPED")); // On its way System.out.println(processor.mapStatus(42)); // Invalid input processor.printOrderSummary(2); } }

متى لا تُعيد الهيكلة

الأساليب الحديثة ليست دائمًا تحسينات. احتفظ بالأسلوب القديم عندما:

  • تستهدف قاعدة الكود إصدارًا من Java أقدم مما تتطلبه الميزة (تحقق من علامة --release).
  • سيُخفي var نوعًا غير واضح في واجهة برمجية عامة أو مُهيِّئ معقّد.
  • سيُخزَّن Optional كحقل أو يُمرَّر كمعامل دالة — هذا نمط مضاد يضيف تكلفة التغليف دون فائدة.
  • يحتاج تعبير switch إلى yield لفروع متعددة الجمل بكثرة لدرجة تجعل عبارة switch التقليدية أوضح قراءةً.

الخلاصة

إعادة هيكلة الكود إلى Java الحديثة هي تمرين مُستهدَف: حدّد نقطة ألم محددة (تسريب null، تحويل نوع مزدحم، تفريع مطوّل)، طبّق الميزة التي تحلّها، وتحقق أن السلوك لم يتغيّر. النتيجة كود بنفس الدلالات في وقت التشغيل لكن بنسبة إشارة إلى ضجيج أعلى — أسهل قراءةً، وأصعب إساءةً، وفي حالة Optional وتعبيرات switch مُتحقَّق منه جزئيًا من قِبَل المُجمِّع نفسه.