OOZOU
تواصلوا معنا
العودة إلى المدونة

التعامل مع قيم متعددة في سلسلة Promise باستخدام السياق ذي الحالة (Stateful Context)

نستكشف سلاسل Promise في JavaScript وكيفية التعامل مع قيم متعددة داخل سياق ذي حالة.

20 نوفمبر 2014
10 دقائق للقراءة
التعامل مع قيم متعددة في سلسلة Promise باستخدام السياق ذي الحالة (Stateful Context)

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

هناك طرق كثيرة لفعل ذلك، وسنستعرضها في هذا المقال.

مثال بسيط

سنبني مثالنا على السيناريو التالي: لاختبار أن ميزة "المتابعة" تعمل في تطبيق شبيه بـ Twitter، يجب أن نتأكد أنه حين يتابع المستخدم A المستخدم B، ثم ينشر B تحديثًا لحالته، يرى A حالة B في الخط الزمني (timeline) الخاص به.

ويتضمن ذلك خطوات مثل:

  1. إنشاء المستخدمَين A وB.
  2. جعل A يتابع B.
  3. جعل B ينشر تحديثًا لحالته.
  4. التحقق من أن الخط الزمني لـ A يحتوي على حالة B.

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

لنعد إلى مثالنا المبسط، ونستكشف الطرق التي يمكننا كتابته بها.

تداخل الـ Promises

بافتراض بيئة ES6، يمكن كتابة كود الاختبار على هذا النحو:

it('should display the status of B in A\'s timeline when A follows B', function() {
  return Promise.all([createUser('foo'), createUser('bar')])
  .then(function([user1, user2]) {
    return user1.follow(user2)
    .then(function() {
      return user2.updateStatus('My status')
    })
    .then(function(status) {
      return user1.getTimeline()
      .then(function(timeline) {
        expectTimelineToContainStatus(timeline, status)
      })
    })
  })
})

لاحظوا كم هو فوضوي! لقد تحول callback hell إلى promise hell.

تمرير القيم عبر السلسلة

نهج آخر هو ربط الـ Promises بشكل خطي، وتمرير كل القيم التي نحتاجها في الخطوات اللاحقة عبر الخطوة الحالية.

it('should display the status of B in A\'s timeline when A follows B', function() {
  return Promise.all([createUser('foo'), createUser('bar')])
  .then(function([user1, user2]) {
    return Promise.all([user1, user2, user1.follow(user2)])
  })
  .then(function([user1, user2]) {
    return Promise.all([user1, user2.updateStatus('My status')])
  })
  .then(function([user1, status]) {
    return Promise.all([user1.getTimeline(), status])
  })
  .then(function([timeline, status]) {
    expectTimelineToContainStatus(timeline, status)
  })
})

الآن، عليكم التأكد من أننا نمرر المتغيرات الصحيحة في كل خطوة.

إنشاء متغيرات محلية كثيرة

ونهج ثالث هو إنشاء متغيرات محلية كثيرة لحفظ كل ما نحتاجه.

it('should display the status of B in A\'s timeline when A follows B', function() {
  var user1, user2, status
  return Promise.all([createUser('foo'), createUser('bar')])
  .then(function([_user1, _user2]) {
    user1 = _user1
    user2 = _user2
    return user1.follow(user2)
  })
  .then(function() {
    return user2.updateStatus('My status')
  })
  .then(function(_status) {
    status = _status
    return user1.getTimeline()
  })
  .then(function(timeline) {
    expectTimelineToContainStatus(timeline, status)
  })
})

هنا ترون العمل اليدوي في إنشاء هذه المتغيرات وتخزين القيم فيها.

استخدام كائن سياق

ماذا لو أنشأنا كائنًا واحدًا فقط يحفظ كل ما نحتاجه (أي سياق)؟

it('should display the status of B in A\'s timeline when A follows B', function() {
  var the = { }
  return Promise.all([createUser('foo'), createUser('bar')])
  .then(function([user1, user2]) {
    the.firstUser = user1
    the.secondUser = user2
    return the.firstUser.follow(the.secondUser)
  })
  .then(function() {
    return the.secondUser.updateStatus('My status')
  })
  .then(function(status) {
    the.status = status
    return the.firstUser.getTimeline()
  })
  .then(function(timeline) {
    expectTimelineToContainStatus(timeline, the.status)
  })
})

الآن، نحتاج إلى إنشاء متغير واحد فقط. لكن بعد كل خطوة غير متزامنة، علينا أن نخزّن يدويًا القيم التي سنحتاجها لاحقًا في الخطوة التالية.

ماذا لو؟

ماذا لو كانت لدينا دالة ما:

  • تستقبل كائنًا من الـ Promises،
  • وتضيف نتيجة كل Promise إلى السياق،
  • وتُرجع Promise يُحسم (resolve) حين تُحسم كل هذه الـ Promises؟

عندها سيبدو الكود هكذا:

it('should display the status of B in A\'s timeline when A follows B', function() {
  var the = { }
  return set(the, {
    firstUser: createUser('foo'),
    secondUser: createUser('bar')
  })
  .then(function() {
    return the.firstUser.follow(the.secondUser)
  })
  .then(function() {
    return set(the, { status: the.secondUser.updateStatus('My status') })
  })
  .then(function() {
    return set(the, { timeline: the.firstUser.getTimeline() })
  })
  .then(function() {
    expectTimelineToContainStatus(the.timeline, the.status)
  })
})

لاحظوا كم هو مقروء ومباشر. فهو يُقرأ كجملة إنجليزية: set the first user to (the result of) creating (a) user (called) foo، أي عيّن المستخدم الأول إلى نتيجة إنشاء مستخدم اسمه foo.

ونقل عملية التهيئة إلى beforeEach مباشر أيضًا:

var the
beforeEach(function() { the = { } })

describe('following another user', function() {

  beforeEach(function() {
    return set(the, {
      firstUser: createUser('foo'),
      secondUser: createUser('bar')
    })
    .then(function() {
      return the.firstUser.follow(the.secondUser)
    })
    .then(function() {
      return set(the, { status: the.secondUser.updateStatus('My status') })
    })
  })

  it('should display the status of B in A\'s timeline when A follows B', function() {
    return the.firstUser.getTimeline().then(function(timeline) {
      expectTimelineToContainStatus(the.timeline, the.status)
    })
  })

})

لننتقل إلى التطبيق الفعلي

هناك مشكلة واحدة: الدالة set لم تكن موجودة. لنكتبها!

when/keys.all تحتوي على دالة تحسم قيم الـ Promises داخل كائن. بعبارة أخرى، هي دالة تحوّل { key: promise } إلى { key: resolved value }. والآن يمكننا استخدامها لكتابة الدالة set.

var keys = require('when/keys')
function set(context, promises) {
  return keys.all(promises).then(function(results) {
    for (var key in results) {
      context[key] = results[key]
    }
  })
}

ومع Object.assign في ES6، يصبح هذا الكود أكثر إيجازًا:

function set(context, promises) {
  return keys.all(promises).then(function(results) {
    Object.assign(context, results)
  })
}

وأكثر إيجازًا أيضًا:

function set(context, promises) {
  return keys.all(promises).then(Object.assign.bind(null, context))
}

stateful-context

أردت أن أمنحها اسمًا، فسميتها "stateful context". توفّر الوحدة stateful-context الدالة set هذه:

var set = require('stateful-context').set

يمكنكم تنزيلها من npm. والكود المصدري متاح على GitHub.

StatefulContext

توفّر الوحدة stateful-context الصنف StatefulContext. وعند إنشاء نسخة من هذا الصنف تحصلون على سياق فارغ مع التابع set، مربوطًا مسبقًا بالسياق الذي أُنشئ. وهذا يعفيكم من تمرير الوسيط الأول the في استدعاءات set.

var StatefulContext = require('stateful-context')

var the, set
beforeEach(function() {
  the = new StatefulContext()
  set = the.set
})

describe('following another user', function() {
  beforeEach(function() {
    return set({
      firstUser: createUser('foo'),
      secondUser: createUser('bar')
    })
    .then(function() {
      return the.firstUser.follow(the.secondUser)
    })
  })
  // ...
})

تسمية the

تناقشنا داخل الفريق حول اسم كائن السياق. فالاسم context محجوز مسبقًا في mocha (إطار اختبارات)، لذا اقترحت الاسم the.

إحدى مشكلاته أن الاسم the لا يشرح نفسه؛ فهو لا يصف الغرض. وفي النهاية استقر رأينا على الاسم locals، فهو قصير في الكتابة، ويحافظ على المقروئية ويوضح الغرض:

var locals
beforeEach(function() {
  locals = new StatefulContext()
})

describe('following another user', function() {
  beforeEach(function() {
    return locals.set({
      firstUser: createUser('foo'),
      secondUser: createUser('bar')
    })
    .then(function() {
      return locals.firstUser.follow(locals.secondUser)
    })
  })
  // ...
})

لكنني في مشاريعي الخاصة سأبقى على the. فرغم أن الاسم لا يصف نفسه، أرى أنه يصنع DSL جيدة.

نقطة الضعف: النهج ليس تصريحيًا

لكن هذا النهج لا يخلو من نقاط ضعف. فما زلنا نهيّئ السيناريو بأسلوب أمري (imperative)، أي خطوة بخطوة. كما أن لدينا حالة عامة قابلة للتعديل (وإن كانت محلية داخل الاختبار)، وقد لا تروق لبعض المطوّرين.

ما أود فعله حقًا هو بناء شيء يشبه helper الـ let في RSpec، لكنه يدرك الـ Promises غير المتزامنة. فهو يتيح لكم تعريف عناصر الاختبار داخل اختباراتكم بأسلوب تصريحي (declarative).

لم أتوصل بعد إلى طريقة أنيقة لفعل ذلك في JavaScript بإصدار ES5. أما في CoffeeScript، فلدي تصور مبدئي قد يبدو هكذا:

describe 'following another user', ->

  declare
    firstUser:   -> createUser('foo')
    secondUser:  -> createUser('bar')
    timeline:    (firstUser)  -> firstUser.getTimeline()
    status:      (secondUser) -> secondUser.updateStatus 'My status'

  beforeEach ->
    get (firstUser, secondUser) -> firstUser.follow secondUser

  it 'should display the status of B in A\'s timeline when A follows B', ->
    get (timeline, status) -> expectTimelineToContainStatus timeline, status

تخيلوا كتابة ذلك بـ JavaScript. ستشتتكم كثرة الدوال والأقواس والأقواس المعقوفة.

لذا إن كانت لديكم أي فكرة عن كيفية تحقيق ذلك في JavaScript بإصدار ES5، يسعدني أن أعرفها. وحتى ذلك الحين، استمتعوا!

فوائد استخدام Stateful Context

1. تبسيط إدارة القيم

باستخدام كائن واحد لتخزين قيم متعددة، تتخلصون من الحاجة إلى تمرير كل قيمة على حدة من .then() إلى أخرى. وهذا يقلل الكود المتكرر (boilerplate) ويبقي السلسلة مركّزة على العمليات نفسها لا على توصيلاتها.

2. تحسين المقروئية

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

3. تسهيل تصحيح الأخطاء

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

4. إتاحة سير عمل معقد

حين تتعاملون مع عمليات غير متزامنة يعتمد بعضها على بعض، يتيح لكم السياق ذو الحالة إدارة قيم متعددة بكفاءة دون تعقيد السلسلة.

كيف يعمل Stateful Context في سلسلة Promise

في التطبيق المعتاد:

  1. تهيئة كائن الحالة: في بداية السلسلة، أنشئوا كائنًا لتخزين القيم اللازمة.
  2. تحديث الحالة في كل خطوة: عدّلوا قيم كائن الحالة أو أضيفوا إليه قيمًا جديدة في كل خطوة من السلسلة.
  3. استخدام الحالة النهائية: في نهاية السلسلة، استخدموا كائن الحالة المكتمل للوصول إلى النتيجة المطلوبة.

حالات استخدام Stateful Context في سلاسل Promise

1. تجميع البيانات

عند جلب البيانات من مصادر متعددة، يمكنكم استخدام السياق ذي الحالة لتخزين نتيجة كل عملية جلب. وفي نهاية السلسلة سيكون لديكم كائن واحد يضم كل البيانات المجمّعة.

2. التعامل مع التبعيات

حين تعتمد عملية على نتيجة عملية أخرى، يتيح لكم السياق ذو الحالة تخزين النتائج الوسيطة واستخدامها في الخطوات اللاحقة.

3. سير العمل متعدد الخطوات

في سير العمل الذي يتضمن مهام غير متزامنة متعددة (مثل رفع الملفات، واستعلامات قاعدة البيانات، واستدعاءات API)، يوفّر السياق ذو الحالة طريقة منظمة لإدارة الحالات الوسيطة.

أفضل الممارسات لاستخدام Stateful Context

  1. أبقوا كائن الحالة بسيطًا: تجنبوا تحميل كائن الحالة بقيم غير ضرورية. خزّنوا فقط ما هو أساسي لسير العمل.
  2. وثّقوا تغييرات الحالة: صِفوا بوضوح ما تفعله كل خطوة في السلسلة وكيف تعدّل كائن الحالة.
  3. تجنبوا التعديل المباشر (mutation): إن أمكن، أنشئوا كائنات جديدة في كل خطوة بدلًا من تعديل الحالة الحالية. فهذا يسهّل تصحيح الأخطاء ويضمن عدم قابلية التغيير (immutability).
  4. تعاملوا مع الأخطاء بسلاسة: استخدموا .catch() لمعالجة الأخطاء، وافحصوا كائن الحالة لتحديد موضع المشكلة.
  5. استخدموا مفاتيح وصفية: اختاروا أسماء مفاتيح ذات معنى في كائن الحالة حتى يتضح ما تمثله كل قيمة.

أفكار ختامية

قد يبدو التعامل مع قيم متعددة في سلسلة Promise مربكًا، لكن السياق ذا الحالة يبسّط العملية. فبالحفاظ على كائن مشترك طوال السلسلة، تستطيعون إدارة القيم بكفاءة، وإبقاء الكود نظيفًا، والتعامل مع سير العمل المعقد بسهولة. وسواء كنتم تجمعون البيانات، أو تتعاملون مع التبعيات، أو تديرون عمليات متعددة الخطوات، فإن اعتماد السياق ذي الحالة سيجعل أسلوبكم في البرمجة غير المتزامنة بـ JavaScript أكثر انسيابية.

Related Articles

كيف أصمّم برمجيات؟ دليل المبتدئين لأصحاب الأعمال

كيف أصمّم برمجيات؟ دليل المبتدئين لأصحاب الأعمال

تصميم البرمجيات عملية ممتعة لكنها معقدة، تتطلب الإبداع والتفكير المنطقي ومهارات حل المشكلات.

اقرأوا المزيد
مستقبل الحوسبة الطرفية: ما تحتاج الشركات إلى معرفته

مستقبل الحوسبة الطرفية: ما تحتاج الشركات إلى معرفته

يتطور المشهد الرقمي بسرعة، وعلى الشركات أن تتكيّف لتواكب الطلب المتزايد على السرعة والكفاءة والمعالجة في الوقت الفعلي.

اقرأوا المزيد
مستقبل قطاع التجزئة: الذكاء الاصطناعي والواقع المعزّز واتجاهات التحوّل الرقمي

مستقبل قطاع التجزئة: الذكاء الاصطناعي والواقع المعزّز واتجاهات التحوّل الرقمي

يتطور قطاع التجزئة بوتيرة غير مسبوقة بفضل الذكاء الاصطناعي (AI) والواقع المعزّز (AR) وتقنيات التحوّل الرقمي. فلم تعد المتاجر التقليدية مجرد أماكن لبيع المنتجات، بل أصبحت متاجر ذكية وتفاعلية.

اقرأوا المزيد

Have a Project in Mind?

Let's discuss how we can help bring your ideas to life with our expertise in web and mobile development.

ابدأوا المحادثةhello@oozou.com
OOZOU Logo

بانكوك · سنغافورة · هونغ كونغ

X
Facebook
Instagram
LinkedIn
OOZOU Co., Ltd.
548 One City Centre
Ploenchit Road
Bangkok 10330
Thailand
+66 2544 0000
hello@oozou.com

الخدمات

  • تطوير الويب
  • وكلاء الذكاء الاصطناعي والذكاء الاصطناعي التوليدي
  • تطوير تطبيقات الجوال
  • تحليلات البيانات وهندستها
  • تصميم تجربة المستخدم وواجهته (UX/UI) وتصميم المنتجات
  • التحوّل الرقمي

الشركة

  • من نحن
  • الوظائف
  • المدونة
  • دراسات الحالة
  • عروض حية
  • تعلّمت اليوم
  • تواصل معنا

المزيد

  • القطاعات
  • شبكة الشركاء
© 2026 OOZOU. جميع الحقوق محفوظة.
سياسة الخصوصيةمدونة قواعد السلوكسياسة مكافحة الرشوة والفساد