التعدادات والسجلّات والأنواع المختومة

الواجهات المغلقة مع السجلات

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

الواجهات المغلقة مع السجلات

في البرمجة الوظيفية يوجد مفهوم يُسمى النوع الجبري للبيانات (ADT) — وهو نوع يكون فيه الكائن واحدًا بالضبط من مجموعة معروفة ومحدودة من المتغيرات. فكّر في الأمر كعائلة مغلقة من الأشكال: Shape إمّا Circle أو Rectangle أو Triangle — ولا شيء آخر أبدًا. يمنحك Java 17 دعمًا أصيلًا لهذا النمط بالجمع بين الواجهات المغلقة (sealed interfaces) والسجلات (records).

لماذا يهمّ هذا الجمع

الواجهة المغلقة تقول: "فقط هذه الأنواع يمكنها تطبيقي." والسجل يقول: "أنا حامل بيانات شفّاف وغير قابل للتغيير." معًا يتيحان لك نمذجة متغير في النطاق كقيمة مُسمّاة وواصفة لنفسها دون أي نماذج تكرارية. يعرف المُصرِّف كل حالة ممكنة — وهو ما يجعل تعبيرات switch الشاملة (التي تُغطّى في الدرس التالي) آمنة على مستوى التصريف.

الفكرة الجوهرية: الجمع بين sealed وrecord يعني "نوع مجموع مغلق حيث كل متغيّر هو مجرّد بيانات". وهو ما يعادل sealed class في Kotlin، أو data في Haskell، أو enum بحقول في Rust.

نمذجة نتيجة الدفع

تخيّل نظام دفع حيث تنتهي معالجة شحنة بإحدى ثلاث نتائج: نجاح، أو رفض ناعم (قابل لإعادة المحاولة)، أو فشل صارم. إليك النوع الجبري:

public sealed interface PaymentResult permits Success, Declined, Failed {} public record Success(String transactionId, long amountCents) implements PaymentResult {} public record Declined(String reason, boolean retryable) implements PaymentResult {} public record Failed(String errorCode, Throwable cause) implements PaymentResult {}

لاحظ ما حصلنا عليه مجانًا من تصريحات السجل: منشئات، equals، hashCode، toString، وأدوات الوصول — كلها مبنية على المكوّنات المُعلَن عنها. الواجهة المغلقة لا تضيف سوى القيد: هذه الأنواع الثلاثة فقط هي الموجودة.

جملة permits مقابل الاستنتاج التلقائي

إذا أُعلِن عن جميع الأنواع المسموح بها في وحدة تصريف واحدة (نفس ملف .java أو نفس الحزمة حسب التداخل)، يمكنك حذف جملة permits ويستنتجها المُصرِّف تلقائيًا. لكن التصريح الصريح يكون عادةً أفضل للقابلية على القراءة في كود الإنتاج:

// permits صريح — مُفضَّل عندما تعيش الأنواع في ملفات منفصلة public sealed interface PaymentResult permits Success, Declined, Failed {} // ضمني (الأنواع الثلاثة في نفس الملف) — مناسب للأنواع الجبرية الصغيرة public sealed interface Shape {} record Circle(double radius) implements Shape {} record Rect(double w, double h) implements Shape {}
استخدم permits الصريح لأي نوع جبري قد ينمو أو يعيش عبر ملفات متعددة. الاستنتاج الضمني مريح للعائلات الصغيرة المتجاورة — كما في اختبار أو حزمة أداة صغيرة.

السجلات يمكنها تطبيق واجهات متعددة

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

public interface Auditable { String auditLabel(); } public sealed interface PaymentResult permits Success, Declined, Failed {} public record Success(String transactionId, long amountCents) implements PaymentResult, Auditable { @Override public String auditLabel() { return "SUCCESS:" + transactionId; } }

إضافة منشئات مدمجة للتحقق

تدعم السجلات داخل تسلسل هرمي مغلق المنشئات المدمجة للتحقق من الصحة. إذا كانت نتيجة Declined يجب أن تحمل دائمًا سببًا غير فارغ، طبّق ذلك عند الإنشاء:

public record Declined(String reason, boolean retryable) implements PaymentResult { public Declined { if (reason == null || reason.isBlank()) { throw new IllegalArgumentException("Decline reason must not be blank"); } } }

يعمل المنشئ المدمج قبل تعيين الحقول — وهو المكان المناسب تمامًا لفحوصات الحارس.

التداخل: واجهات مغلقة داخل السجلات

يمكن أن تذهب الأنواع الجبرية إلى أعماق أكثر. لنفترض أن Failed تحتاج إلى التمييز بين خطأ شبكة وحجب احتيال. يمكنك تضمين واجهة مغلقة أخرى بداخلها:

public record Failed(FailureKind kind, String detail) implements PaymentResult { public sealed interface FailureKind permits NetworkError, FraudBlock {} public record NetworkError(int httpStatus) implements FailureKind {} public record FraudBlock(String ruleId) implements FailureKind {} }

الآن Failed هو نفسه سجل شفاف، لكن حقله kind هو نوع جبري داخلي مُصنَّف وقابل للمطابقة الشاملة.

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

استهلاك النوع الجبري (معاينة لمطابقة الأنماط)

حتى قبل switch الشامل لمطابقة الأنماط (الدرس 9)، يمكنك بالفعل استخدام أنماط instanceof لاستهلاك تسلسل السجلات المغلقة بشكل نظيف:

public static String summarise(PaymentResult result) { if (result instanceof Success s) { return "Charged " + s.amountCents() + " cents, txn=" + s.transactionId(); } else if (result instanceof Declined d) { return "Declined: " + d.reason() + (d.retryable() ? " (retryable)" : ""); } else if (result instanceof Failed f) { return "Error " + f.errorCode(); } throw new AssertionError("unreachable — sealed type"); }

سطر throw new AssertionError هو حارس دفاعي. في الدرس 9 ستستبدل هذه الطريقة بأكملها بتعبير switch واحد، وسيتحقق المُصرِّف من معالجة كل متغيّر — دون الحاجة إلى حارس.

الخلاصة

يمنح الجمع بين الواجهات المغلقة والسجلات Java طريقة موجزة وآمنة على مستوى الأنواع لنمذجة أنواع المتغيرات المغلقة (الأنواع الجبرية). الواجهة المغلقة تُسمّي مجموعة المتغيرات وتُقيّدها؛ وكل سجل يوفّر بيانات المتغيّر بشفافية. يتتبّع المُصرِّف كل حالة ممكنة، مما يُتيح التحليل الشامل ويُلغي الحاجة إلى فروع احتياطية دفاعية. هذا النمط هو الأساس لـ switch بمطابقة الأنماط الذي ستكتبه في الدرس التالي.