تتعامل العديد من فرق TypeScript مع abstract class و interface كأنهما حشوتان قابلتان للتبادل في نفس الشطيرة. لكنهما ليسا كذلك. إن اختيار الأداة الخاطئة يسبب أضراراً حقيقية: تكرار منطق العمل (business logic)، وأشجار فئات (class trees) جامدة تقاوم كل عملية إعادة هيكلة (refactor)، وأخطاء وقت التشغيل (runtime errors) خفية تعود جذورها إلى فئة أساسية (base class) افترض شخص ما أنها آمنة. الحل ليس في حفظ كتاب القواعد، بل في تعلم النظر إلى متطلباتك الفعلية واختيار الأداة التي تناسبها.
الواجهات (Interfaces) هي مجرد عقود
الواجهة (interface) هي حدود وقت التجميع (compile-time boundary). فهي تصف كيف يجب أن يبدو الكائن وما يجب أن يكون قادراً على فعله، ولكنها لا تقدم أي تنفيذ (implementation). بعد تحويل TypeScript إلى JavaScript، تختفي الواجهة تماماً؛ فهي لا تترك أي مُنشئ (constructor)، ولا سلسلة نماذج أولية (prototype chain)، ولا أي بايتات إضافية في حزمة ملفاتك (bundle).
تخيل أنك تبني نظام إشعارات. ستقوم بتعريف واجهة:
interface Notifier {
send(message: string): void;
readonly channel: string;
}
أي فئة (class)، أو حتى كائن بسيط (object literal)، يستوفي هذا الشكل يعتبر Notifier صالحاً. يمكن لـ EmailNotifier تنفيذها، وكذلك SmsNotifier أو SlackNotifier أو كائن وهمي (mock object) تقدمه لاختبار الوحدة (unit test). ولأن الواجهة لا تحمل أي كود، فإن كل تنفيذ يكتب طريقة send الخاصة به من الصفر. وهذا هو بالضبط ما تريده عندما لا تشترك التنفيذات في أي شيء سوى واجهتها العامة.
الفئات المجردة (Abstract Classes) تحمل كوداً حقيقياً
الفئة المجردة (abstract class) هي فئة كاملة الصلاحيات ولكنها تمنع الإنشاء المباشر (direct instantiation). يمكنها تعريف حقول (fields)، وطرق ملموسة (concrete methods) تحتوي على منطق، ومنطق المُنشئ (constructor logic). وهي تجبر الفئات الفرعية (subclasses) على ملء الفراغات من خلال طرق مجردة (abstract methods)، ولكنها تمنحها أيضاً سلوكاً موروثاً لا يتعين عليها كتابته بنفسها.
تخيل طبقة الوصول إلى البيانات (data access layer). يحتاج كل مستودع (repository) في تطبيقك إلى تحليل معرف (identifier)، والتحقق من صحته مقابل مخطط (schema)، ومن ثم تشغيل استعلام خاص بالتخزين. لا يمكن للواجهة التقاط هذا التسلسل المشترك لأن الواجهة لا يمكنها احتواء كود قابل للتنفيذ. أما الفئة المجردة فيمكنها ذلك:
abstract class BaseRepository<T> {
protected validateId(id: string): boolean {
return /^[a-z0-9\-]+$/.test(id);
}
abstract fetchById(id: string): Promise<T | null>;
async findById(id: string): Promise<T | null> {
if (!this.validateId(id)) throw new Error("Invalid ID format");
return this.fetchById(id);
}
}
تفرض BaseRepository الهيكل؛ فهي تطلب من الفئات الفرعية تنفيذ fetchById ولكنها توفر أيضاً منطقاً جاهزاً للعمل. تحصل الفئات الفرعية على الضوابط والكود المتكرر (boilerplate) مجاناً. إذا حاولت استبدال هذا بواجهة، فسينتهي بك الأمر بنسخ validateId و findById في كل مستودع على حدة. هذا ليس تجريداً (abstraction)، بل هو ضريبة صيانة.
السؤال الوحيد الذي يحسم الأمر
يجب أن يعتمد اختيارك دائماً على سؤال واحد: هل يحتاج هذا العقد إلى تقديم كود مشترك؟
إذا كانت الإجابة لا، فاستخدم واجهة (interface). إذا كانت الإجابة نعم، فاستخدم فئة مجردة (abstract class).
الخطأ في هذا الأمر له عواقب فورية. إذا استخدمت فئة مجردة في مكان يكفي فيه مجرد تحديد الشكل، فإنك تجبر كل تنفيذ على الدخول في سلسلة وراثة (inheritance chain). فجأة، تتطلب اختبارات الوحدة عمليات محاكاة (mocking) مربكة أو نماذج جزئية (partial stubs) لفئة حقيقية. كما يجب على الإضافات الخارجية أن ترث من فئتك الأساسية بدلاً من مجرد مطابقة الشكل. لقد حولت عقداً بسيطاً إلى جبل.
ومن ناحية أخرى، إذا استخدمت واجهة في مكان يوجد فيه سلوك مشترك، فستقوم بنثر نسخ من نفس المنطق عبر قاعدة الكود الخاصة بك. وعندما يحتوي هذا المنطق على خطأ (bug)، فلن تتمكن من إصلاحه في مكان واحد؛ بل ستضطر للبحث عبر عشرة تنفيذات وتأمل ألا يفوتك الحادي عشر.
كيف يختلفان فعلياً في الممارسة العملية
بعيداً عن الانقسام الفلسفي، هناك ثلاث فجوات عملية تفصل بينهما.
الأداء وحجم الحزمة (bundle size). يتم مسح الواجهات أثناء التجميع. فهي لا تضيف أي وزن على الإطلاق إلى مخرجات JavaScript الخاصة بك ولا تستهلك أي ذاكرة وقت التشغيل. أما الفئات المجردة فتُحول إلى وظائف مُنشئ (constructor functions) حقيقية وسلاسل نماذج أولية (prototype chains). كل فئة تُعرفها تزيد من حجم حزمتك وتستقر في الذاكرة عند إنشائها. وفي خادم يتعامل مع آلاف المثيلات (instances)، أو في حزمة واجهة أمامية (frontend bundle) تخضع للتدقيق، فإن هذا الفرق ليس مجرد فرق نظري.
مرونة التركيب (composition). يمكن لفئة واحدة تنفيذ العديد من الواجهات في وقت واحد. قد تبني FileCache تنفذ Cache و Disposable و EventEmitter في آن واحد. TypeScript سيكون سعيداً لأن الواجهات لا تفرض أي تسلسل هرمي. ومع ذلك، لا يمكن للفئة إلا أن ترث من فئة مجردة واحدة فقط؛ فسلسلة النماذج الأولية في JavaScript لا تدعم الوراثة المتعددة (multiple inheritance). إذا اعتمدت بشكل مفرط على الفئات المجردة، فستواجه في النهاية المعضلة الكلاسيكية المتمثلة في محاولة دمج فئتين أساسيتين تحتوي كل منهما على
