نمط الأسلوب القالب والأمر
نمطان يُجمعان معًا في الغالب لأنهما يتعاملان مع تغليف السلوك — فالأسلوب القالب يُثبّت هيكل الخوارزمية بينما يترك للأصناف الفرعية ملء التفاصيل، والأمر يحوّل الطلب إلى كائن من الدرجة الأولى يمكن تخزينه وإضافته إلى قائمة الانتظار وإلغاؤه. إتقان هذين النمطين يعزّز قدرتك على تصميم أنظمة قابلة للتوسع ومنفصلة الأجزاء.
نمط الأسلوب القالب
الأسلوب القالب نمط سلوكي يُحدّد الخطوات العامة لخوارزمية في صنف أساسي، ثم يتيح للأصناف الفرعية تجاوز خطوات بعينها دون تغيير البنية الكلية. الجزء الثابت يقيم في الصنف المجرد الأعلى، والأجزاء المتغيرة تكون أساليب خطّافات أو مجردة.
الفكرة الجوهرية: "لا تتصل بنا، نحن من سيتصل بك." يتحكّم الصنف الأساسي في التدفق؛ لا تُقدّم الأصناف الفرعية إلا الأجزاء المختلفة. هذا هو مبدأ هوليوود مطبّقًا على الوراثة.
تشريح النمط
- الصنف المجرد — يُعلن عن
templateMethod() (عادةً final) وعن الخطوات المجردة والخطّافات.
- الصنف المحدّد — يمتد من الصنف المجرد وينفّذ الخطوات المتغيرة فحسب.
مثال واقعي: مُصدّرات البيانات
تخيّل نظام تقارير يتّبع فيه كل تصدير دورة حياة موحّدة: فتح الاتصال ↔ جلب البيانات ↔ تنسيقها ↔ كتابة المخرجات ↔ إغلاق الاتصال. منطق الفتح والإغلاق متطابق عبر جميع الصيغ؛ لا يتغيّر إلا الجلب والتنسيق.
// الصنف الأساسي المجرد — يُعرّف هيكل الخوارزمية الثابت
public abstract class DataExporter {
// الأسلوب القالب — final لمنع الأصناف الفرعية من إعادة ترتيب الخطوات
public final void export(String destination) {
openConnection();
List<DataRecord> data = fetchData();
String formatted = formatData(data);
writeOutput(formatted, destination);
closeConnection();
}
// خطوات ثابتة — تنفيذ مشترك
private void openConnection() {
System.out.println("Opening data source connection");
}
private void closeConnection() {
System.out.println("Closing connection");
}
// خطوات متغيّرة — يجب على الأصناف الفرعية تنفيذها
protected abstract List<DataRecord> fetchData();
protected abstract String formatData(List<DataRecord> data);
// خطّاف اختياري — يمكن للأصناف الفرعية تجاوزه لتخصيص سلوك الكتابة
protected void writeOutput(String content, String dest) {
System.out.printf("Writing %d bytes to %s%n", content.length(), dest);
}
}
// مُصدّر CSV محدّد
public class CsvExporter extends DataExporter {
@Override
protected List<DataRecord> fetchData() {
return List.of(new DataRecord("Alice", 1), new DataRecord("Bob", 2));
}
@Override
protected String formatData(List<DataRecord> rows) {
StringBuilder sb = new StringBuilder("name,id\n");
rows.forEach(r -> sb.append(r.name()).append(",").append(r.id()).append("\n"));
return sb.toString();
}
}
// مُصدّر JSON محدّد
public class JsonExporter extends DataExporter {
@Override
protected List<DataRecord> fetchData() {
return List.of(new DataRecord("Alice", 1), new DataRecord("Bob", 2));
}
@Override
protected String formatData(List<DataRecord> rows) {
StringBuilder sb = new StringBuilder("[");
rows.forEach(r ->
sb.append("{\"name\":\"").append(r.name())
.append("\",\"id\":").append(r.id()).append("},"));
sb.deleteCharAt(sb.length() - 1);
sb.append("]");
return sb.toString();
}
}
// الاستخدام — هيكل الخوارزمية لا يتغيّر أبدًا، فقط النوع المحدّد يتبدّل
DataExporter exporter = new CsvExporter();
exporter.export("/reports/output.csv");
exporter = new JsonExporter();
exporter.export("/reports/output.json");
متى تجعل الخطوة خطّافًا ومتى تجعلها مجردة: إذا كانت للخطوة قيمة افتراضية معقولة (مثل الكتابة إلى الإخراج القياسي) فاجعلها أسلوبًا محميًا محدّدًا — أي خطّافًا. يمكن للأصناف الفرعية تجاوزه دون إجبار. أما إذا لم تكن للخطوة قيمة افتراضية مناسبة وكان التغيير حتميًا، فأعلنها abstract. تُقلّل الخطّافات التكرار الزائد في الأصناف التي لا تحتاج إلى تجاوز كل خطوة.
المقايضات ومتى تستخدم الأسلوب القالب
- إيجابي: يزيل تكرار هيكل الخوارزمية عبر الأصناف الفرعية؛ أي تغيير في الهيكل ينتشر تلقائيًا.
- إيجابي: مبدأ الفتح/الإغلاق — أضف صيغة جديدة بإضافة صنف لا بتعديل الأساسي.
- سلبي: يعتمد على الوراثة مما يُنشئ اقترانًا وثيقًا. إذا نما التسلسل الهرمي عمقًا، يصبح هشًا.
- بديل: حين تريد المرونة ذاتها دون وراثة، استخدم Strategy مع التركيب. الأسلوب القالب = وراثة؛ الاستراتيجية = تفويض.
نمط الأمر
الأمر نمط سلوكي يُغلّف طلبًا — الإجراء والهدف والمعاملات — داخل كائن. المُستدعي لا يعلم ماذا يفعل الأمر ولا كيف؛ يكتفي باستدعاء execute(). هذا الفصل يُتيح قوائم الانتظار والتسجيل والتراجع والتكرار الماكروي.
المشاركون
- واجهة الأمر — تُعلن
execute() واختياريًا undo().
- الأمر المحدّد — يربط إجراءً بمُستقبِل ويخزّن الحالة اللازمة للتراجع.
- المُستقبِل — الكائن الذي يؤدّي العمل الفعلي (مثل
TextEditor).
- المُستدعي — يخزّن الأوامر ويستدعيها؛ لا يعتمد أبدًا على الأنواع المحدّدة.
- العميل — ينشئ الأوامر المحدّدة ويربطها بالمُستدعي.
مثال واقعي: محرر نصوص مع التراجع
// واجهة الأمر
public interface EditorCommand {
void execute();
void undo();
}
// المُستقبِل — الكائن الذي يؤدّي العمل الفعلي
public class TextEditor {
private StringBuilder text = new StringBuilder();
public void insertText(String s, int position) {
text.insert(position, s);
}
public void deleteText(int start, int length) {
text.delete(start, start + length);
}
public String getText() { return text.toString(); }
}
// الأمر المحدّد — إدراج نص، يخزّن الحالة اللازمة للتراجع
public class InsertCommand implements EditorCommand {
private final TextEditor editor;
private final String text;
private final int position;
public InsertCommand(TextEditor editor, String text, int position) {
this.editor = editor;
this.text = text;
this.position = position;
}
@Override
public void execute() {
editor.insertText(text, position);
}
@Override
public void undo() {
editor.deleteText(position, text.length());
}
}
// المُستدعي — يحتفظ بسجل الأوامر للتراجع والإعادة
public class CommandHistory {
private final Deque<EditorCommand> history = new ArrayDeque<>();
public void executeCommand(EditorCommand cmd) {
cmd.execute();
history.push(cmd);
}
public void undo() {
if (!history.isEmpty()) {
history.pop().undo();
}
}
}
// العميل — يربط كل شيء معًا
TextEditor editor = new TextEditor();
CommandHistory history = new CommandHistory();
EditorCommand insertHello = new InsertCommand(editor, "Hello", 0);
EditorCommand insertWorld = new InsertCommand(editor, " World", 5);
history.executeCommand(insertHello); // "Hello"
history.executeCommand(insertWorld); // "Hello World"
System.out.println(editor.getText()); // Hello World
history.undo(); // يحذف " World"
System.out.println(editor.getText()); // Hello
التعابير اللامدائية كأوامر: حين لا يكون التراجع مطلوبًا، يمكن استبدال واجهة @FunctionalInterface بـ Runnable أو واجهة وظيفية مخصّصة، وتُكتب الأوامر كتعابير لامدائية. هذا أسلوب محاوري في Java 17+ لسيناريوهات قوائم انتظار المهام البسيطة التي لا تحتاج إلى تراجع.
// قائمة أوامر خفيفة باستخدام تعابير لامدائية (بدون تراجع)
Queue<Runnable> commandQueue = new LinkedList<>();
commandQueue.add(() -> System.out.println("Send email"));
commandQueue.add(() -> System.out.println("Generate report"));
while (!commandQueue.isEmpty()) {
commandQueue.poll().run();
}
حالات استخدام متقدمة
- التسجيل الماكروي — خزّن قائمة أوامر وأعِد تشغيلها بنداء واحد. مفيد في أتمتة الاختبارات والبرمجة النصية والمعالجة الدفعية.
- التراجع المعاملاتي — إذا فشل أي أمر في تسلسل، استدعِ
undo() على جميع الأوامر المنفّذة سابقًا بترتيب عكسي.
- قوائم الانتظار غير المتزامنة — سلسل كائنات الأوامر وادفعها إلى وسيط رسائل؛ يُزيل المستهلك التسلسل وينفّذها، مما يُتيح الأنظمة الموزّعة.
تضخّم حالة التراجع: يجب على كل كائن أمر تخزين ما يكفي من الحالة لعكس تأثيره. في جلسة طويلة مع مئات الأوامر القابلة للتراجع، قد يستهلك هذا ذاكرة كبيرة. ضع حدًا أقصى لحجم مكدس السجل، وفكّر في استخدام نمط Memento للقطات مرجعية للرسوم البيانية معقّدة الكائنات بدلًا من تخزين الحالة في كل أمر.
المقايضات
- إيجابي: يفصل المُستدعي عن المُستقبِل فصلًا تامًا؛ يمكن لأي منهما التغيير باستقلالية.
- إيجابي: التراجع والإعادة والتسجيل وقوائم الانتظار تنبثق بشكل طبيعي من تخزين كائنات الأوامر.
- سلبي: مزيد من الأصناف — أمر محدّد واحد لكل عملية. في التطبيقات الكبيرة يمكن التخفيف من هذا بالتعابير اللامدائية أو الأوامر العامة مع مراجع الأساليب.
الأسلوب القالب مقابل الأمر — اختيار النمط المناسب
- استخدم الأسلوب القالب حين يكون لديك دورة حياة خوارزمية ثابتة ولا يتغيّر إلا خطوات بعينها. المُستدعي والصنف المحدّد يشتركان في تسلسل هرمي واحد.
- استخدم الأمر حين تحتاج إلى تمرير عملية كمعامل للمُستدعي، أو دعم التراجع، أو وضع العمل في قائمة انتظار، أو تسجيل السجل. المُستدعي والإجراء منفصلان تمامًا.
- النمطان يتكاملان بشكل جيد: مُستدعي
CommandHistory يمكنه استدعاء أوامر مبنية على الأسلوب القالب، مما يمنح كل أمر هيكل خوارزميته الخاص.
الخلاصة
يستخدم الأسلوب القالب الوراثة لتثبيت بنية الخوارزمية ويُفوّض الخطوات المتغيّرة للأصناف الفرعية عبر أساليب مجردة وخطّافات — ضع final على القالب لمنع إعادة الترتيب غير المقصودة. ويستخدم الأمر التركيب لتحويل الإجراءات إلى كائنات — أبقِ كل أمر محدّد صغيرًا وخزّن فقط الحالة اللازمة للتراجع، وفضّل التعابير اللامدائية حين لا يكون التراجع ضروريًا. كلا النمطين تطبيق مباشر لمبدأ الفتح/الإغلاق ويظهران في قواعد كود Java الإنتاجية: الأسلوب القالب في الأطر (AbstractList، JdbcTemplate الخاص بـ Spring)، والأمر في أدوات واجهة المستخدم ومُجدوِلي المهام والأنظمة المعاملاتية.