البرمجة التفاعلية نهج جديد لتطوير التطبيقات ويحظى بضجة كبيرة اليوم. تعلّم كيف ترتقي بالكود لديك ليتفاعل مع التغييرات تلقائيًا، وطرقًا جديدة لمعالجة المشكلات ومعالجة البيانات غير المتزامنة كتدفقات.
قد تكون البرمجة التفاعلية نموذجًا غير مألوف، لكنها في الواقع ليست جديدة حقًا. بُني Microsoft Excel عليها قبل وقت طويل من انتشارها.
في السنوات الأخيرة، وجدت البرمجة التفاعلية طريقها إلى التطبيقات الحديثة عبر أشياء مثل إدارة الحالة في أطر عمل الويب وأدوات محركات ألعاب الفيديو.
يركّز هذا المقال على تنفيذ JavaScript لـ ReactiveX (Rx.js)، وهي مكتبة لبناء تطبيقات غير متزامنة ومعتمدة على الأحداث باستخدام البرمجة التفاعلية.
لكن قبل الحديث عن Rx.js، علينا تعلّم بعض الأمور مثل البرمجة التعريفية والوظيفية والتفاعلية، ونمط المراقب (Observer)، وObservables، وكيف تندمج جميعها معًا.
البرمجة الأمرية مقابل التعريفية
الخطوة الأولى نحو كتابة كود تفاعلي هي التفكير بشكل مختلف حول كتابة الكود. ربما سمعت هذين المصطلحين البرمجيين من قبل:
- البرمجة الأمرية (Imperative)
- البرمجة التعريفية (Declarative)
تعتمد البرمجة الأمرية على خطوات مفصلة يجب اتباعها لتنفيذ عملية ما. أما التعريفية فتعتمد فقط على ما يجب إنجازه.
لنلقِ نظرة على بضعة أمثلة عن الاثنين. في كل هذه الأمثلة، يقوم مقتطفا كود بنفس الشيء تمامًا، لكن بطريقة مختلفة:
# 1
النهج الأمري
const isPositiveNumber = currentNumber > 0 ? true : false;النهج التعريفي
const isPositiveNumber = currentNumber > 0; # 2
النهج الأمري
function calculateSum(a, b) {
const firstNumber = a;
const secondNumber = b;
const sum = firstNumber + secondNumber;
return sum;
}النهج التعريفي
function calculateSum(a, b) {
return a + b;
} # 3
النهج الأمري
const evenNumbers = [];
const allNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for(let i = 1; i <= allNumbers.length; i++) {
if (i % 2 === 0) {
evenNumbers.push(i);
}
}
// [ 2, 4, 6, 8, 10 ]النهج التعريفي
const allNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = allNumbers.filter(n => n % 2 === 0);
// [ 2, 4, 6, 8, 10 ]من الواضح أن البرمجة التعريفية ليست أقصر فحسب، بل تتطلب أيضًا نهجًا مختلفًا للموقف المطروح. فهي مباشرة حول ما يجب إنجازه بدلًا من كيف.
تشبه البرمجة التعريفية في عالم ReactiveX إلى حد كبير دوال الرتبة العليا في JavaScript (أو LINQ في C#)، حيث تكون كل عملية أسلوبًا مسلسلًا (chained) إلى الذي قبله.
from([1, 2, 3, 4, 5])
.pipe(
map(x => x * 10)
)
.subscribe((data) => console.log(data))
// 10, 20, 30, 40, 50تعتمد البرمجة التفاعلية في ReactiveX على Observables، والتي سنتوسع فيها لاحقًا.
صورة من Pixabay عبر Pexels
سير التحكم
تتيح لنا الصياغة التعريفية التركيز على الأمور واحدًا تلو الآخر بالترتيب الصحيح:
- الإنشاء (Create)
- التحويل (Transform)
- التنفيذ (Execute)
- الاكتمال (Complete)
هناك عدد قليل فقط من المتغيرات التي يجب تذكرها لأنها تظهر بالترتيب (في نفس السطر/خط الأنابيب). يمكننا الاطلاع على كل ما يجب أن يحدث فقط بقراءة الكود من الأعلى إلى الأسفل،
Producer() // Observable source
.pipe(
operation1(),
operation2(),
operation3(),
)
.subscribe()على عكس أن يكون المنطق مبعثرًا صعودًا وهبوطًا عبر الملف (النهج الأمري).
هنا، تُنقَل الحالة من عملية إلى أخرى، ما يعني أنه يمكنك بسهولة تتبعها إلى شكلها الأصلي.
التعديلات مرحّب بها أيضًا. يمكنك بسهولة تحويل أو جدولة أو تصفية القيم داخل الأنبوب (pipe) فقط بإضافة معاملات:
interval(1000)
.pipe(
map(value => value * 1000), // 0, 1, 2, 3, 4, * 1000
filter(value => value < 3000), // return not greater than 3000
take(5) // execute exactly 5 times
)
.subscribe((data) => console.log(data)) // 0, 1000, 2000Rx.js ليست تعريفية فقط، بل تفاعلية أيضًا. فهي تتفاعل مع التغييرات. لشرح ما يعنيه هذا سنلقي نظرة على نمط Observer بعد ذلك.
صورة من Atypeek Dgn عبر Pexels
نمط المراقب (Observer)
في تصميم وهندسة البرمجيات، نمط المراقب هو نمط تصميم برمجي يحتفظ فيه كائن، يُسمى Subject، بقائمة من المعتمدين عليه، تُسمى Observers، ويُخطرها تلقائيًا بأي تغييرات في الحالة، عادة عبر استدعاء إحدى دوالها. -ويكيبيديا
بعبارة بسيطة، لديك كائن أو دالة أو مكوّن واحد ينتج أحداثًا، وقائمة من المشتركين في تلك الأحداث يُستدعَون (تلقائيًا) كلما وصل حدث جديد.
مثال على نمط Observer
لنحاول التفكير في كيفية عمل يوتيوب. أنت كـ مستخدم يوتيوب يمكنك البحث عن الفيديوهات ومشاهدتها والتعليق عليها والإعجاب بها، لكن الجزء المثير للاهتمام هو أنه يمكنك (1) الاشتراك في بعض قنوات يوتيوب وسيتم (2) إخطارك كلما تم نشر فيديو جديد.
class YoutubeUser {
private currentUser;
get username() {
return this.currentUser;
}
constructor(user: string) {
this.currentUser = user;
}
searchVideos() {}
watchVideos() {}
// (1)
subscribeToChannel(youtubeChannel: YoutubeChannel) {
youtubeChannel.addSubscriber(this.currentUser);
}
// (2)
getVideoNotification(videoName: string, youtuber: string) {
console.log(`[${this.currentUser} Notification]:
${youtuber} has uploaded a new video: ${videoName}`);
}
}من جانب قناة يوتيوب، تحتفظ القناة بقائمة من (1) المشتركين، والتي (2) تُحدَّث عندما يشترك مستخدم يوتيوب في القناة. يُخطَر كل مشترك في القناة (من القائمة) عند (3) رفع فيديو جديد.
class YoutubeChannel {
private youtuber!: YoutubeUser;
// (1)
channelSubscribers: YoutubeUser[] = [];
get channelName() {
return this.youtuber.username;
}
constructor(channelName: string) {
this.youtuber = new YoutubeUser(channelName);
}
// (2)
addSubscriber(channelName: string) {
this.channelSubscribers.push(new YoutubeUser(channelName))
}
// (3)
uploadVideo(videoName: string) {
this.channelSubscribers
.forEach((subscriber: YoutubeUser) => subscriber
.getVideoNotification(videoName, this.youtuber.username))
// each subscriber is an instance of YoutubeUser from
// channelSubscribers array
// each calls it's getVideoNotification() method that
// will print notification whenever uploadVideo() is called
}
}لننشئ بعض قنوات يوتيوب:
const ninjaTurtlesChannel = new YoutubeChannel('Master Splinter');
const dragonBallChannel = new YoutubeChannel('Master Roshi');ولننشئ مستخدمين سيشتركون في هذه القنوات:
const youtubeUserLeo = new YoutubeUser('Leonardo')
const youtubeUserRaf = new YoutubeUser('Raphael')
const youtubeUserGoku = new YoutubeUser('Goku');
const youtubeUserKrillin = new YoutubeUser('Krillin');
// Youtube Users are added to the subscribers list of each channel
youtubeUserLeo.subscribeToChannel(ninjaTurtlesChannel);
youtubeUserRaf.subscribeToChannel(ninjaTurtlesChannel);
youtubeUserGoku.subscribeToChannel(dragonBallChannel);وماذا يحدث الآن؟ حسنًا، لا شيء. الآن ننتظر أن ترفع إحدى القنوات التي اشتركنا فيها فيديو جديدًا:
ninjaTurtlesChannel.uploadVideo('Ninjutsu lessons');
dragonBallChannel.uploadVideo('Teaching Kamehameha');عندما يُرفَع فيديو جديد، يُخطَر المشتركون فورًا:
[LOG]: "[Leonardo Notification]:
Master Splinter has uploaded a new video: Ninjutsu lessons"
[LOG]: "[Raphael Notification]:
Master Splinter has uploaded a new video: Ninjutsu lessons"
[LOG]: "[Goku Notification]:
Master Roshi has uploaded a new video: Teaching Kamehameha"إذا نُشر فيديو آخر من Master Splinter، سيُخطَر مشتركوه (Ninja Turtles) مرة أخرى:
ninjaTurtlesChannel.uploadVideo('Kung Fu lessons');
[LOG]: "[Leonardo Notification]:
Master Splinter has uploaded a new video: Kung Fu lessons"
[LOG]: "[Raphael Notification]:
Master Splinter has uploaded a new video: Kung Fu lessons" ما علاقة كل هذا بالبرمجة التفاعلية؟
إنها رياضيات بسيطة:
البرمجة التعريفية + نمط Observer + قائمة ضخمة من المعاملات = ReactiveX
في سياق البرمجة التفاعلية:
- قناة يوتيوب هي Subject - تحتفظ بقائمة من المشتركين وتنتج قيمًا (الفيديوهات)
- مستخدم يوتيوب هو Observer - يراقب التغييرات (الفيديوهات الجديدة)
- تُغلَّف البيانات المُمرَّرة بين الاثنين في Observable
ReactiveX مكتبة تُستخدَم لبناء تطبيقات غير متزامنة باستخدام تدفقات Observable.
هدفها هو جلب قوة البرمجة التفاعلية إلى تطبيقاتك، من خلال توفير حلول للغات البرمجة الرئيسية:
- Rx.js (JavaScript)
- RxJava (Java)
- Rx.NET (C# .NET)
- RxPy (Python)
- RxCpp (C++)، إلخ.
يستخدم ReactiveX نمط Observer لتتبع التغييرات ويجرّد ذلك المنطق باستخدام مبادئ تعريفية. هذا الجمع بين البرمجة التعريفية ونمط Observer هو ما يجعل التعامل مع البيانات غير المتزامنة سهلًا.
تم توليدها بواسطة MidJourney AI
الـ Observables
في صميم ReactiveX تكمن الـ Observables.
تُغلَّف البيانات المُمرَّرة من المُنتِج (Subject) إلى المستهلِك/المستقبِل (Observer) في Observable. يمثّل الـ Observable سلسلة من الأحداث تحدث على فترات لا تنتهي.
Observable حيث المُنتِج محطة راديو
مع ذلك يمكنك إلغاء الاشتراك من Observable في أي وقت. لاستخراج البيانات من Observable (أو لنقل تحويل Observable<T> إلى <T> عادية)، تضع استدعاءً لدالة subscribe() في النهاية. تُعيد دالة subscribe() دالة استدعاء (callback) تحمل البيانات الخام كمعامل أول لها.
of('Hello World') // creates Observable<string>
.pipe(
// in between you can transform Observable<string>
)
// here you get the raw data
.subscribe((data: string) => {
console.log(data); // 'Hello World'
})يرتبط نمط Observable ارتباطًا وثيقًا بنمط Observer. الفرق الرئيسي هو أنه في نمط Observer، يسجّل المراقبون لدى Subject لتلقي التحديثات، بينما في نمط Observable، يشترك المراقبون في Observable لتلقي القيم أو الأحداث المُصدَرة.
البرمجة التفاعلية هي البرمجة باستخدام تدفقات بيانات تُسمى Observables.
خصائص Observables
للعمل بفعالية مع Observables، نحتاج لفهم سلوكها بشكل كامل:
# الـ Observables كسولة (Lazy)
هذا يعني أنه بدون استدعاء subscribe() في النهاية، لن يُفعَّل الـ Observable أبدًا، ولن ينشئ استدعاء API أو يرسل بيانات إلى المستهلِكين أبدًا.
function getGreeting() {
return of('Hello World');
}
function sayGreeting() {
getGreeting()
// nothing will happen until you subscribe
} # متزامن وغير متزامن
يمكن للـ Observable أن يعمل بطريقة متزامنة أو غير متزامنة حسب المُنتِج. لا شك أن الـ Observable المُنشَأ من قيمة بدائية (مثل سلسلة نصية) متزامن، بينما الـ Observable المُنتَج من واجهة برمجة تطبيقات أو مؤقّت يكون غير متزامن. بالإضافة إلى ذلك، يمكن أيضًا تحويل الـ Observables من نوع إلى آخر باستخدام Schedulers.
# يمكن استدعاء الـ Observables عدة مرات
لأن الـ observables تعمل على دوال الاستدعاء (callbacks).
# يمكن إلغاء الاشتراك من الـ Observable
يمكن تحقيق ذلك بعدة طرق، إما باستخدام اشتراك:
const subscription$ = of('Hello').subscribe();
subscription$.unsubscribe();أو تفاعليًا بعد استدعاء الـ Observable عدة مرات:
of('Hello')
.pipe(
take(1) // or takeUntil, takeWhile
)
.subscribe();إذا قارنّا هذا بـ Promise في JavaScript، يمكننا أن نرى فورًا أن الـ Promises:
تُفعَّل عند استدعائها.
- تتصرف بطريقة حريصة (Eager)
- غير متزامنة فقط
- يمكنها معالجة عنصر بيانات واحد في كل مرة
يمكن أن تكون الـ Promises في إحدى ثلاث حالات: قيد الانتظار، أو مُحلَّة، أو مرفوضة.
- يمكن استدعاؤها مرة واحدة فقط في كل مرة
ما لم تكن تستخدم Abort Controller مع Fetch API.
- لا يمكن إلغاؤها
ويمكنك أن ترى كم من المساحة يغطيها الـ Observable.
صورة من Helena Jankovičová Kováčová عبر Pexels
العمل مع Observables
لنلقِ نظرة ثانية على Observables.
هناك نوعان من Observables:
- أحادي البث (بارد - Cold)
- متعدد البث (ساخن - Hot)
الفرق بين الاثنين هو كيفية مشاركة البيانات مع المشتركين الحاليين والمتأخرين. في الأول، يُستدعى الـ Observable كلما انضم مشترك جديد (اشترك في الـ Observable).
مع الـ Observables الساخنة، يحصل المشتركون الأوائل على البيانات، بينما لا يحصل المتأخرون على شيء حتى يُصدر الـ Observable مرة أخرى، وفي تلك الحالة يحصل كلاهما (الأوائل والمتأخرون) على نفس البيانات (الأحدث). هذا بالطبع يمكن أن يختلف حسب نوع المُنتِج (Subject).
الـ Observables باردة بطبيعتها، لكن يمكن تحويلها إلى ساخنة باستخدام Subjects أو مجموعة من المعاملات (مثل share()).
يمكن أن يكون الـ Observable في إحدى ثلاث حالات:
- حالة التالي (نشطة) (-a-a-a-a-)
- حالة الخطأ (-a-a-#)
- حالة الاكتمال (-a-a-|)
أثناء وجوده في حالة التالي، يمكن للـ Observable أن يُصدر البيانات باستمرار، ولهذا أشرنا إليه سابقًا كتدفق لا ينتهي. لكن، في حالة حدوث خطأ غير مُعالَج، سيتوقف الـ Observable عن إصدار البيانات. هناك أيضًا حالة مكتملة يُعتبر فيها الـ Observable قد انتهى بنجاح ولم يعد بإمكانه إصدار البيانات.
في أطر عمل الويب، يُنصَح بإلغاء الاشتراك من أي observables نشطة في المكونات التي لم تعد مستخدَمة لتجنّب تسرّبات الذاكرة.
اصطلاحات التسمية
عادةً ما ينتهي اسم المتغيّر الذي يحمل الـ Observable بعلامة $، مثل data$.
الإنشاء، التحويل، التنفيذ، الاكتمال
لإنشاء Observable جديد، نستخدم نسخة Observable التي تُعيد Observer كمعامل لدالة استدعاء.
const data$ = new Observable((observer: Subscriber<string>) => {
observer.next('Hello'); // emits strings as specified above
if (somethingWentWrong) {
observer.error('Something went wrong');
}
observer.complete();
})هناك بدائل مختصرة لإنشاء Observables مثل of() أو from().
بين مصدر الـ Observable ودالة subscribe()، يمكنك إنشاء دالة pipe تحمل معاملات ستحوّل أو تصفّي أو تُتلِف (تلغي الاشتراك) أو تجدول أو تدمج الـ Observables.
of([...])
.pipe(
// transformations
)
.subscribe()تُعيد دالة subscribe أيضًا كائن Observer يمكنه تتبع التغييرات.
data$.subscribe({
next: (data: string) => console.log(data), // Success
error: (error: unknown) => console.log(error), // Fail
complete: () => console.log('Observable completed') // Completion
});كما هو الحال سابقًا، هناك نسخة مختصرة لدالة subscribe.
// Success path
data$.subscribe((data: string) => console.log(data)); النهج المتزامن مقابل غير المتزامن في Rx
في تطبيق JavaScript النموذجي لديك، هناك فرق واضح في كيفية التعامل مع البيانات المتزامنة أو غير المتزامنة.
لكن Rx.js تستخدم نفس الواجهة البرمجية لكليهما. بالنظر إلى الكود أدناه، من الصعب معرفة ما إذا كان الكود غير متزامن أم لا.
of('Hello')
.pipe(
take(1)
)
.subscribe();هذا متزامن، بينما المثال أدناه غير متزامن:
timer(1000)
.pipe(
take(1)
)
.subscribe();حتى الأحداث المتزامنة بطبيعتها (مثل نص سلسلة نصية) يمكن تحويلها إلى غير متزامنة باستخدام المجدولات (schedulers) أو التأخيرات، أو باستدعاء واجهة برمجة تطبيقات في الخطوة التالية بناءً على سلسلة نصية مزوَّدة في المُنتِج (قيمة المصدر).
of('Hello', AsapScheduler) // async operation
.pipe(
take(1)
)
.subscribe();خلاصة القول، عند العمل مع تدفقات Observable في Rx.js لا يوجد فرق في معالجة البيانات المتزامنة مقارنة بغير المتزامنة.
إيجابيات وسلبيات ReactiveX
لنلقِ نظرة الآن على الإيجابيات والسلبيات ومتى قد ترغب في استخدام Rx.js:
الفوائد:
مع أكثر من 100 معامل تحت تصرفك، تشكّل المخرَج كما تشاء.
- وفرة من المعاملات
- البرمجة التعريفية
- الـ Observables
تُستخدَم Rx.js بكثافة في Angular، لكن يمكن أيضًا دمجها مع أطر عمل أخرى.
- دعم واسع للغات وأطر العمل
مع Rx، يمكنك الإلغاء وإعادة المحاولة والتخزين المؤقت بين المستهلِكين، وتأخير واجهة برمجة تطبيقات حتى تنتهي الأخرى الجارية، والتعامل مع الأخطاء، ومعالجة حالات التسابق (race conditions)، إلخ.
- واجهات برمجة تطبيقات أفضل
- نفس الواجهة البرمجية لمعالجة العمليات المتزامنة وغير المتزامنة
تدعم Rx مجموعة متنوعة من الـ Subjects التي توفّر حالة غير قابلة للتغيير يمكن مشاركتها بين المكونات.
- إدارة الحالة
يتيح لنا TestScheduler الخاص بـ Rx.js اختبار الكود غير المتزامن كما لو كان متزامنًا، باستخدام مخططات Marble، ومحاكاة التأخيرات ما ينتج عنه اختبارات أفضل أداءً.
- الاختبار
العيوب:
البرمجة التفاعلية صعبة الاعتياد عليها في البداية لأن المطورين غير معتادين على التعامل مع الأمور بهذه الطريقة.
- صعب الفهم
هذا أيضًا أمر سلبي لأنه سيستغرق وقتًا طويلًا لتعلّم هذه المعاملات والمفاهيم والأدوات، وما تفعله كل منها، وكيفية استخدامها بفعالية.
- وفرة المعاملات
إعداد Rx.js في مشروعك
هناك عدة طرق لتضمين Rx.js في مشروعك الحالي:
- تطبيق ويب: الصق رابط CDN في ملف جذر المشروع (index.html)
- تطبيق خلفي في Node.js - ثبّت Rx.js في مشروعك باستخدام npm عبر npm i rxjs
- تطبيق ويب بأطر عمل - ثبّت Rx.js في مشروعك باستخدام npm i rxjs أو yarn add rxjs
- Angular وNest.js - تدعم هذه الأطر Rx.js وتأتي مثبَّتة بها جاهزة
نشرت مؤخرًا مدونة تشرح كيفية استخدام وحدات NPM في تطبيقات الويب بدون أطر عمل، وهنا أعددت Rx.js في تطبيق ويب JS/TS أساسي (بدون CDN). إذا كنت مهتمًا، إليك دليل كامل ومستودع.
ختامًا
باختصار، تصف البرمجة التفاعلية نموذجًا يعتمد على منطق برمجي غير متزامن لتحفيز التغييرات في الوقت الفعلي. يكمن المنطق وراءها في نمط Observer. يُسمى مُنتِجو الأحداث Subjects التي ترسل بيانات مُغلَّفة في Observable إلى Observers التي تتصرف بناءً عليها. يُكتَب الكود نفسه بطريقة تعريفية حيث تكون كل خطوة معاملًا يمكنه التلاعب بالبيانات.
بشكل عام، البرمجة التفاعلية وObservables وReactiveX مواضيع واسعة. نأمل أن تكون في هذا المقال قد تعرّفت على إمكانيات أسلوب البرمجة هذا.
إلى اللقاء!
لمزيد من القراءة
- لماذا يجب أن تفكر في البرمجة التفاعلية [Good Guy Daniel]
- ما هي البرمجة التفاعلية؟ [Tech Target]
- Observables في JavaScript خلال 5 دقائق [Stack Chief]
- البرمجة التفاعلية في JavaScript باستخدام Rx.js [DZone]
- Rx.js in Action [Live Book]
- مسرد مصطلحات Rx.js [Rx.js Dev]