أنماط التصميم: المفرد والمصنع
ما هي أنماط التصميم؟
أنماط التصميم هي حلول مُثبتة وقابلة لإعادة الاستخدام لمشاكل شائعة في تصميم البرمجيات. ليست كوداً تنسخه وتلصقه -- إنها قوالب لحل تحديات التصميم المتكررة. في هذا الدرس، سنستكشف اثنين من أكثر أنماط الإنشاء أساسية: المفرد (ضمان وجود نسخة واحدة فقط) والمصنع (تفويض إنشاء الكائنات لطرق متخصصة).
نمط المفرد
المفرد يضمن أن الفئة لها بالضبط نسخة واحدة في التطبيق بأكمله، ويوفر نقطة وصول عامة لتلك النسخة. استخدمه عندما تحتاج لمورد مشترك واحد -- مثل مجمع اتصالات قاعدة البيانات، أو مسجل، أو تكوين التطبيق.
المفرد مع مُنشئ المصنع (أسلوب 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);
// هذا يضيف توجيهاً بدون فائدة
}
// المصانع مفيدة عندما:
// - تُعيد أنواعاً فرعية مختلفة بناءً على المدخلات
// - الإنشاء يتضمن منطقاً معقداً
// - تريد تخزين/إعادة استخدام النسخ
get_it أو provider أو riverpod بدلاً من المفردات الخام. هذه الأدوات تمنحك فوائد النسخ الواحدة مع الحفاظ على قابلية الاختبار والنمطية في كودك.