مشروع: نظام إضافات باستخدام الواجهات
على مدار هذا البرنامج التعليمي درستَ الواجهات بوصفها ميزةً لغوية. في هذا الدرس الأخير ستُطبّق كل ما تعلّمته بتصميم نظام إضافات صغير وواقعي — نمطٌ يُستخدم في بيئات التطوير المتكاملة ومحركات الألعاب وأدوات البناء وعدد لا يُحصى من أُطر العمل. الهدف هو كتابة نواة تطبيق قابلة للتوسعة في وقت التشغيل دون تعديل الكود الموجود، وهو تطبيق مباشر لمبدأ الفتح/الإغلاق.
ما هو نظام الإضافات ولماذا يهمّنا؟
يفصل نظام الإضافات بين المُضيف (نواة التطبيق الثابتة) والإضافات (امتدادات مكتوبة باستقلالية). يعلم المُضيف فقط بعقد الواجهة؛ تُنفّذ كل إضافة ذلك العقد. لا يحتاج أيٌّ من الطرفين لمعرفة تفاصيل الآخر.
يُعطيك هذا ثلاث فوائد ملموسة:
- قابلية التوسعة دون تعديل — إضافة ملحق جديد لا تلمس المُضيف أبدًا.
- قابلية الاختبار — يمكنك حقن إضافة وهمية أثناء اختبارات الوحدة.
- قابلية الاستبدال — استبدل التنفيذات (مثلًا انتقل من مُصدِّر ملفات إلى مُصدِّر سحابي) دون إعادة كتابة منطق الأعمال.
يُسمّى هذا أيضًا نمط الاستراتيجية. حين تُختار "الإضافة" في وقت التشغيل وتُغلّف خوارزمية ما، فأنت تُنفّذ نمط Strategy الكلاسيكي من كتاب Gang of Four. الواجهات هي الأداة الطبيعية لذلك في Java.
الخطوة الأولى — تحديد عقد الإضافة
أول قرار تصميمي هو تحديد الواجهة التي يجب أن ترضاها كل إضافة. احرص على تركيزها: واجهة واحدة ومسؤولية واحدة.
package plugins;
/**
* العقد الذي يجب أن تُوفّره كل إضافة لتصدير التقارير.
*/
public interface ReportExporter {
/** اسم قصير يظهر في القوائم. */
String name();
/**
* تصدير الصفوف المعطاة إلى وجهة ما.
* @param headers أسماء الأعمدة
* @param rows بيانات الجدول (كل قائمة داخلية تمثّل صفًّا)
*/
void export(List<String> headers, List<List<String>> rows);
}
طريقتان واضحتان وعقد محدد. يعتمد المُضيف فقط على ReportExporter — لا على أي فئة ملموسة.
الخطوة الثانية — تنفيذ عدة إضافات
كل إضافة هي فئة مستقلة تُنفّذ الواجهة. إليك ثلاثة تنفيذات ملموسة.
package plugins;
import java.util.List;
/** يكتب CSV نصيًا بسيطًا إلى System.out (أو ملف حقيقي في الإنتاج). */
public class CsvExporter implements ReportExporter {
@Override
public String name() { return "CSV Export"; }
@Override
public void export(List<String> headers, List<List<String>> rows) {
System.out.println(String.join(",", headers));
for (List<String> row : rows) {
System.out.println(String.join(",", row));
}
}
}
package plugins;
import java.util.List;
/** يعرض جدولًا ASCII في وحدة التحكم. */
public class ConsoleTableExporter implements ReportExporter {
@Override
public String name() { return "Console Table"; }
@Override
public void export(List<String> headers, List<List<String>> rows) {
String separator = "+-----------+-----------+-----------+";
System.out.println(separator);
System.out.printf("| %-9s | %-9s | %-9s |%n",
headers.get(0), headers.get(1), headers.get(2));
System.out.println(separator);
for (List<String> row : rows) {
System.out.printf("| %-9s | %-9s | %-9s |%n",
row.get(0), row.get(1), row.get(2));
}
System.out.println(separator);
}
}
package plugins;
import java.util.List;
/** ينتج سلسلة HTML تُمثّل جدولًا بسيطًا. */
public class HtmlExporter implements ReportExporter {
@Override
public String name() { return "HTML Export"; }
@Override
public void export(List<String> headers, List<List<String>> rows) {
StringBuilder sb = new StringBuilder("<table>\n<tr>");
for (String h : headers) sb.append("<th>").append(h).append("</th>");
sb.append("</tr>\n");
for (List<String> row : rows) {
sb.append("<tr>");
for (String cell : row) sb.append("<td>").append(cell).append("</td>");
sb.append("</tr>\n");
}
sb.append("</table>");
System.out.println(sb);
}
}
الخطوة الثالثة — بناء سجل الإضافات
يحتفظ المُضيف بسجل: خريطة Map مفتاحها اسم الإضافة. تُسجّل الإضافات نفسها؛ ويبحث عنها المُضيف بالاسم عند اختيار المستخدم.
package plugins;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
public class PluginRegistry {
private final Map<String, ReportExporter> plugins = new LinkedHashMap<>();
/** تسجيل إضافة. تُستبدل إذا كان الاسم موجودًا مسبقًا. */
public void register(ReportExporter plugin) {
plugins.put(plugin.name(), plugin);
}
/** استرجاع إضافة باسمها، أو null إن لم توجد. */
public ReportExporter get(String name) {
return plugins.get(name);
}
/** جميع أسماء الإضافات المسجّلة بترتيب الإدراج. */
public Collection<String> availablePlugins() {
return plugins.keySet();
}
}
فضّل التركيب على الوراثة هنا. يحتفظ السجل بـمراجع لكائنات الإضافات بدلًا من توسيع فئة مجردة. هذا يبقي المُضيف منفصلًا تمامًا: لا يهمّه من أي فئة جاءت الإضافة، بل فقط أنها ترضي ReportExporter.
الخطوة الرابعة — الربط الكامل في التطبيق المُضيف
تُسجّل الفئة الرئيسية للتطبيق الإضافاتِ المتاحة وتُفوّض إلى الإضافة التي يختارها المستخدم. لاحظ أن منطق الاختيار ومنطق التصدير منفصلان تمامًا.
package plugins;
import java.util.List;
import java.util.Scanner;
public class ReportApp {
public static void main(String[] args) {
// --- بناء السجل ---
PluginRegistry registry = new PluginRegistry();
registry.register(new CsvExporter());
registry.register(new ConsoleTableExporter());
registry.register(new HtmlExporter());
// --- بيانات عينة ---
List<String> headers = List.of("Name", "Role", "Score");
List<List<String>> rows = List.of(
List.of("Alice", "Dev", "92"),
List.of("Bob", "Design", "88"),
List.of("Carol", "QA", "95")
);
// --- المستخدم يختار مُصدِّرًا ---
System.out.println("Available exporters: " + registry.availablePlugins());
Scanner scanner = new Scanner(System.in);
System.out.print("Choose exporter: ");
String choice = scanner.nextLine().trim();
ReportExporter exporter = registry.get(choice);
if (exporter == null) {
System.out.println("Unknown exporter: " + choice);
return;
}
// --- التفويض الكامل إلى الإضافة ---
exporter.export(headers, rows);
}
}
لإضافة تنسيق تصدير جديد — مثلًا JSON — تُنشئ فئة جديدة واحدة تُنفّذ ReportExporter وتُضيف استدعاء register واحدًا. صفر أسطر داخل ReportApp أو أي إضافة موجودة تتغيّر.
الخطوة الخامسة — إضافة طريقة افتراضية إلى العقد
يمكن للواجهات أن تحمل طرقًا default، وهو ما يُفيد للسلوك الاختياري الذي تشترك فيه معظم الإضافات. لنفترض أن كل مُصدِّر يجب أن يتمكّن من وصف نفسه:
public interface ReportExporter {
String name();
void export(List<String> headers, List<List<String>> rows);
/** اختياري: وصف يقرأه البشر. أعِد تعريفه عند الحاجة. */
default String description() {
return "No description provided for " + name();
}
}
تُكمل الإضافات الموجودة تجميعها دون أي تغيير. فقط الإضافات التي تريد وصفًا مخصصًا تُعيد تعريف description(). هكذا تُطوّر Java واجهات برمجية دون كسر التنفيذات الموجودة.
لا تُسئ استخدام الطرق الافتراضية. صُمّمت لتطور واجهة برمجية (إضافة سلوك لواجهة موجودة دون تغيير مُكسِر)، لا كاختصار لتجنّب كتابة التنفيذات. إذا كانت كل إضافة تحتاج سلوكًا خاصًا بها فعلًا، فابقِ الطريقة مجردة.
مراجعة التصميم
يستخدم النظام بأكمله ثلاث ميزات فقط من الواجهات التي درستها:
- الطرق المجردة (
name()، export()) — العقد الإلزامي.
- الطرق الافتراضية (
description()) — تحسينات اختيارية مع قيمة احتياطية منطقية.
- تعدد الأشكال — يعمل المُضيف عبر مرجع
ReportExporter؛ يُحدَّد النوع الفعلي (CSV، HTML، …) تلقائيًا في وقت التشغيل.
يتوسّع هذا النمط من إضافتين إلى مئتين. أُطر عمل مثل Spring وIntelliJ IDEA وMaven تستخدمه على نطاق واسع. إتقانه خطوة أساسية نحو كتابة كود Java احترافي وقابل للتوسعة.
الخلاصة
نظام الإضافات هو عقد واجهة مضاف إليه سجل. يُحدّد المُضيف العقد، تُنفّذ كل إضافة استقلاليًا، ويصلهما السجل في وقت التشغيل. إضافة ملحق جديد لا تُعدّل المُضيف أبدًا — هذه هي قوة البرمجة نحو واجهة لا نحو تنفيذ.