التركيب مقابل الوراثة
القاعدة الذهبية: فضّل التركيب على الوراثة
أحد أهم المبادئ في البرمجة الكائنية هو: “فضّل التركيب على الوراثة.” هذا لا يعني أن الوراثة سيئة -- يعني أنك يجب أن تفكر ملياً قبل استخدامها. الوراثة تُنشئ اقتراناً محكماً بين الأب والابن. التركيب يمنحك مرونة أكبر بتجميع الكائنات من قطع أصغر قابلة لإعادة الاستخدام.
السؤال الأساسي هو: هو-نوع مقابل لديه. Dog هو-نوع Animal (وراثة). Car لديه Engine (تركيب). الحصول على هذا التمييز صحيحاً هو أساس تصميم OOP الجيد.
مشكلة الوراثة العميقة
// إشكالي: تسلسل هرمي وراثي عميق
class Animal {
void eat() => print('يأكل...');
}
class FlyingAnimal extends Animal {
void fly() => print('يطير...');
}
class SwimmingAnimal extends Animal {
void swim() => print('يسبح...');
}
// المشكلة: البطة تستطيع الطيران والسباحة معاً!
// Dart تسمح فقط بوراثة واحدة، لذا يجب أن تختار واحدة:
class Duck extends FlyingAnimal {
// لا يمكن أيضاً توسيع SwimmingAnimal!
// يجب تكرار منطق السباحة هنا
void swim() => print('يسبح...');
}
// ماذا عن البطريق؟ يسبح لكن لا يطير.
class Penguin extends SwimmingAnimal {
// إذا ورثنا من FlyingAnimal ستكون لدينا طريقة fly()
// لا معنى لها للبطريق.
}
// ماذا عن السمكة الطائرة؟ تطير وتسبح لكنها ليست طائراً!
// التسلسل الهرمي ينهار تماماً.
التركيب للإنقاذ
مع التركيب، تمنح الكائنات قدرات بربط كائنات السلوك، بدلاً من الوراثة من أب. كل قدرة هي فئة صغيرة ومركزة. الحيوان يمكن أن يمتلك أي مجموعة من القدرات.
التركيب مع كائنات القدرات
// فئات قدرات صغيرة ومركزة
class FlyingAbility {
final double maxAltitude;
FlyingAbility({this.maxAltitude = 1000});
void fly() => print('يطير حتى ${maxAltitude} متر');
}
class SwimmingAbility {
final double maxDepth;
SwimmingAbility({this.maxDepth = 10});
void swim() => print('يسبح حتى عمق ${maxDepth} متر');
}
class RunningAbility {
final double maxSpeed;
RunningAbility({this.maxSpeed = 30});
void run() => print('يركض بسرعة ${maxSpeed} كم/ساعة');
}
// الحيوانات لديها قدرات (تركيب)
class Animal {
final String name;
final FlyingAbility? flying;
final SwimmingAbility? swimming;
final RunningAbility? running;
Animal(this.name, {this.flying, this.swimming, this.running});
void describe() {
print('$name يستطيع:');
if (flying != null) flying!.fly();
if (swimming != null) swimming!.swim();
if (running != null) running!.run();
}
}
void main() {
// البطة: تطير وتسبح
final duck = Animal('البطة',
flying: FlyingAbility(maxAltitude: 500),
swimming: SwimmingAbility(maxDepth: 2),
);
// البطريق: يسبح ويركض لكن لا يطير
final penguin = Animal('البطريق',
swimming: SwimmingAbility(maxDepth: 50),
running: RunningAbility(maxSpeed: 12),
);
// السمكة الطائرة: تطير وتسبح
final flyingFish = Animal('السمكة الطائرة',
flying: FlyingAbility(maxAltitude: 5),
swimming: SwimmingAbility(maxDepth: 100),
);
duck.describe();
// البطة يستطيع:
// يطير حتى 500 متر
// يسبح حتى عمق 2 متر
penguin.describe();
// البطريق يستطيع:
// يسبح حتى عمق 50 متر
// يركض بسرعة 12 كم/ساعة
flyingFish.describe();
// السمكة الطائرة يستطيع:
// يطير حتى 5 متر
// يسبح حتى عمق 100 متر
}
ClimbingAbility) لا تتطلب أي تغييرات في الحيوانات الموجودة -- فقط مررها للحيوانات التي تحتاجها.متى تستخدم الوراثة مقابل التركيب
كلاهما له مكانه. إليك إرشادات للاختيار:
دليل القرار
// استخدم الوراثة عندما:
// 1. هناك علاقة "هو-نوع" حقيقية
// 2. الفئة الفرعية هي حقاً نسخة متخصصة من الأب
// 3. تريد إعادة استخدام تنفيذ وواجهة الأب
// 4. التسلسل الهرمي ضحل (2-3 مستويات كحد أقصى)
abstract class Shape {
double get area;
double get perimeter;
}
class Circle extends Shape {
final double radius;
Circle(this.radius);
@override
double get area => 3.14159 * radius * radius;
@override
double get perimeter => 2 * 3.14159 * radius;
}
// Circle هو-نوع Shape -- هذا صحيح
// استخدم التركيب عندما:
// 1. هناك علاقة "لديه" أو "يستخدم"
// 2. تحتاج لدمج سلوكيات متعددة
// 3. السلوكيات قد تتغير وقت التشغيل
// 4. تريد مشاركة السلوك بين فئات غير مرتبطة
class GameCharacter {
final String name;
int health;
MovementStrategy movement; // لديه استراتيجية حركة
AttackStrategy attack; // لديه استراتيجية هجوم
final Inventory inventory; // لديه مخزون
GameCharacter({
required this.name,
this.health = 100,
required this.movement,
required this.attack,
required this.inventory,
});
}
// الشخصية لديها حركة ولديها هجمات ولديها مخزون
// الشخصية ليست حركة أو هجوماً
نمط التفويض
التفويض هو تركيب حيث يُحوّل كائن استدعاءات الطرق إلى كائن آخر يحتويه. الفئة الخارجية تُقدّم نفس الواجهة لكنها تُفوّض العمل الفعلي. هذا يسمح لك بتبديل التنفيذات دون تغيير الفئة الخارجية.
التفويض في الممارسة
// واجهة للتخزين
abstract class Storage {
Future<void> save(String key, String value);
Future<String?> load(String key);
Future<void> delete(String key);
}
// تنفيذات ملموسة
class MemoryStorage implements Storage {
final Map<String, String> _data = {};
@override
Future<void> save(String key, String value) async {
_data[key] = value;
print('[ذاكرة] حُفظ "$key"');
}
@override
Future<String?> load(String key) async {
print('[ذاكرة] تحميل "$key"');
return _data[key];
}
@override
Future<void> delete(String key) async {
_data.remove(key);
print('[ذاكرة] حُذف "$key"');
}
}
class FileStorage implements Storage {
@override
Future<void> save(String key, String value) async {
print('[ملف] كتابة "$key" على القرص');
}
@override
Future<String?> load(String key) async {
print('[ملف] قراءة "$key" من القرص');
return 'file_value';
}
@override
Future<void> delete(String key) async {
print('[ملف] إزالة "$key" من القرص');
}
}
// CacheService يُفوّض لتنفيذ Storage
class CacheService {
Storage _storage;
final Duration ttl;
CacheService({required Storage storage, this.ttl = const Duration(minutes: 5)})
: _storage = storage;
// يمكن تبديل التخزين وقت التشغيل!
void switchStorage(Storage newStorage) {
_storage = newStorage;
print('تم تبديل واجهة التخزين');
}
Future<void> cache(String key, String value) async {
await _storage.save(key, value);
await _storage.save('${key}_expiry', DateTime.now().add(ttl).toIso8601String());
}
Future<String?> get(String key) async {
return await _storage.load(key);
}
Future<void> invalidate(String key) async {
await _storage.delete(key);
await _storage.delete('${key}_expiry');
}
}
void main() async {
// ابدأ بتخزين الذاكرة (سريع، للتطوير)
final cache = CacheService(storage: MemoryStorage());
await cache.cache('user_name', 'Alice');
await cache.get('user_name');
// بدّل لتخزين الملفات (دائم، للإنتاج)
cache.switchStorage(FileStorage());
await cache.cache('user_name', 'Alice');
await cache.get('user_name');
}
// [ذاكرة] حُفظ "user_name"
// [ذاكرة] حُفظ "user_name_expiry"
// [ذاكرة] تحميل "user_name"
// تم تبديل واجهة التخزين
// [ملف] كتابة "user_name" على القرص
// [ملف] كتابة "user_name_expiry" على القرص
// [ملف] قراءة "user_name" من القرص
CacheService لا يرث من Storage -- بل يحتوي على كائن Storage ويُفوّض الاستدعاءات إليه. هذا يعني أنك تستطيع التبديل من MemoryStorage إلى FileStorage (أو حتى RedisStorage) دون تغيير سطر واحد في CacheService. هذه هي قوة التفويض.أساسيات حقن الاعتماديات
حقن الاعتماديات (DI) هو ممارسة تمرير الاعتماديات إلى كائن بدلاً من السماح للكائن بإنشائها داخلياً. إنه تركيب مطبق على بناء الكائنات. حقن الاعتماديات يجعل الكود أكثر قابلية للاختبار والمرونة وأقل اقتراناً.
حقن الاعتماديات
// بدون حقن الاعتماديات -- اقتران محكم
class BadUserService {
// ينشئ اعتماديته بنفسه -- مستحيل الاختبار أو التبديل
final _api = HttpApiClient();
Future<String> getUsername(int id) async {
final response = await _api.get('/users/$id');
return response['name'];
}
}
// مع حقن الاعتماديات -- اقتران فضفاض
abstract class ApiClient {
Future<Map<String, dynamic>> get(String path);
}
class HttpApiClient implements ApiClient {
@override
Future<Map<String, dynamic>> get(String path) async {
print('HTTP GET: $path');
return {'name': 'Alice'}; // محاكاة
}
}
class MockApiClient implements ApiClient {
@override
Future<Map<String, dynamic>> get(String path) async {
return {'name': 'مستخدم اختبار'};
}
}
class UserService {
final ApiClient _client; // الاعتمادية محقونة
UserService(this._client); // عبر المُنشئ
Future<String> getUsername(int id) async {
final response = await _client.get('/users/$id');
return response['name'] ?? 'غير معروف';
}
}
void main() async {
// في الإنتاج:
final prodService = UserService(HttpApiClient());
print(await prodService.getUsername(1));
// HTTP GET: /users/1
// Alice
// في الاختبارات:
final testService = UserService(MockApiClient());
print(await testService.getUsername(1));
// مستخدم اختبار -- لا حاجة لاستدعاء HTTP!
}
get_it (محدد الخدمات)، provider، و riverpod كلها تسهّل حقن الاعتماديات. عندما تُمرر Repository إلى Bloc، أو تُسجّل خدمة مع GetIt، فأنت تقوم بحقن الاعتماديات -- تمنح الكائنات اعتمادياتها من الخارج بدلاً من إنشائها داخلياً.مثال عملي: نظام كيانات اللعبة
لنبنِ نظام كيانات لعبة يوضح التركيب والتفويض وحقن الاعتماديات يعملون معاً:
نظام كيانات اللعبة بالتركيب
// سلوكيات قابلة للتركيب (مكونات)
abstract class MovementBehavior {
String get type;
void move(String direction);
}
class WalkMovement implements MovementBehavior {
@override
String get type => 'مشي';
@override
void move(String direction) => print(' يمشي $direction');
}
class FlyMovement implements MovementBehavior {
@override
String get type => 'طيران';
@override
void move(String direction) => print(' يطير $direction');
}
class SwimMovement implements MovementBehavior {
@override
String get type => 'سباحة';
@override
void move(String direction) => print(' يسبح $direction');
}
abstract class AttackBehavior {
String get type;
int get damage;
void attack(String target);
}
class MeleeAttack implements AttackBehavior {
@override
String get type => 'قتال قريب';
@override
int get damage => 25;
@override
void attack(String target) => print(' ضربة سيف على $target! ($damage ضرر)');
}
class RangedAttack implements AttackBehavior {
@override
String get type => 'قتال بعيد';
@override
int get damage => 15;
@override
void attack(String target) => print(' رمية سهم على $target! ($damage ضرر)');
}
class MagicAttack implements AttackBehavior {
@override
String get type => 'سحر';
@override
int get damage => 40;
@override
void attack(String target) => print(' كرة نارية على $target! ($damage ضرر)');
}
// كيان مركب من السلوكيات
class GameEntity {
final String name;
int health;
MovementBehavior movement;
AttackBehavior attack;
GameEntity({
required this.name,
this.health = 100,
required this.movement,
required this.attack,
});
void performMove(String direction) {
print('$name (صحة: $health):');
movement.move(direction);
}
void performAttack(String target) {
print('$name يهاجم:');
attack.attack(target);
}
// السلوكيات يمكن تبديلها وقت التشغيل!
void equipWeapon(AttackBehavior newAttack) {
print('$name يبدل من ${attack.type} إلى ${newAttack.type}');
attack = newAttack;
}
void gainAbility(MovementBehavior newMovement) {
print('$name يكتسب قدرة ${newMovement.type}!');
movement = newMovement;
}
}
void main() {
// محارب: يمشي + قتال قريب
final warrior = GameEntity(
name: 'المحارب',
health: 120,
movement: WalkMovement(),
attack: MeleeAttack(),
);
// ساحر: يمشي + سحر
final mage = GameEntity(
name: 'الساحر',
health: 80,
movement: WalkMovement(),
attack: MagicAttack(),
);
// تنين: يطير + قتال قريب (مخالب)
final dragon = GameEntity(
name: 'التنين',
health: 300,
movement: FlyMovement(),
attack: MeleeAttack(),
);
warrior.performMove('شمالاً');
warrior.performAttack('التنين');
// المحارب يجد عصا سحرية!
warrior.equipWeapon(MagicAttack());
warrior.performAttack('التنين');
// الساحر يشرب جرعة طيران
mage.gainAbility(FlyMovement());
mage.performMove('للأعلى');
}
// المحارب (صحة: 120):
// يمشي شمالاً
// المحارب يهاجم:
// ضربة سيف على التنين! (25 ضرر)
// المحارب يبدل من قتال قريب إلى سحر
// المحارب يهاجم:
// كرة نارية على التنين! (40 ضرر)
// الساحر يكتسب قدرة طيران!
// الساحر (صحة: 80):
// يطير للأعلى
Warrior التي ترث من MeleeCharacter لن تستطيع أبداً اكتساب السحر. ستحتاج لإنشاء MeleeWarrior و MagicWarrior و FlyingMeleeWarrior إلخ -- انفجار توافقي. مع التركيب، تبدل ببساطة كائن attack. الكيان لا يهتم بنوع الهجوم الذي لديه؛ يُفوّض فقط لأي AttackBehavior يحمله حالياً.Circle حقاً هو-نوع Shape ولن يحتاج أبداً لأن يكون شيئاً آخر، الوراثة هي الأداة الصحيحة. القاعدة هي “فضّل التركيب” وليس “استخدم دائماً التركيب.”