أنماط التصميم: المراقب والاستراتيجية
أنماط التصميم السلوكية
في الدرس السابق، استكشفنا أنماط الإنشاء (المفرد، المصنع) التي تركز على كيفية إنشاء الكائنات. الآن ننتقل إلى الأنماط السلوكية التي تركز على كيفية تواصل الكائنات وتقاسم المسؤوليات. نمط المراقب يتعامل مع إشعارات واحد-إلى-كثير، بينما نمط الاستراتيجية يجعل الخوارزميات قابلة للتبادل وقت التشغيل.
نمط المراقب
نمط المراقب يُعرّف اعتمادية واحد-إلى-كثير بين الكائنات: عندما يتغير حالة كائن (الموضوع)، يتم إشعار جميع تابعيه (المراقبين) تلقائياً. هذا هو أساس أنظمة الأحداث والبرمجة التفاعلية و ChangeNotifier في Flutter.
نمط المراقب الأساسي
// العقد الذي يجب على كل مراقب تحقيقه
abstract class Observer<T> {
void onUpdate(T data);
}
// الموضوع الذي يشترك فيه المراقبون
class Subject<T> {
final List<Observer<T>> _observers = [];
T? _state;
T? get state => _state;
void addObserver(Observer<T> observer) {
if (!_observers.contains(observer)) {
_observers.add(observer);
}
}
void removeObserver(Observer<T> observer) {
_observers.remove(observer);
}
void notify(T data) {
_state = data;
// أنشئ نسخة لتجنب مشاكل إذا أضاف/أزال المراقبون أثناء التكرار
for (final observer in List.of(_observers)) {
observer.onUpdate(data);
}
}
int get observerCount => _observers.length;
}
// موضوع ملموس: مستشعر حرارة
class TemperatureSensor extends Subject<double> {
void updateReading(double celsius) {
print('المستشعر: قراءة جديدة = ${celsius.toStringAsFixed(1)} درجة مئوية');
notify(celsius);
}
}
// مراقبون ملموسون
class DisplayPanel implements Observer<double> {
final String name;
DisplayPanel(this.name);
@override
void onUpdate(double celsius) {
print(' [$name] الحرارة: ${celsius.toStringAsFixed(1)} درجة مئوية');
}
}
class AlarmSystem implements Observer<double> {
final double threshold;
AlarmSystem(this.threshold);
@override
void onUpdate(double celsius) {
if (celsius > threshold) {
print(' [إنذار] الحرارة ${celsius.toStringAsFixed(1)} تتجاوز الحد ${threshold.toStringAsFixed(1)}!');
}
}
}
void main() {
final sensor = TemperatureSensor();
final display = DisplayPanel('الشاشة الرئيسية');
final alarm = AlarmSystem(30.0);
sensor.addObserver(display);
sensor.addObserver(alarm);
sensor.updateReading(25.5);
// المستشعر: قراءة جديدة = 25.5 درجة مئوية
// [الشاشة الرئيسية] الحرارة: 25.5 درجة مئوية
sensor.updateReading(35.2);
// المستشعر: قراءة جديدة = 35.2 درجة مئوية
// [الشاشة الرئيسية] الحرارة: 35.2 درجة مئوية
// [إنذار] الحرارة 35.2 تتجاوز الحد 30.0!
}
نمط ناقل الأحداث
نسخة أكثر مرونة من المراقب هي ناقل الأحداث -- مركز مركزي حيث يمكن لأي جزء من تطبيقك نشر أحداث وأي جزء آخر يمكنه الاشتراك في أنواع أحداث محددة. هذا يفصل الناشرين والمشتركين تماماً.
ناقل أحداث آمن النوع
// نوع الحدث الأساسي
abstract class AppEvent {
final DateTime timestamp;
AppEvent() : timestamp = DateTime.now();
}
// أحداث ملموسة
class UserLoggedIn extends AppEvent {
final String username;
UserLoggedIn(this.username);
}
class OrderPlaced extends AppEvent {
final String orderId;
final double total;
OrderPlaced(this.orderId, this.total);
}
class ErrorOccurred extends AppEvent {
final String message;
ErrorOccurred(this.message);
}
// اسم مستعار للنوع لمعالجات الأحداث
typedef EventHandler<T extends AppEvent> = void Function(T event);
// ناقل الأحداث
class EventBus {
// خريطة من نوع الحدث إلى قائمة المعالجات
final Map<Type, List<Function>> _handlers = {};
// الاشتراك في نوع حدث محدد
void on<T extends AppEvent>(EventHandler<T> handler) {
_handlers.putIfAbsent(T, () => []).add(handler);
}
// إلغاء اشتراك معالج
void off<T extends AppEvent>(EventHandler<T> handler) {
_handlers[T]?.remove(handler);
}
// نشر حدث لجميع المشتركين من ذلك النوع
void emit<T extends AppEvent>(T event) {
final handlers = _handlers[T];
if (handlers != null) {
for (final handler in List.of(handlers)) {
(handler as EventHandler<T>)(event);
}
}
}
// مسح جميع المعالجات
void dispose() => _handlers.clear();
}
void main() {
final bus = EventBus();
// الاشتراك في أنواع أحداث مختلفة
bus.on<UserLoggedIn>((event) {
print('مرحباً، ${event.username}!');
});
bus.on<OrderPlaced>((event) {
print('الطلب ${event.orderId}: \$${event.total.toStringAsFixed(2)}');
});
bus.on<ErrorOccurred>((event) {
print('خطأ: ${event.message}');
});
// أي جزء من التطبيق يمكنه إصدار أحداث
bus.emit(UserLoggedIn('alice'));
bus.emit(OrderPlaced('ORD-001', 49.99));
bus.emit(ErrorOccurred('فشل الدفع'));
// مرحباً، alice!
// الطلب ORD-001: $49.99
// خطأ: فشل الدفع
}
ChangeNotifier هي في الأساس تنفيذ للمراقب. عندما تستدعي notifyListeners()، جميع المستمعين المسجلين (العناصر) تُعاد بناؤها. مكتبات مثل bloc و riverpod تستخدم بنيات مشابهة مدفوعة بالأحداث داخلياً.نمط الاستراتيجية
نمط الاستراتيجية يُعرّف عائلة من الخوارزميات، يُغلّف كل واحدة كفئة منفصلة، ويجعلها قابلة للتبادل. كود العميل يمكنه تبديل الخوارزميات وقت التشغيل دون تغيير منطقه الخاص. هذا مثالي عندما لديك طرق متعددة لفعل شيء وتريد الاختيار وقت التشغيل.
استراتيجية الدفع
// واجهة الاستراتيجية
abstract class PaymentStrategy {
String get name;
bool validate();
Future<bool> processPayment(double amount);
}
// استراتيجيات ملموسة
class CreditCardPayment implements PaymentStrategy {
final String cardNumber;
final String expiryDate;
CreditCardPayment(this.cardNumber, this.expiryDate);
@override
String get name => 'بطاقة ائتمان';
@override
bool validate() {
return cardNumber.length == 16 && expiryDate.contains('/');
}
@override
Future<bool> processPayment(double amount) async {
print('معالجة \$$amount عبر بطاقة ائتمان تنتهي بـ ${cardNumber.substring(12)}');
await Future.delayed(Duration(milliseconds: 100));
return true;
}
}
class PayPalPayment implements PaymentStrategy {
final String email;
PayPalPayment(this.email);
@override
String get name => 'PayPal';
@override
bool validate() => email.contains('@');
@override
Future<bool> processPayment(double amount) async {
print('معالجة \$$amount عبر PayPal ($email)');
await Future.delayed(Duration(milliseconds: 100));
return true;
}
}
class CryptoPayment implements PaymentStrategy {
final String walletAddress;
CryptoPayment(this.walletAddress);
@override
String get name => 'عملة مشفرة';
@override
bool validate() => walletAddress.startsWith('0x') && walletAddress.length == 42;
@override
Future<bool> processPayment(double amount) async {
print('معالجة \$$amount عبر العملة المشفرة إلى ${walletAddress.substring(0, 10)}...');
await Future.delayed(Duration(milliseconds: 200));
return true;
}
}
// فئة السياق التي تستخدم الاستراتيجية
class PaymentProcessor {
PaymentStrategy? _strategy;
void setStrategy(PaymentStrategy strategy) {
_strategy = strategy;
}
Future<bool> checkout(double amount) async {
if (_strategy == null) {
print('لم يتم اختيار طريقة دفع!');
return false;
}
print('--- الدفع: \$$amount ---');
print('الطريقة: ${_strategy!.name}');
if (!_strategy!.validate()) {
print('فشل التحقق لـ ${_strategy!.name}');
return false;
}
return await _strategy!.processPayment(amount);
}
}
void main() async {
final processor = PaymentProcessor();
// المستخدم يختار بطاقة ائتمان
processor.setStrategy(CreditCardPayment('4111111111111234', '12/25'));
await processor.checkout(99.99);
// --- الدفع: $99.99 ---
// الطريقة: بطاقة ائتمان
// معالجة $99.99 عبر بطاقة ائتمان تنتهي بـ 1234
// المستخدم يبدل إلى PayPal
processor.setStrategy(PayPalPayment('alice@example.com'));
await processor.checkout(49.50);
// --- الدفع: $49.50 ---
// الطريقة: PayPal
// معالجة $49.50 عبر PayPal (alice@example.com)
}
مثال استراتيجية الفرز
حالة استخدام كلاسيكية أخرى هي خوارزميات الفرز القابلة للتبادل:
الفرز بنمط الاستراتيجية
// واجهة الاستراتيجية للفرز
abstract class SortStrategy<T> {
String get algorithmName;
List<T> sort(List<T> items, int Function(T a, T b) compare);
}
class BubbleSort<T> implements SortStrategy<T> {
@override
String get algorithmName => 'فرز الفقاعة';
@override
List<T> sort(List<T> items, int Function(T a, T b) compare) {
final result = List.of(items);
for (int i = 0; i < result.length - 1; i++) {
for (int j = 0; j < result.length - i - 1; j++) {
if (compare(result[j], result[j + 1]) > 0) {
final temp = result[j];
result[j] = result[j + 1];
result[j + 1] = temp;
}
}
}
return result;
}
}
class QuickSort<T> implements SortStrategy<T> {
@override
String get algorithmName => 'الفرز السريع';
@override
List<T> sort(List<T> items, int Function(T a, T b) compare) {
final result = List.of(items);
_quickSort(result, 0, result.length - 1, compare);
return result;
}
void _quickSort(List<T> arr, int low, int high, int Function(T, T) compare) {
if (low < high) {
int pivotIndex = _partition(arr, low, high, compare);
_quickSort(arr, low, pivotIndex - 1, compare);
_quickSort(arr, pivotIndex + 1, high, compare);
}
}
int _partition(List<T> arr, int low, int high, int Function(T, T) compare) {
T pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (compare(arr[j], pivot) <= 0) {
i++;
final temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
final temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
}
// السياق
class DataProcessor<T> {
SortStrategy<T> _strategy;
DataProcessor(this._strategy);
void setStrategy(SortStrategy<T> strategy) {
_strategy = strategy;
}
List<T> process(List<T> data, int Function(T a, T b) compare) {
print('الفرز بـ ${_strategy.algorithmName}...');
final sorted = _strategy.sort(data, compare);
return sorted;
}
}
void main() {
final data = [38, 27, 43, 3, 9, 82, 10];
final processor = DataProcessor<int>(BubbleSort());
print(processor.process(data, (a, b) => a.compareTo(b)));
// الفرز بـ فرز الفقاعة...
// [3, 9, 10, 27, 38, 43, 82]
// التبديل إلى الفرز السريع لمجموعات بيانات أكبر
processor.setStrategy(QuickSort());
print(processor.process(data, (a, b) => a.compareTo(b)));
// الفرز بـ الفرز السريع...
// [3, 9, 10, 27, 38, 43, 82]
}
دمج المراقب والاستراتيجية
التطبيقات الحقيقية غالباً ما تدمج الأنماط. إليك نظام إشعارات يستخدم المراقب للتسليم والاستراتيجية للتنسيق:
نمط مدمج -- نظام الإشعارات
// الاستراتيجية: كيف يتم تنسيق الرسالة
abstract class MessageFormatter {
String format(String title, String body, DateTime time);
}
class PlainTextFormatter implements MessageFormatter {
@override
String format(String title, String body, DateTime time) {
return '$title\n$body\n-- ${time.toIso8601String()}';
}
}
class HtmlFormatter implements MessageFormatter {
@override
String format(String title, String body, DateTime time) {
return '<h1>$title</h1><p>$body</p><small>$time</small>';
}
}
class JsonFormatter implements MessageFormatter {
@override
String format(String title, String body, DateTime time) {
return '{"title":"$title","body":"$body","time":"$time"}';
}
}
// المراقب: من يستقبل الإشعار
abstract class NotificationChannel {
final String name;
MessageFormatter formatter;
NotificationChannel(this.name, this.formatter);
void receive(String title, String body) {
final formatted = formatter.format(title, body, DateTime.now());
deliver(formatted);
}
void deliver(String formattedMessage);
}
class EmailChannel extends NotificationChannel {
EmailChannel(MessageFormatter fmt) : super('بريد', fmt);
@override
void deliver(String msg) => print(' [بريد] $msg');
}
class SlackChannel extends NotificationChannel {
SlackChannel(MessageFormatter fmt) : super('Slack', fmt);
@override
void deliver(String msg) => print(' [Slack] $msg');
}
// الموضوع: موزع الإشعارات
class NotificationHub {
final List<NotificationChannel> _channels = [];
void addChannel(NotificationChannel channel) => _channels.add(channel);
void removeChannel(NotificationChannel channel) => _channels.remove(channel);
void broadcast(String title, String body) {
print('البث: "$title"');
for (final channel in _channels) {
channel.receive(title, body);
}
}
}
void main() {
final hub = NotificationHub();
// كل قناة يمكنها استخدام استراتيجية تنسيق مختلفة
hub.addChannel(EmailChannel(HtmlFormatter()));
hub.addChannel(SlackChannel(PlainTextFormatter()));
hub.broadcast('اكتمل النشر', 'الإصدار 2.1.0 مباشر الآن!');
// البث: "اكتمل النشر"
// [بريد] <h1>اكتمل النشر</h1><p>الإصدار 2.1.0 مباشر الآن!</p>...
// [Slack] اكتمل النشر\nالإصدار 2.1.0 مباشر الآن!\n-- ...
}
ChangeNotifier + Provider هو في الأساس مراقب. AnimationController مع كائنات Curve مختلفة هو في الأساس استراتيجية. ListView.builder مع callback itemBuilder مخصص هو أيضاً استراتيجية (القائمة تفوض إنشاء العناصر لـ callback الخاص بك). تستخدم هذه الأنماط كل يوم في Flutter دون أن تدرك ذلك!VoidCallback أو ValueChanged<T>) أبسط وأوضح من إعداد مراقب كامل. استخدم نمط المراقب عندما يكون لديك حقاً مشتركون متعددون يتغيرون ديناميكياً وقت التشغيل.