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

التغليف والتحكم في الوصول

40 دقيقة الدرس 8 من 8

ما هو التغليف؟

التغليف هو ممارسة تجميع البيانات (الخصائص) والطرق التي تعمل على تلك البيانات في وحدة واحدة (فئة) مع التحكم في الوصول للتفاصيل الداخلية. الفكرة بسيطة: أخفِ ما يجب إخفاؤه واكشف ما يحتاج للكشف. يجب أن يكون الكائن مثل آلة البيع -- تتفاعل عبر الأزرار (الواجهة العامة) لكن الآليات الداخلية (التنفيذ الخاص) مخفية ومحمية.

التغليف يمنع الكود الخارجي من وضع كائنك في حالة غير صالحة. بدونه يمكن لأي شخص تعيين bankAccount.balance = -1000000 مباشرة متجاوزاً كل منطق التحقق.

نموذج الخصوصية في Dart: خاص على مستوى المكتبة

التحكم في الوصول في Dart يختلف عن Java أو C++. هناك مستويان فقط:

  • عام -- بدون بادئة. قابل للوصول من أي مكان.
  • خاص على مستوى المكتبة -- بادئة شرطة سفلية (_). قابل للوصول فقط داخل نفس الملف (المكتبة).

لا يوجد protected أو package-private أو خاص على مستوى الفئة في Dart.

عام مقابل خاص

class User {
  // عام -- قابل للوصول من أي مكان
  String name;

  // خاص -- قابل للوصول فقط في هذا الملف
  String _password;
  int _loginAttempts = 0;

  User(this.name, this._password);

  // طريقة عامة -- الواجهة المتحكم فيها
  bool login(String password) {
    if (_loginAttempts >= 3) {
      print('الحساب مقفل. محاولات كثيرة جداً.');
      return false;
    }

    if (password == _password) {
      _loginAttempts = 0;
      print('مرحباً $name!');
      return true;
    }

    _loginAttempts++;
    print('كلمة مرور خاطئة. ${3 - _loginAttempts} محاولات متبقية.');
    return false;
  }

  // طريقة عامة لتغيير كلمة المرور مع التحقق
  bool changePassword(String oldPassword, String newPassword) {
    if (oldPassword != _password) {
      print('كلمة المرور الحالية غير صحيحة.');
      return false;
    }
    if (newPassword.length < 8) {
      print('كلمة المرور الجديدة يجب أن تكون 8 أحرف على الأقل.');
      return false;
    }
    _password = newPassword;
    print('تم تغيير كلمة المرور بنجاح.');
    return true;
  }
}

void main() {
  var user = User('أحمد', 'secret123');

  // الوصول العام يعمل
  print(user.name);       // أحمد
  user.login('secret123');  // مرحباً أحمد!

  // الوصول الخاص -- يعمل في نفس الملف
  print(user._password);  // secret123 (قابل للوصول هنا!)

  // لكن من ملف آخر:
  // print(user._password);  // خطأ! _password خاصة
}
فهم حاسم: الشرطة السفلية تجعل الأعضاء خاصة على مستوى الملف (المكتبة) وليس الفئة. الكود في نفس الملف يمكنه الوصول لـ _password مباشرة. هذا خيار تصميم Dart -- يبسط الاختبار ويبقي الأمور عملية. في تطبيق حقيقي كل فئة عادة في ملفها الخاص لذا الخاص على مستوى المكتبة يصبح فعلياً خاصاً على مستوى الفئة.

لماذا التغليف مهم

بدون تغليف (خطير)

// سيء -- بدون تغليف
class BankAccount {
  String owner;
  double balance;  // عام! أي شخص يمكنه التعديل مباشرة
  List<String> transactions;

  BankAccount(this.owner, this.balance) : transactions = [];
}

void main() {
  var account = BankAccount('أحمد', 1000);

  // أي شخص يمكنه فعل هذا -- بدون تحقق!
  account.balance = -999999;  // رصيد سالب؟ بالتأكيد!
  account.transactions.add('معاملة مزورة');  // تزوير السجل!
  account.owner = '';  // مالك فارغ؟ لا مشكلة!
}

مع تغليف (آمن)

// جيد -- مغلف بشكل صحيح
class BankAccount {
  final String _owner;
  double _balance;
  final List<String> _transactions = [];

  BankAccount(String owner, double initialBalance)
      : _owner = owner,
        _balance = initialBalance >= 0 ? initialBalance : 0;

  // وصول للقراءة فقط
  String get owner => _owner;
  double get balance => _balance;
  List<String> get transactions => List.unmodifiable(_transactions);

  // تغييرات متحكم فيها
  void deposit(double amount) {
    if (amount <= 0) throw ArgumentError('المبلغ يجب أن يكون موجباً');
    _balance += amount;
    _addTransaction('إيداع', amount);
  }

  bool withdraw(double amount) {
    if (amount <= 0) throw ArgumentError('المبلغ يجب أن يكون موجباً');
    if (amount > _balance) return false;
    _balance -= amount;
    _addTransaction('سحب', amount);
    return true;
  }

  // مساعد خاص -- للاستخدام الداخلي فقط
  void _addTransaction(String type, double amount) {
    _transactions.add('$type: \$${amount.toStringAsFixed(2)}');
  }

  @override
  String toString() => 'حساب($_owner, \$${_balance.toStringAsFixed(2)})';
}

void main() {
  var account = BankAccount('أحمد', 1000);

  // فقط العمليات الصالحة مسموحة
  account.deposit(500);          // حسناً
  account.withdraw(200);         // حسناً
  print(account.balance);        // 1300.0

  // العمليات غير الصالحة مستحيلة
  // account._balance = -999999;    // خطأ من ملفات أخرى!
  // account.balance = 999;         // خطأ! لا يوجد setter
}

أنماط التغليف

النمط 1: خصائص للقراءة فقط

عدة طرق للقراءة فقط

class Config {
  // الطريقة 1: final (يُعيّن مرة في المُنشئ)
  final String appName;

  // الطريقة 2: حقل خاص + getter فقط
  String _apiKey;
  String get apiKey => _apiKey;

  // الطريقة 3: getter محسوب (بدون حقل خلفي)
  String get displayName => '$appName v$_version';

  // الطريقة 4: const (ثابت وقت الترجمة)
  static const int maxRetries = 3;

  double _version;
  double get version => _version;

  Config({
    required this.appName,
    required String apiKey,
    required double version,
  })  : _apiKey = apiKey,
        _version = version;
}

النمط 2: Setters مع التحقق

Setters تفرض القواعد

class Product {
  String _name;
  double _price;
  int _stock;

  Product({required String name, required double price, int stock = 0})
      : _name = name, _price = price, _stock = stock;

  String get name => _name;
  set name(String value) {
    if (value.trim().isEmpty) throw ArgumentError('الاسم لا يمكن أن يكون فارغاً');
    _name = value.trim();
  }

  double get price => _price;
  set price(double value) {
    if (value < 0) throw ArgumentError('السعر لا يمكن أن يكون سالباً');
    _price = value;
  }

  int get stock => _stock;
  set stock(int value) {
    if (value < 0) throw ArgumentError('المخزون لا يمكن أن يكون سالباً');
    _stock = value;
  }

  bool get inStock => _stock > 0;
}

void main() {
  var p = Product(name: 'لابتوب', price: 999.99, stock: 10);
  p.name = 'ماك بوك برو';    // حسناً
  try {
    p.price = -50;  // يطرح ArgumentError
  } catch (e) {
    print(e);  // السعر لا يمكن أن يكون سالباً
  }
}

النمط 3: الكائنات غير القابلة للتغيير

فئات غير قابلة للتغيير بالكامل

class Coordinate {
  final double latitude;
  final double longitude;

  const Coordinate(this.latitude, this.longitude);

  // لا setters -- غير قابل للتغيير تماماً

  // إرجاع كائنات جديدة بدلاً من التعديل
  Coordinate move(double dLat, double dLng) {
    return Coordinate(latitude + dLat, longitude + dLng);
  }

  @override
  String toString() => '($latitude, $longitude)';
}

void main() {
  const home = Coordinate(24.4539, 54.3773);  // أبوظبي
  var office = home.move(0.01, 0.02);  // يُرجع إحداثية جديدة

  print(home);    // (24.4539, 54.3773) -- لم يتغير!
  print(office);  // (24.4639, 54.3973)
}
ارتباط Flutter: ودجات Flutter غير قابلة للتغيير. const Text('مرحبا') لا يمكن تغييره بعد الإنشاء. إذا أردت نصاً مختلفاً تنشئ ودجة Text جديدة. نمط عدم القابلية للتغيير يجعل محرك رسم Flutter فعالاً -- يمكنه تخطي إعادة بناء الودجات غير المتغيرة.

النمط 4: النسخ الدفاعية

حماية المجموعات الداخلية

class TodoList {
  final String _name;
  final List<String> _items = [];

  TodoList(this._name);

  String get name => _name;

  // خطأ: يكشف القائمة الحقيقية
  // List<String> get items => _items;

  // صحيح: إرجاع عرض غير قابل للتعديل
  List<String> get items => List.unmodifiable(_items);

  int get count => _items.length;

  void add(String item) {
    if (item.trim().isEmpty) return;
    _items.add(item.trim());
  }

  bool remove(String item) => _items.remove(item);
  void clear() => _items.clear();
}

void main() {
  var list = TodoList('التسوق');
  list.add('حليب');
  list.add('خبز');

  // آمن -- لا يمكن تعديل القائمة الداخلية
  var items = list.items;
  // items.add('اختراق');  // يطرح UnsupportedError!

  print(list.items);  // [حليب, خبز] -- لا تزال سليمة
}

إخفاء المعلومات عملياً

الفئة المغلفة جيداً تكشف واجهة عامة بسيطة وتخفي كل شيء آخر:

واجهة عامة بسيطة

class EmailService {
  // تفاصيل التنفيذ الخاصة
  final String _smtpHost;
  final int _smtpPort;
  final String _username;
  final String _password;
  bool _isConnected = false;

  EmailService({
    required String host,
    required int port,
    required String username,
    required String password,
  })  : _smtpHost = host,
        _smtpPort = port,
        _username = username,
        _password = password;

  // الواجهة العامة -- فقط 3 طرق مكشوفة
  Future<bool> send({
    required String to,
    required String subject,
    required String body,
  }) async {
    _ensureConnected();
    return _sendMail(to, subject, body);
  }

  bool get isReady => _isConnected;

  void disconnect() {
    if (_isConnected) {
      _closeConnection();
      _isConnected = false;
    }
  }

  // التنفيذ الخاص -- مخفي من الخارج
  void _ensureConnected() {
    if (!_isConnected) _connect();
  }

  void _connect() {
    print('الاتصال بـ $_smtpHost:$_smtpPort...');
    _isConnected = true;
  }

  void _closeConnection() {
    print('قطع الاتصال بـ $_smtpHost...');
  }

  Future<bool> _sendMail(String to, String subject, String body) async {
    print('إرسال "$subject" إلى $to...');
    return true;
  }
}

void main() async {
  var email = EmailService(
    host: 'smtp.gmail.com',
    port: 587,
    username: 'me@gmail.com',
    password: 'app-password',
  );

  // واجهة عامة بسيطة -- كل التعقيد مخفي
  await email.send(to: 'ahmed@example.com', subject: 'مرحبا!', body: 'هذا بريد تجريبي.');
  email.disconnect();

  // لا يمكن الوصول للداخليات:
  // email._password;       // خطأ من ملفات أخرى!
  // email._connect();      // خطأ من ملفات أخرى!
}
قاعدة عامة: إذا لم يكن العضو يحتاج للوصول من خارج الفئة اجعله خاصاً. ابدأ بكل شيء خاص واجعل الأشياء عامة فقط عندما يكون لديك سبب. من الأسهل بكثير جعل عضو خاص عاماً لاحقاً من جعل عضو عام خاصاً (لأن كوداً قد يعتمد عليه بالفعل).

مثال عملي: عربة التسوق

عربة تسوق مغلفة بالكامل

class CartItem {
  final String productId;
  final String name;
  final double price;
  int _quantity;

  CartItem({
    required this.productId,
    required this.name,
    required this.price,
    int quantity = 1,
  }) : _quantity = quantity > 0 ? quantity : 1;

  int get quantity => _quantity;
  set quantity(int value) {
    if (value < 1) throw ArgumentError('الكمية يجب أن تكون 1 على الأقل');
    _quantity = value;
  }

  double get subtotal => price * _quantity;

  @override
  String toString() => '$name x$_quantity = \$${subtotal.toStringAsFixed(2)}';
}

class ShoppingCart {
  final List<CartItem> _items = [];
  String? _couponCode;
  double _discountPercent = 0;

  List<CartItem> get items => List.unmodifiable(_items);
  int get itemCount => _items.fold(0, (sum, item) => sum + item.quantity);
  bool get isEmpty => _items.isEmpty;

  double get subtotal => _items.fold(0, (sum, item) => sum + item.subtotal);
  double get discount => subtotal * (_discountPercent / 100);
  double get total => subtotal - discount;

  void addItem(String productId, String name, double price, {int quantity = 1}) {
    var existing = _findItem(productId);
    if (existing != null) {
      existing.quantity = existing.quantity + quantity;
    } else {
      _items.add(CartItem(productId: productId, name: name, price: price, quantity: quantity));
    }
  }

  void removeItem(String productId) {
    _items.removeWhere((item) => item.productId == productId);
  }

  bool applyCoupon(String code) {
    var coupons = {'SAVE10': 10.0, 'SAVE20': 20.0, 'HALF': 50.0};
    if (coupons.containsKey(code.toUpperCase())) {
      _couponCode = code.toUpperCase();
      _discountPercent = coupons[code.toUpperCase()]!;
      return true;
    }
    return false;
  }

  void clear() {
    _items.clear();
    _couponCode = null;
    _discountPercent = 0;
  }

  CartItem? _findItem(String productId) {
    try {
      return _items.firstWhere((item) => item.productId == productId);
    } catch (_) {
      return null;
    }
  }
}

void main() {
  var cart = ShoppingCart();
  cart.addItem('p001', 'لابتوب', 999.99);
  cart.addItem('p002', 'ماوس', 29.99, quantity: 2);
  cart.applyCoupon('SAVE10');

  print('المجموع الفرعي: \$${cart.subtotal.toStringAsFixed(2)}');
  print('الخصم: \$${cart.discount.toStringAsFixed(2)}');
  print('الإجمالي: \$${cart.total.toStringAsFixed(2)}');
}

تمرين عملي

افتح DartPad وابنِ فئة PasswordManager: (1) حقل خاص _passwords (Map<String, String>) يخزن أزواج اسم-الموقع إلى كلمة-مرور-مشفرة. (2) طريقة خاصة _encrypt(String password) تُرجع نسخة “مشفرة” بسيطة (عكس النص + إضافة بادئة). (3) طريقة خاصة _decrypt(String encrypted) تعكس التشفير. (4) طريقة عامة store(String site, String password) تتحقق (8 أحرف على الأقل ويجب أن تحتوي رقماً) وتخزن مشفرة. (5) طريقة عامة retrieve(String site) تُرجع كلمة المرور المفكوكة أو null. (6) getter عام sites يُرجع قائمة أسماء المواقع (ليس كلمات المرور). (7) getter عام count. (8) طريقة عامة delete(String site). (9) تأكد أن لا طريقة تُرجع أو تكشف الخريطة الداخلية الخام. اختبر مع 3-4 مواقع.

اكتمل الدرس!

تهانينا! لقد أكملت جميع الدروس في هذا البرنامج التعليمي.