نمطا Adapter و Facade
من أكثر الأنماط الهيكلية نفعًا في الواقع العملي هما نمط Adapter ونمط Facade. كلاهما يجلس بين كودك وشيء لا تتحكم فيه تمامًا — مكتبة خارجية، أو نظام قديم، أو نظام فرعي معقّد — غير أنّهما يحلّان مشكلتين متعاكستين: Adapter يجعل واجهة غير متوافقة متوافقة، بينما Facade يجعل واجهة معقّدة بسيطة.
نمط Adapter
نية GoF: "حوِّل واجهة كلاس إلى واجهة أخرى يتوقعها العميل. يتيح Adapter تعاون كلاسات لم يكن بإمكانها ذلك بسبب تعارض واجهاتها."
تخيّل محوّل القابس الكهربائي: المقبس له شكل، وجهازك له شكل آخر، والمحوّل يجلس بينهما ويتحدّث اللغتين دون تغيير أيٍّ من الطرفين.
متى تلجأ إليه
- عند دمج كلاس خارجي أو قديم لا تستطيع (أو لا ينبغي) تغيير واجهته.
- عندما تريد استخدام عدة كلاسات غير متجانسة من خلال واجهة واحدة (مثل بوابات دفع متعددة خلف واجهة
PaymentGateway موحّدة).
- عندما تريد فصل كود العميل عن تنفيذ محدّد ليتسنّى لك استبداله لاحقًا.
Class Adapter مقابل Object Adapter
تدعم Java كلتا الصورتين. محوّل الكلاس (class adapter) يرث من الـ adaptee (ممكن فقط إذا لم يكن ثمّة superclass أخرى مطلوبة). محوّل الكائن (object adapter) — الأكثر شيوعًا بفارق كبير — يلفّ الـ adaptee بالتركيب (composition)، وهو المفضّل عمومًا لأنّه لا يقيّدك بكلاس محدّد ويعمل مع subclasses من الـ adaptee أيضًا.
فضّل التركيب على الوراثة. محوّل الكائن (object adapter) هو الخيار الصحيح في جميع الحالات تقريبًا بلغة Java. أما محوّل الكلاس (class adapter) فهو مؤشّر على ضعف في التصميم ما لم يكن لديك سبب مقنع.
مثال واقعي: توصيل خدمة تقارير XML قديمة بخطّ أنابيب يعتمد JSON
لنفترض أنّ تطبيقك يعمل مع واجهة ReportExporter تتوقع مخرجات JSON، بينما لديك مكتبة خارجية لا تنتج إلا XML، ولا يمكنك تعديلها.
// الواجهة المستهدفة — اللغة التي يتحدثها تطبيقك
public interface ReportExporter {
String exportAsJson(String reportId);
}
// الـ Adaptee — مكتبة XML القديمة التي لا يمكن تعديلها
public class LegacyXmlReportService {
public String generateXmlReport(String id) {
// تخيّل توليد XML حقيقي هنا
return "<report><id>" + id + "</id></report>";
}
public String getMetadataXml(String id) {
return "<meta><generated>true</generated></meta>";
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
// Object Adapter — يلفّ الـ adaptee وينفّذ الواجهة المستهدفة
public class XmlToJsonReportAdapter implements ReportExporter {
private final LegacyXmlReportService xmlService;
private final XmlMapper xmlMapper = new XmlMapper();
private final ObjectMapper jsonMapper = new ObjectMapper();
public XmlToJsonReportAdapter(LegacyXmlReportService xmlService) {
this.xmlService = xmlService;
}
@Override
public String exportAsJson(String reportId) {
try {
String xml = xmlService.generateXmlReport(reportId);
// حلّل XML إلى map عام ثم أعِد تسلسله كـ JSON
var node = xmlMapper.readTree(xml.getBytes());
return jsonMapper.writeValueAsString(node);
} catch (Exception e) {
throw new ReportConversionException("Failed to adapt XML report to JSON", e);
}
}
}
// كود العميل — لا يعلم شيئًا عن XML، يتعامل فقط مع ReportExporter
public class ReportingService {
private final ReportExporter exporter;
public ReportingService(ReportExporter exporter) {
this.exporter = exporter;
}
public void publishReport(String reportId) {
String json = exporter.exportAsJson(reportId);
// أرسل json إلى خط الأنابيب المنبعث...
System.out.println("Publishing: " + json);
}
}
// الربط (مثلاً في factory أو إعداد حقن التبعيات)
var legacy = new LegacyXmlReportService();
ReportExporter adapter = new XmlToJsonReportAdapter(legacy);
var service = new ReportingService(adapter);
service.publishReport("Q1-2025");
لا يرى العميل أبدًا LegacyXmlReportService. غدًا يمكنك استبداله بمزوّد JSON أصيل عبر كتابة تنفيذ مختلف لـ ReportExporter — دون تغيير أي كود في العميل.
اختبار المحوّلات: المحوّل طبقة ترجمة رفيعة، لذا اختباراته الوحدوية بسيطة — استخدم stub للـ adaptee، استدعِ تابع المحوّل، وتحقّق من المخرج المترجَم. لا تدع المنطق التجاري يتسلّل إلى المحوّلات؛ أبقِها مركّزة على ترجمة الواجهات فقط.
نمط Facade
نية GoF: "قدِّم واجهة مبسّطة لمجموعة معقّدة من الكود."
لا يُكيّف الـ Facade واجهة مع أخرى — بل يخفي نظامًا فرعيًا بأكمله خلف نقطة دخول واحدة ومتماسكة. كلاسات النظام الفرعي لا تزال موجودة ويمكن استخدامها مباشرةً من الكود الذي يحتاج تحكّمًا دقيقًا، لكن معظم المستدعين يكتفون بالـ facade.
متى تلجأ إليه
- عندما نما النظام الفرعي وبات على المستدعين تنسيق كلاسات كثيرة بترتيب معيّن.
- عندما تريد تطبيق نظام بطبقات (UI يستدعي facade، وfacade ينسّق طبقات Domain وInfrastructure).
- عندما تريد فصل النظام الفرعي عن عملائه حتى يتطوّر بدون تموّج التغييرات للخارج.
مثال واقعي: خطّ ترميز الفيديو
ترميز فيديو يتضمّن خطوات عدة: التحقق من الملف المصدر، واستخراج الصوت، وتغيير أبعاد الإطارات، والترميز، وتوليد صورة مصغّرة، وكتابة البيانات الوصفية. بدون facade، يجب على كل مستدعٍ معرفة وتنسيق جميع هذه الكلاسات.
// كلاسات النظام الفرعي — كل منها مركّز على مهمة واحدة
public class VideoValidator {
public void validate(String filePath) {
System.out.println("Validating: " + filePath);
}
}
public class AudioExtractor {
public String extract(String filePath) {
System.out.println("Extracting audio from: " + filePath);
return filePath + ".aac";
}
}
public class VideoEncoder {
public String encode(String filePath, String resolution) {
System.out.println("Encoding " + filePath + " at " + resolution);
return filePath + ".mp4";
}
}
public class ThumbnailGenerator {
public String generate(String videoPath) {
System.out.println("Generating thumbnail for: " + videoPath);
return videoPath + ".jpg";
}
}
public class MetadataWriter {
public void write(String videoPath, String audioPath, String thumbnail) {
System.out.println("Writing metadata for: " + videoPath);
}
}
// Facade — ينسّق النظام الفرعي خلف تابع واحد بسيط
public class VideoTranscodingFacade {
private final VideoValidator validator;
private final AudioExtractor audioExtractor;
private final VideoEncoder encoder;
private final ThumbnailGenerator thumbnailGenerator;
private final MetadataWriter metadataWriter;
// التبعيات تُحقَن — الـ facade لا ينشئها بنفسه
public VideoTranscodingFacade(
VideoValidator validator,
AudioExtractor audioExtractor,
VideoEncoder encoder,
ThumbnailGenerator thumbnailGenerator,
MetadataWriter metadataWriter) {
this.validator = validator;
this.audioExtractor = audioExtractor;
this.encoder = encoder;
this.thumbnailGenerator = thumbnailGenerator;
this.metadataWriter = metadataWriter;
}
public String transcode(String sourcePath, String resolution) {
validator.validate(sourcePath);
String audio = audioExtractor.extract(sourcePath);
String encoded = encoder.encode(sourcePath, resolution);
String thumbnail = thumbnailGenerator.generate(encoded);
metadataWriter.write(encoded, audio, thumbnail);
return encoded;
}
}
// كود العميل — يستدعي تابعًا واحدًا، بمعزل تام عن النظام الفرعي
VideoTranscodingFacade facade = new VideoTranscodingFacade(
new VideoValidator(),
new AudioExtractor(),
new VideoEncoder(),
new ThumbnailGenerator(),
new MetadataWriter()
);
String output = facade.transcode("/uploads/raw-video.mov", "1080p");
System.out.println("Done: " + output);
الـ facade ليس كائن إله (God Object). إذا كدّست فيه المنطق التجاري ومعالجة الأخطاء والتخزين المؤقت والاهتمامات المتقاطعة أصبح كابوسًا في الصيانة. أبقِ الـ facade طبقة تنسيق رفيعة. إذا تجاوز ~150 سطرًا فقسّم النظام الفرعي إلى facades أصغر.
Adapter مقابل Facade — اختيار النمط الصحيح
كلا النمطين يُدخلان كلاسًا وسيطًا، لذا يسهل الخلط بينهما. الفارق يكمن في الغرض:
- Adapter: الواجهة المستهدفة ثابتة (يحدّدها العميل)؛ واجهة الـ adaptee ثابتة (تحدّدها المكتبة)؛ والـ adapter يترجم بينهما. الواجهتان الموجودتان هما اللتان تُملِيان تصميمه.
- Facade: لا يوجد تعارض في الواجهات. الـ facade يعرّف واجهة جديدة وأبسط تخفي نظامًا فرعيًا معقّدًا تملكه أو تستطيع الاطّلاع على داخله.
في الممارسة الفعلية، كثيرًا ما يُستخدم النمطان معًا: facade للنظام الفرعي الداخلي، مع adapters لكل مكتبة خارجية يعتمد عليها ذلك النظام.
الخلاصة
يجسر نمط Adapter الواجهات غير المتوافقة بلفّ كائن الـ adaptee (object adapter) أو بالوراثة منه (class adapter). يتيح دمج كود خارجي أو قديم دون لمس كود العميل. أما نمط Facade فيعرّف واجهة مبسّطة فوق نظام فرعي معقّد، ممّا يُقلّل الاقتران والجهد الذهني على المستدعين. كلاهما لا غنى عنه في تطوير Java الاحترافي: استخدم Adapter عند مواجهة تعارض في الواجهات، واستخدم Facade عند مواجهة تعقيد ينبغي إخفاؤه.