البرمجة كائنية التوجه في Dart

الفئات المجردة والواجهات

45 دقيقة الدرس 5 من 8

ما هي الفئات المجردة؟

الفئة المجردة هي فئة لا يمكن إنشاء نسخة منها مباشرة -- لا يمكنك إنشاء كائن منها باستخدام new. بدلاً من ذلك تعمل كمخطط يجب على الفئات الأخرى أن ترثه وتكمله. الفئات المجردة يمكن أن تحتوي على طرق مجردة (بدون تنفيذ فقط التوقيع) وطرق ملموسة (بتنفيذ كامل). استخدم الكلمة المفتاحية abstract قبل class.

فكر في الفئة المجردة مثل وصفة غير مكتملة: تخبرك بالخطوات (توقيعات الطرق) لكن بعض الخطوات تقول “أنت تقرر كيف تفعل هذا الجزء” (الطرق المجردة). كل فئة فرعية يجب أن تملأ تلك الفراغات.

أول فئة مجردة لك

// لا يمكن إنشاء: Shape() -- إنها مجردة
abstract class Shape {
  String color;

  Shape(this.color);

  // طريقة مجردة -- بدون جسم فقط التوقيع
  // الفئات الفرعية يجب أن تنفذها
  double get area;

  // طريقة مجردة
  double get perimeter;

  // طريقة ملموسة -- لها جسم ترثها الفئات الفرعية
  void describe() {
    print('شكل $color: المساحة=${area.toStringAsFixed(2)}، المحيط=${perimeter.toStringAsFixed(2)}');
  }
}

class Circle extends Shape {
  double radius;

  Circle(String color, this.radius) : super(color);

  @override
  double get area => 3.14159 * radius * radius;

  @override
  double get perimeter => 2 * 3.14159 * radius;
}

class Rectangle extends Shape {
  double width;
  double height;

  Rectangle(String color, this.width, this.height) : super(color);

  @override
  double get area => width * height;

  @override
  double get perimeter => 2 * (width + height);
}

void main() {
  // Shape s = Shape('أحمر');  // خطأ! لا يمكن إنشاء فئة مجردة

  Circle c = Circle('أزرق', 5);
  Rectangle r = Rectangle('أخضر', 10, 4);

  c.describe();  // شكل أزرق: المساحة=78.54، المحيط=31.42
  r.describe();  // شكل أخضر: المساحة=40.00، المحيط=28.00

  // يمكن استخدام النوع المجرد كنوع متغير
  List<Shape> shapes = [c, r];
  double totalArea = shapes.fold(0, (sum, s) => sum + s.area);
  print('المساحة الإجمالية: ${totalArea.toStringAsFixed(2)}');  // 118.54
}
قاعدة أساسية: إذا ورثت فئة فرعية من فئة مجردة يجب أن تنفذ جميع الطرق المجردة. إذا لم تفعل يجب أن تُعلن الفئة الفرعية نفسها abstract. المترجم سيعطيك خطأ إذا نسيت تنفيذ طريقة مجردة.

الطرق المجردة مقابل الطرق الملموسة

الفئة المجردة يمكنها مزج النوعين بحرية:

مزج المجرد والملموس

abstract class Animal {
  String name;

  Animal(this.name);

  // مجردة -- كل حيوان يصدر صوتاً مختلفاً
  void makeSound();

  // مجردة -- كل حيوان يتحرك بشكل مختلف
  void move();

  // ملموسة -- كل الحيوانات تأكل بنفس الطريقة
  void eat(String food) {
    print('$name يأكل $food.');
  }

  // ملموسة تعتمد على مجردة
  void introduce() {
    print('أنا $name.');
    makeSound();  // تستدعي تنفيذ الفئة الفرعية!
    move();
  }
}

class Dog extends Animal {
  Dog(String name) : super(name);

  @override
  void makeSound() => print('$name: هاو! هاو!');

  @override
  void move() => print('$name يركض على أربع أرجل.');
}

class Bird extends Animal {
  Bird(String name) : super(name);

  @override
  void makeSound() => print('$name: تغريد! تغريد!');

  @override
  void move() => print('$name يطير في الهواء.');
}

void main() {
  Dog dog = Dog('ريكس');
  Bird bird = Bird('تويتي');

  dog.introduce();
  // أنا ريكس.
  // ريكس: هاو! هاو!
  // ريكس يركض على أربع أرجل.

  bird.introduce();
  // أنا تويتي.
  // تويتي: تغريد! تغريد!
  // تويتي يطير في الهواء.

  // الطريقة الملموسة موروثة
  dog.eat('عظام');   // ريكس يأكل عظام.
}
نمط قوي: لاحظ كيف أن introduce() طريقة ملموسة تستدعي الطرق المجردة makeSound() و move(). الأب يحدد تسلسل العمليات بينما الفئات الفرعية تحدد السلوك المحدد. هذا يسمى نمط طريقة القالب -- أحد أكثر أنماط التصميم فائدة في OOP.

الواجهات في Dart

على عكس Java أو C# لا تملك Dart كلمة مفتاحية interface منفصلة. بدلاً من ذلك كل فئة هي ضمنياً واجهة. أي فئة يمكن استخدامها كواجهة مع الكلمة المفتاحية implements. عندما تنفذ فئة يجب أن تتجاوز جميع أعضائها -- الخصائص والطرق.

استخدام implements

// هذه الفئة تعمل تلقائياً كواجهة
class Printable {
  void printFormatted() {
    print('مخرجات الطباعة الافتراضية');
  }
}

class Savable {
  void save() {
    print('حفظ افتراضي');
  }

  void delete() {
    print('حذف افتراضي');
  }
}

// ينفذ كلاهما -- يجب تجاوز جميع الأعضاء من كليهما
class Document implements Printable, Savable {
  String title;
  String content;

  Document(this.title, this.content);

  @override
  void printFormatted() {
    print('=== $title ===');
    print(content);
    print('===============');
  }

  @override
  void save() {
    print('حفظ المستند "$title" على القرص...');
  }

  @override
  void delete() {
    print('حذف المستند "$title"...');
  }
}

void main() {
  Document doc = Document('تقريري', 'هذا هو المحتوى.');
  doc.printFormatted();
  doc.save();

  // يمكن استخدام أنواع الواجهات
  Printable p = doc;
  p.printFormatted();  // يعمل! Doc ينفذ Printable
}
extends مقابل implements:
extends -- وراثة التنفيذ. تحصل على كل كود الأب مجاناً وتتجاوز فقط ما تريد تغييره. يمكن وراثة فئة واحدة فقط.
implements -- الوعد بتوفير التنفيذ. لا تحصل على شيء مجاناً ويجب تجاوز كل عضو. يمكن تنفيذ عدة فئات.
استخدم extends عندما تريد إعادة استخدام الكود. استخدم implements عندما تريد ضمان أن فئة تتبع عقداً.

الفئات المجردة كواجهات

أكثر نمط شيوعاً هو تعريف فئات مجردة خصيصاً لاستخدامها كواجهات. هذا يعطيك طرقاً مجردة (العقد) بدون أي تنفيذ يحتاج للتجاوز.

نمط الواجهة المجردة

// تعريف العقود بفئات مجردة
abstract class AuthService {
  Future<bool> login(String email, String password);
  Future<void> logout();
  bool get isLoggedIn;
  String? get currentUserEmail;
}

// تنفيذ Firebase
class FirebaseAuth implements AuthService {
  bool _loggedIn = false;
  String? _email;

  @override
  Future<bool> login(String email, String password) async {
    print('Firebase: تسجيل دخول $email...');
    _loggedIn = true;
    _email = email;
    return true;
  }

  @override
  Future<void> logout() async {
    _loggedIn = false;
    _email = null;
  }

  @override
  bool get isLoggedIn => _loggedIn;

  @override
  String? get currentUserEmail => _email;
}

// تنفيذ وهمي للاختبار
class MockAuth implements AuthService {
  bool _loggedIn = false;

  @override
  Future<bool> login(String email, String password) async {
    _loggedIn = email == 'test@test.com' && password == 'password';
    return _loggedIn;
  }

  @override
  Future<void> logout() async => _loggedIn = false;

  @override
  bool get isLoggedIn => _loggedIn;

  @override
  String? get currentUserEmail => _loggedIn ? 'test@test.com' : null;
}

// الكود يعتمد على الواجهة وليس التنفيذ
class LoginScreen {
  final AuthService auth;  // يعمل مع أي AuthService

  LoginScreen(this.auth);

  Future<void> handleLogin(String email, String password) async {
    bool success = await auth.login(email, password);
    if (success) {
      print('مرحباً ${auth.currentUserEmail}!');
    } else {
      print('فشل تسجيل الدخول.');
    }
  }
}

void main() async {
  // الإنتاج: استخدم Firebase
  var screen = LoginScreen(FirebaseAuth());
  await screen.handleLogin('ahmed@test.com', 'secret123');

  // الاختبار: استخدم Mock
  var testScreen = LoginScreen(MockAuth());
  await testScreen.handleLogin('test@test.com', 'password');
}
لماذا هذا مهم في Flutter: هذا النمط يُستخدم في كل مكان في تطبيقات Flutter الاحترافية. تعرّف واجهة (فئة مجردة) وتنشئ تنفيذاً حقيقياً وتنفيذاً وهمياً ثم تبدلهما بسهولة للاختبار. حزم مثل get_it و provider تجعل هذا أقوى مع حقن التبعيات.

دمج extends و implements

يمكن للفئة أن ترث من أب وتنفذ واجهات في نفس الوقت:

extends + implements معاً

abstract class Loggable {
  void log(String message);
}

abstract class Serializable {
  Map<String, dynamic> toJson();
}

class BaseModel {
  final String id;
  final DateTime createdAt;

  BaseModel({required this.id}) : createdAt = DateTime.now();
}

// يرث من BaseModel وينفذ واجهتين
class User extends BaseModel implements Loggable, Serializable {
  String name;
  String email;

  User({required super.id, required this.name, required this.email});

  @override
  void log(String message) {
    print('[مستخدم:$id] $message');
  }

  @override
  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'email': email,
    'created_at': createdAt.toIso8601String(),
  };

  @override
  String toString() => 'مستخدم($name, $email)';
}

void main() {
  var user = User(id: 'u001', name: 'أحمد', email: 'ahmed@test.com');

  print(user.id);  // u001 (من BaseModel)
  user.log('تم تحديث الملف');  // [مستخدم:u001] تم تحديث الملف
  print(user.toJson());

  // يمكن استخدامه كأي من أنواعه
  Loggable loggable = user;
  Serializable serializable = user;
  BaseModel base = user;
}

الكلمة المفتاحية interface (Dart 3+)

قدم Dart 3 مُعدّلات الفئات لمزيد من التحكم:

مُعدّلات فئات Dart 3

// sealed class -- يمكن الوراثة فقط في نفس الملف (رائعة للتبديل الشامل)
sealed class Result {}

class Success extends Result {
  final String data;
  Success(this.data);
}

class Failure extends Result {
  final String error;
  Failure(this.error);
}

class Loading extends Result {}

String handleResult(Result result) {
  // Dart تعرف جميع الأنواع الفرعية الممكنة -- شامل!
  return switch (result) {
    Success(data: var d) => 'حصلنا على: $d',
    Failure(error: var e) => 'خطأ: $e',
    Loading() => 'جاري التحميل...',
  };
}

void main() {
  print(handleResult(Success('مرحبا')));  // حصلنا على: مرحبا
  print(handleResult(Failure('404')));     // خطأ: 404
  print(handleResult(Loading()));           // جاري التحميل...
}
ملخص مُعدّلات Dart 3:
abstract -- لا يمكن إنشاء نسخة مباشرة.
interface -- يمكن التنفيذ لكن لا يمكن الوراثة (خارج مكتبتها).
base -- يمكن الوراثة لكن لا يمكن التنفيذ (خارج مكتبتها).
final -- لا يمكن الوراثة أو التنفيذ (خارج مكتبتها).
sealed -- لا يمكن الوراثة خارج نفس الملف. تُمكّن مطابقة الأنماط الشاملة في switch.
mixin -- يمكن استخدامها كمزيج (تُغطى في الدرس التالي).

مثال عملي: بنية الإضافات

بناء نظام إضافات الدفع

// تعريف عقد الواجهة
abstract class PaymentGateway {
  String get name;
  bool get isAvailable;

  Future<PaymentResult> processPayment({
    required double amount,
    required String currency,
  });
}

class PaymentResult {
  final bool success;
  final String transactionId;
  final String? errorMessage;

  PaymentResult.success(this.transactionId)
      : success = true, errorMessage = null;

  PaymentResult.failure(this.errorMessage)
      : success = false, transactionId = '';

  @override
  String toString() => success
      ? 'الدفع تم (معاملة: $transactionId)'
      : 'فشل الدفع: $errorMessage';
}

class StripeGateway implements PaymentGateway {
  @override
  String get name => 'Stripe';

  @override
  bool get isAvailable => true;

  @override
  Future<PaymentResult> processPayment({
    required double amount,
    required String currency,
  }) async {
    print('Stripe: معالجة \$$amount $currency...');
    return PaymentResult.success('stripe_${DateTime.now().millisecondsSinceEpoch}');
  }
}

// خدمة الدفع تعتمد على الواجهة
class CheckoutService {
  final PaymentGateway gateway;

  CheckoutService(this.gateway);

  Future<void> checkout(double amount) async {
    if (!gateway.isAvailable) {
      print('${gateway.name} غير متاح.');
      return;
    }

    var result = await gateway.processPayment(amount: amount, currency: 'USD');
    print(result);
  }
}

void main() async {
  var checkout = CheckoutService(StripeGateway());
  await checkout.checkout(99.99);
}

تمرين عملي

افتح DartPad وابنِ: (1) أنشئ فئة مجردة DataRepository بطرق مجردة: Future<List<Map>> getAll() و Future<Map?> getById(String id) و Future<void> create(Map data) و Future<void> update(String id, Map data) و Future<void> delete(String id) وطريقة ملموسة exists(String id) تستدعي getById. (2) أنشئ InMemoryRepository implements DataRepository تخزن البيانات في List<Map>. (3) أنشئ LoggingRepository extends InMemoryRepository تتجاوز كل طريقة لطباعة سجل قبل استدعاء super. (4) استخدم متغير DataRepository للعمل مع كلا التنفيذين. (5) أنشئ فئة sealed ApiResponse بأنواع فرعية Success و Error و Loading وتعامل معها بتعبير switch شامل.