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

الفئات المختومة (Sealed Classes)

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

الفئات المختومة (Sealed Classes)

سمح نظام الأنواع في Java دائمًا لأي فئة بتوسيع أي فئة أخرى، ما لم تُضف إليها final. غير أن final خيار إما/أو: إما لا يمتد منها أحد، أو يمتد الجميع. الفئات المختومة (Sealed Classes)، التي أصبحت ميزة رسمية في Java 17، تمنحك حلًا وسطًا: تحدد أنت بالضبط أي الفئات يحق لها أن تمتد منك. يُطبّق المُصرِّف هذه القائمة ولا شيء خارجها يمكنه الانضمام إلى التسلسل الهرمي.

المشكلة التي تحلها الفئات المختومة

تخيّل أنك تُنمذج نتيجة عملية دفع. تريد ثلاثة نتائج فقط: Success وFailure وPending. مع الوراثة العادية، يمكن لأي كود خارجي إضافة نتيجة رابعة بصمت، مما يُفسد منطق switch الشامل الخاص بك. تجعل الفئات المختومة التسلسل الهرمي مغلقًا ومعروفًا أثناء الترجمة، فيتمكن المُصرِّف من التحقق من أنك تعاملت مع كل حالة.

الصيغة: sealed و permits

تُعلن عن فئة مختومة باستخدام المُعدِّل sealed وتُدرج كل فئة فرعية مسموح بها في جملة permits:

public sealed class PaymentResult permits Success, Failure, Pending { // الحالة أو الأساليب المشتركة تذهب هنا }

يجب على كل فئة فرعية مسموح بها إعلان أحد المُعدِّلات الثلاثة:

  • final — لا يمكن تمديد الفئة الفرعية ذاتها إلى أبعد من ذلك.
  • sealed — الفئة الفرعية مختومة هي الأخرى ويجب أن توفر قائمة permits الخاصة بها (تتيح تسلسلًا هرميًا مُقيَّدًا أعمق).
  • non-sealed — تُعيد الفئة الفرعية فتح التسلسل الهرمي؛ يمكن لأي شخص تمديدها بحرية.
يجب على جميع الفئات الفرعية المسموح بها اختيار أحد هذه المُعدِّلات الثلاثة. نسيان إضافته يُعدّ خطأ في الترجمة، وهذا بالضبط ما يُقصد به — يمنع المُصرِّف التمديد العرضي المفتوح.

إليك المثال الكامل مع جميع النتائج الثلاث بوصفها سجلات final (السجلات مناسبة طبيعية — المزيد في الدرس القادم):

public sealed class PaymentResult permits Success, Failure, Pending {} public final class Success extends PaymentResult { private final String transactionId; public Success(String transactionId) { this.transactionId = transactionId; } public String transactionId() { return transactionId; } } public final class Failure extends PaymentResult { private final String reason; public Failure(String reason) { this.reason = reason; } public String reason() { return reason; } } public final class Pending extends PaymentResult { private final long estimatedMs; public Pending(long estimatedMs) { this.estimatedMs = estimatedMs; } public long estimatedMs() { return estimatedMs; } }

استخدام التسلسل الهرمي المختوم في switch

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

static String describe(PaymentResult result) { return switch (result) { case Success s -> "Paid. Transaction: " + s.transactionId(); case Failure f -> "Failed: " + f.reason(); case Pending p -> "Pending, ~" + p.estimatedMs() + "ms"; }; }

إذا أضفت لاحقًا فئة رابعة مسموحًا بها، يُشير المُصرِّف فورًا إلى كل switch لا يتعامل معها. هذا هو أمان الأنواع الجبرية (algebraic data types) في Java.

صُمِّمت الفئات المختومة ومطابقة الأنماط في switch معًا. اعتد على تعريف التسلسلات الهرمية المغلقة الآن؛ سيجعلها مطابقة الأنماط في الدرس التاسع أكثر قوة.

التداخل: الفئات الفرعية المختومة

قد تكون الفئة الفرعية المسموح بها مختومة هي الأخرى، مما يتيح لك بناء شجرة مُقيَّدة من مستويين:

public sealed class Shape permits Circle, Polygon {} public final class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } public double radius() { return radius; } } // Polygon مختومة — Triangle و Rectangle فقط يمكنهما تمديدها public sealed class Polygon extends Shape permits Triangle, Rectangle {} public final class Triangle extends Polygon {} public final class Rectangle extends Polygon {}

مجموعة الأنواع الكاملة التي يراها المُصرِّف هي: Circle وTriangle وRectangle. يجب على switch على Shape التعامل مع الأنواع الثلاثة.

non-sealed: إعادة فتح التسلسل الهرمي

في بعض الأحيان تريد تقييد معظم التسلسل الهرمي لكن تسمح لفرع واحد بأن يمتده الأطراف الثالثة بحرية. اوسم ذلك الفرع بـ non-sealed:

public sealed class Notification permits EmailNotification, SmsNotification, CustomNotification {} public final class EmailNotification extends Notification {} public final class SmsNotification extends Notification {} // يمكن لأي شخص توسيع CustomNotification — التسلسل الهرمي مفتوح من هنا public non-sealed class CustomNotification extends Notification {}
استخدام non-sealed يُزيل ضمانات الشمولية لذلك الفرع. لا يستطيع المُصرِّف بعد الآن التحقق من أن switch يتعامل مع كل نوع فرعي محتمل من CustomNotification. استخدمه فقط عندما يكون التمديد المفتوح مقصودًا.

قواعد الملف والحزمة

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

الخلاصة

تتيح لك الفئات المختومة إعلان مجموعة مُقيَّدة ومُطبَّقة من المُصرِّف من الأنواع الفرعية. استخدم sealed … permits … على الأصل، ثم ضع على كل فئة فرعية مسموح بها final أو sealed أو non-sealed. التسلسل الهرمي موثق ومُدار بالإصدارات وقابل للفحص الشامل — لا سيما عند دمجه مع switch لمطابقة الأنماط. في الدرس القادم، ستعرف كيف تتعاون الواجهات المختومة مع السجلات لإنتاج أنواع جبرية آمنة وموجزة.