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

أنماط التصميم: المفرد والمصنع

55 دقيقة الدرس 18 من 24

ما هي أنماط التصميم؟

أنماط التصميم هي حلول مُثبتة وقابلة لإعادة الاستخدام لمشاكل شائعة في تصميم البرمجيات. ليست كوداً تنسخه وتلصقه -- إنها قوالب لحل تحديات التصميم المتكررة. في هذا الدرس، سنستكشف اثنين من أكثر أنماط الإنشاء أساسية: المفرد (ضمان وجود نسخة واحدة فقط) والمصنع (تفويض إنشاء الكائنات لطرق متخصصة).

نمط المفرد

المفرد يضمن أن الفئة لها بالضبط نسخة واحدة في التطبيق بأكمله، ويوفر نقطة وصول عامة لتلك النسخة. استخدمه عندما تحتاج لمورد مشترك واحد -- مثل مجمع اتصالات قاعدة البيانات، أو مسجل، أو تكوين التطبيق.

المفرد مع مُنشئ المصنع (أسلوب Dart)

class AppConfig {
  // 1. حقل ثابت خاص يحمل النسخة الوحيدة
  static final AppConfig _instance = AppConfig._internal();

  // 2. مُنشئ المصنع يُعيد دائماً نفس النسخة
  factory AppConfig() {
    return _instance;
  }

  // 3. مُنشئ مسمى خاص -- يُستدعى مرة واحدة فقط
  AppConfig._internal();

  // حقول النسخة
  String apiBaseUrl = 'https://api.example.com';
  bool debugMode = false;
  int maxRetries = 3;

  void configure({String? apiUrl, bool? debug, int? retries}) {
    if (apiUrl != null) apiBaseUrl = apiUrl;
    if (debug != null) debugMode = debug;
    if (retries != null) maxRetries = retries;
  }
}

void main() {
  // كلا المتغيرين يشيران إلى نفس الكائن
  final config1 = AppConfig();
  final config2 = AppConfig();

  print(identical(config1, config2)); // true -- نفس النسخة!

  config1.configure(apiUrl: 'https://prod.api.com', debug: true);
  print(config2.apiBaseUrl); // https://prod.api.com
  print(config2.debugMode);  // true -- التغييرات تنعكس في كل مكان
}
كيف يعمل: الكلمة المفتاحية factory تخبر Dart أن هذا المُنشئ لا ينشئ دائماً نسخة جديدة. بدلاً من ذلك، يُعيد _instance الذي أُنشئ مرة واحدة بواسطة المُنشئ الخاص _internal(). كل استدعاء لـ AppConfig() يُعيد نفس الكائن.

المفرد الكسول

المثال أعلاه هو مفرد حريص -- النسخة تُنشأ عند تحميل الفئة. أحياناً تريد مفرداً كسولاً يُنشأ فقط عند الوصول إليه لأول مرة، خاصة إذا كان التهيئة مكلفة.

نمط المفرد الكسول

class DatabaseConnection {
  // النسخة فارغة حتى أول وصول
  static DatabaseConnection? _instance;

  // تتبع حالة الاتصال
  final String connectionString;
  bool _isConnected = false;

  // مُنشئ المصنع مع تهيئة كسولة
  factory DatabaseConnection(String connStr) {
    // أنشئ النسخة فقط في الاستدعاء الأول
    _instance ??= DatabaseConnection._internal(connStr);
    return _instance!;
  }

  DatabaseConnection._internal(this.connectionString);

  Future<void> connect() async {
    if (_isConnected) return;
    print('جارٍ الاتصال بـ $connectionString...');
    // محاكاة تأخير الاتصال
    await Future.delayed(Duration(milliseconds: 100));
    _isConnected = true;
    print('تم الاتصال!');
  }

  bool get isConnected => _isConnected;
}

void main() async {
  // الاستدعاء الأول ينشئ النسخة
  final db1 = DatabaseConnection('postgresql://localhost/mydb');
  await db1.connect();

  // الاستدعاء الثاني يُعيد نفس النسخة (يتم تجاهل المعامل)
  final db2 = DatabaseConnection('mysql://other/db');
  print(identical(db1, db2));   // true
  print(db2.connectionString);  // postgresql://localhost/mydb
  print(db2.isConnected);       // true
}
تحذير: لاحظ أن معامل الاستدعاء الثاني ('mysql://other/db') يُتجاهل بصمت لأن النسخة موجودة بالفعل. هذا مصدر شائع للارتباك. إذا كان مفردك يحتاج تكوينات مختلفة، فكّر في استخدام طريقة initialize() منفصلة بدلاً من تمرير معاملات للمُنشئ.

المفرد مع Getter ثابت

نهج بديل يستخدم getter ثابت مع الكلمة المفتاحية late للتهيئة الكسولة بدون مُنشئ مصنع:

مفرد Getter الثابت

class Logger {
  // late + static = تهيئة كسولة
  static late final Logger instance = Logger._();

  Logger._();

  final List<String> _logs = [];

  void log(String message) {
    final timestamp = DateTime.now().toIso8601String();
    final entry = '[$timestamp] $message';
    _logs.add(entry);
    print(entry);
  }

  void warning(String message) => log('تحذير: $message');
  void error(String message) => log('خطأ: $message');

  List<String> get history => List.unmodifiable(_logs);
  void clear() => _logs.clear();
}

void main() {
  // الوصول عبر getter الثابت
  Logger.instance.log('بدء التطبيق');
  Logger.instance.warning('ذاكرة منخفضة');

  // نفس النسخة في كل مكان
  final logger = Logger.instance;
  logger.error('فشل الشبكة');

  print('إجمالي السجلات: ${Logger.instance.history.length}'); // 3
}

نمط طريقة المصنع

نمط طريقة المصنع يفوّض إنشاء الكائنات لطريقة بدلاً من استدعاء مُنشئ مباشرة. هذا يسمح لك بإعادة أنواع فرعية مختلفة بناءً على المدخلات، وإخفاء منطق الإنشاء المعقد، وجعل كودك أسهل للتوسيع.

مصنع بسيط مع طريقة ثابتة

abstract class Notification {
  final String title;
  final String body;

  Notification(this.title, this.body);

  void send();

  // طريقة المصنع -- تُعيد النوع الفرعي الصحيح
  static Notification create(String channel, String title, String body) {
    return switch (channel) {
      'email' => EmailNotification(title, body),
      'sms'   => SmsNotification(title, body),
      'push'  => PushNotification(title, body),
      _       => throw ArgumentError('قناة غير معروفة: $channel'),
    };
  }
}

class EmailNotification extends Notification {
  EmailNotification(super.title, super.body);

  @override
  void send() => print('بريد: "$title" -- $body');
}

class SmsNotification extends Notification {
  SmsNotification(super.title, super.body);

  @override
  void send() => print('رسالة: "$title" -- $body');
}

class PushNotification extends Notification {
  PushNotification(super.title, super.body);

  @override
  void send() => print('إشعار: "$title" -- $body');
}

void main() {
  // المستدعي لا يحتاج لمعرفة أي فئة يُنشئ
  final channels = ['email', 'sms', 'push'];
  for (final ch in channels) {
    final notification = Notification.create(ch, 'مرحباً', 'أهلاً بك!');
    notification.send();
  }
  // بريد: "مرحباً" -- أهلاً بك!
  // رسالة: "مرحباً" -- أهلاً بك!
  // إشعار: "مرحباً" -- أهلاً بك!
}
نصيحة: طريقة المصنع ممتازة عند دمجها مع الفئات المختومة (من الدرس السابق). يمكنك إنشاء تسلسل هرمي مختوم واستخدام طريقة مصنع لإنتاج النوع الفرعي الصحيح مع الحفاظ على منطق الإنشاء مركزياً.

نمط المصنع المجرد

نمط المصنع المجرد يأخذ المفهوم أبعد -- يُعرّف واجهة لإنشاء عائلات من الكائنات المترابطة. هذا مفيد عندما تحتاج لإنشاء كائنات متعددة مترابطة يجب أن تكون متسقة مع بعضها (مثل سمات واجهة المستخدم أو عناصر واجهة خاصة بالمنصة).

المصنع المجرد -- نظام السمات

// المنتجات المجردة
abstract class Button {
  void render();
}

abstract class TextField {
  void render();
}

abstract class Card {
  void render();
}

// المصنع المجرد
abstract class ThemeFactory {
  Button createButton(String label);
  TextField createTextField(String placeholder);
  Card createCard(String title, String content);

  // طريقة مصنع للحصول على السمة الصحيحة
  static ThemeFactory getTheme(String theme) {
    return switch (theme) {
      'material' => MaterialThemeFactory(),
      'cupertino' => CupertinoThemeFactory(),
      _ => throw ArgumentError('سمة غير معروفة: $theme'),
    };
  }
}

// عائلة Material Design
class MaterialButton extends Button {
  final String label;
  MaterialButton(this.label);

  @override
  void render() => print('[زر Material: $label]');
}

class MaterialTextField extends TextField {
  final String placeholder;
  MaterialTextField(this.placeholder);

  @override
  void render() => print('[حقل نص Material: $placeholder]');
}

class MaterialCard extends Card {
  final String title, content;
  MaterialCard(this.title, this.content);

  @override
  void render() => print('[بطاقة Material: $title - $content]');
}

class MaterialThemeFactory extends ThemeFactory {
  @override
  Button createButton(String label) => MaterialButton(label);
  @override
  TextField createTextField(String ph) => MaterialTextField(ph);
  @override
  Card createCard(String t, String c) => MaterialCard(t, c);
}

// عائلة Cupertino (iOS)
class CupertinoButton extends Button {
  final String label;
  CupertinoButton(this.label);

  @override
  void render() => print('[زر Cupertino: $label]');
}

class CupertinoTextField extends TextField {
  final String placeholder;
  CupertinoTextField(this.placeholder);

  @override
  void render() => print('[حقل نص Cupertino: $placeholder]');
}

class CupertinoCard extends Card {
  final String title, content;
  CupertinoCard(this.title, this.content);

  @override
  void render() => print('[بطاقة Cupertino: $title - $content]');
}

class CupertinoThemeFactory extends ThemeFactory {
  @override
  Button createButton(String label) => CupertinoButton(label);
  @override
  TextField createTextField(String ph) => CupertinoTextField(ph);
  @override
  Card createCard(String t, String c) => CupertinoCard(t, c);
}

void main() {
  // غيّر هذا السطر فقط لتبديل سمة واجهة المستخدم بالكامل
  final factory = ThemeFactory.getTheme('material');

  final btn = factory.createButton('إرسال');
  final input = factory.createTextField('أدخل البريد الإلكتروني');
  final card = factory.createCard('مرحباً', 'أهلاً بك!');

  btn.render();    // [زر Material: إرسال]
  input.render();  // [حقل نص Material: أدخل البريد الإلكتروني]
  card.render();   // [بطاقة Material: مرحباً - أهلاً بك!]
}
رؤية أساسية: المصنع المجرد يضمن الاتساق. إذا استخدمت MaterialThemeFactory، جميع أزرارك وحقول النص والبطاقات ستكون بنمط Material. لا يمكنك مزج أزرار Material مع حقول نص Cupertino عن طريق الخطأ. في Flutter، هذا المفهوم يُبنى عليه الفرق بين MaterialApp و CupertinoApp.

أنماط مضادة يجب تجنبها

أنماط التصميم قوية، لكن سوء استخدامها يخلق مشاكل أكثر مما يحل:

الأنماط المضادة الشائعة

// النمط المضاد 1: المفرد لكل شيء
// سيء -- جعل UserService مفرداً يخلق اقتراناً محكماً
class UserService {
  static final UserService _i = UserService._();
  factory UserService() => _i;
  UserService._();

  // هذا صعب الاختبار! لا يمكنك محاكاته أو استبداله.
  Future<User> getUser(int id) { /* ... */ }
}

// أفضل -- استخدم حقن الاعتماديات
class UserService {
  final ApiClient _client;
  UserService(this._client); // احقن الاعتمادية

  Future<User> getUser(int id) => _client.get('/users/$id');
}

// النمط المضاد 2: مصنع الإله
// سيء -- مصنع واحد ينشئ كائنات غير مترابطة
class AppFactory {
  static Object create(String type) {
    return switch (type) {
      'user' => User(),
      'logger' => Logger(),
      'database' => Database(),
      'button' => Button(),
      _ => throw Error(),
    };
  }
}

// أفضل -- مصانع منفصلة للعائلات المترابطة
class NotificationFactory { /* بريد، رسالة، إشعار */ }
class ThemeFactory { /* أزرار، مدخلات، بطاقات */ }

// النمط المضاد 3: مصنع يُعيد نوعاً ملموساً
// سيء -- لا فائدة من استدعاء المُنشئ مباشرة
class UserFactory {
  static User create(String name) => User(name);
  // هذا يضيف توجيهاً بدون فائدة
}

// المصانع مفيدة عندما:
// - تُعيد أنواعاً فرعية مختلفة بناءً على المدخلات
// - الإنشاء يتضمن منطقاً معقداً
// - تريد تخزين/إعادة استخدام النسخ
تحذير: الخطأ الأول مع المفردات هو الإفراط في الاستخدام. ليست كل خدمة مشتركة يجب أن تكون مفردة. إذا لم تكن الفئة تحتاج حقاً لتكون فريدة عالمياً، فضّل حقن الاعتماديات. المفردات تجعل الاختبار أصعب لأنك لا تستطيع استبدالها بمحاكيات بسهولة. استخدم المفردات باعتدال -- عادةً للمسجلات والتكوين والتخزين المؤقت.
أفضل ممارسة: في تطبيقات Flutter، فضّل استخدام نظام حقن اعتماديات مثل get_it أو provider أو riverpod بدلاً من المفردات الخام. هذه الأدوات تمنحك فوائد النسخ الواحدة مع الحفاظ على قابلية الاختبار والنمطية في كودك.