SocialCalc
معمارية تطبيقات المصادر المفتوحة (المجلد الأول)SocialCalc
معمارية تطبيقات المصادر المفتوحة (المجلد الأول) SocialCalc
Audrey Tang
إذا استمتعتُ بهذه الكتب، فقد تستمتع أيضًا بـ Software Design by Example in Python و Software Design by Example in JavaScript.
يمتدّ تاريخ جداول البيانات (spreadsheets) لأكثر من ثلاثين عامًا. فقد تصوّر Dan Bricklin برنامج جداول البيانات الأول، VisiCalc، عام 1978 وصدر عام 1979. وكان المفهوم الأصلي في غاية البساطة: جدولٌ يمتدّ في بُعدين إلى ما لا نهاية، وخلاياه مملوءة بنصوص وأرقام وصيغ. وتُؤلَّف الصيغ من معاملات حسابية عادية ودوال مدمجة متنوّعة، ويمكن لكل صيغة أن تستخدم المحتوى الحالي لخلايا أخرى بوصفه قيمًا.
ولئن كانت الاستعارة (metaphor) بسيطة، فإن لها تطبيقات عديدة: المحاسبة، وجرد المخزون، وإدارة القوائم، هي سوى بعض الأمثلة. وكانت الإمكانات عمليًا بلا حدود. وقد جعلت جميع هذه الاستخدامات من VisiCalc أول «تطبيق قاتل» (killer app) في عصر الحاسوب الشخصي.
وفي العقود التالية، أدخلت تطبيقات مثل Lotus 1-2-3 و Excel تحسينات تدريجية، لكن الاستعارة الأساسية بقيت كما هي. كانت معظم جداول البيانات تُخزَّن كملفات على القرص، وتُحمَّل في الذاكرة عند فتحها للتحرير. وكان التعاون صعبًا على وجه الخصوص في النموذج المعتمد على الملفات:
- كان على كل مستخدم تثبيت نسخة من محرّر جداول البيانات.
- أضاف كلٌّ من تبادل البريد الإلكتروني ذهابًا وإيابًا، والمجلدات المشتركة، وإقامة نظام إدارة إصدارات مخصّص عبءً إداريًا.
- كان تتبّع التغييرات محدودًا؛ فمثلًا لا يحتفظ Excel بسجل لتغييرات التنسيق ولا بتعليقات الخلايا.
- كان تحديث التنسيقات أو الصيغ في القوالب يستلزم تغييرات مرهقة في ملفات جداول البيانات القائمة التي تستخدم ذلك القالب.
ولحسن الحظ، ظهر نموذج تعاون جديد يعالج هذه المشكلات ببساطة أنيقة. إنه نموذج الويكي (wiki)، الذي اخترعه Ward Cunningham عام 1994، وروّجت له ويكيبيديا في مطلع الألفية الثالثة.
وبدلًا من الملفات، يقوم نموذج الويكي على صفحات تُستضاف على خادم، قابلة للتحرير داخل المتصفح من دون الحاجة إلى برمجيات خاصة. ويمكن لتلك الصفحات النصية التشعبية (hypertext) أن ترتبط ببعضها بسهولة، وأن تتضمّن حتى أجزاء من صفحات أخرى لتكوّن صفحة أكبر. ويطّلع جميع المشاركين على أحدث نسخة ويحرّرونها افتراضيًا، فيما يتولى الخادم إدارة سجلّ المراجعات تلقائيًا.
وبإلهام من نموذج الويكي، بدأ Dan Bricklin العمل على WikiCalc عام 2005. وهو يهدف إلى الجمع بين سهولة التأليف والتحرير المتعدّد الأشخاص في الويكيات مع استعارة التنسيق المرئي والحساب المألوفة في جداول البيانات.
19.1. WikiCalc
اتّسم الإصدار الأول من WikiCalc (الشكل 19.1) بعدة ميزات ميّزته عن جداول البيانات الأخرى في ذلك الوقت:
- عرض البيانات النصية بنص عادي و HTML وترميز بأسلوب الويك.
- نص بأسلوب الويك يتضمّن أوامر لإدراج الروابط والصور والقيم من مراجع الخلايا.
- يمكن لخلايا الصيغ أن تشير إلى قيم صفحات WikiCalc أخرى المستضافة على مواقع ويب أخرى.
- القدرة على إنشاء مُخرَجات يمكن تضمينها في صفحات ويب أخرى، سواء كانت بيانات ساكنة أو حيّة.
- تنسيق الخلايا مع إمكان الوصول إلى سمات أنماط CSS وأصناف CSS.
- تسجيل جميع عمليات التحرير في سجل تدقيق (audit trail).
- الاحتفاظ، على طريقة الويكي، بكل نسخة جديدة من الصفحة مع إمكانية التراجع عنها.

الشكل 19.1: واجهة WikiCalc 1.0

الشكل 19.2: مكوّنات WikiCalc

الشكل 19.3: تدفّق WikiCalc
كانت البنية الداخلية لـ WikiCalc 1.0 (الشكل 19.2) وتدفّق المعلومات فيها (الشكل 19.3) بسيطين عن قصد، لكنهما مع ذلك قويّان. وقد أثبتت القدرة على تركيب جدول بيانات رئيسي من عدة جداول بيانات أصغر فائدتها العملية. فمثلًا، تخيّل سيناريو يحتفظ فيه كل مندوب مبيعات بأرقامه في صفحة جدول بيانات. ثم يجمع كل مدير مبيعات أرقام مندوبيه في جدول بيانات إقليمي، ثم يجمع نائب رئيس المبيعات الأرقام الإقليمية في جدول بيانات على المستوى الأعلى.
وفي كل مرة يُحدَّث فيها أحد جداول البيانات الفردية، يمكن لجميع جداول البيانات التجميعية أن تعكس ذلك التحديث. وإذا أراد أحدهم تفاصيل أكثر، فليكبّر عبر المستويات لعرض الجدول الكامن خلف الجدول. وتُلغي هذه القدرة التجميعية الجهد المكرر والمعرَّض للخطأ في تحديث الأرقام في أماكن متعددة، وتضمن بقاء جميع وجهات النظر على المعلومات طازجة.
ولضمان بقاء عمليات إعادة الحساب محدَّثة، تبنّى WikiCalc تصميم العميل الرفيع (thin client)، مُبقيًا جميع معلومات الحالة في جانب الخادم. ويُمثَّل كل جدول بيانات في المتصفح على هيئة عنصر ``؛ وتحرير خلية يُرسل نداء ajaxsetcell إلى الخادم، ثم يخبر الخادم المتصفح ما الخلايا التي تحتاج إلى تحديث.
وليس من المستغرب أن يعتمد هذا التصميم على اتصال سريع بين المتصفح والخادم. فحين يكون زمن الاستجابة (latency) مرتفعًا، يبدأ المستخدمون في ملاحظة تكرار ظهور رسائل «جارٍ التحميل…» (Loading…) في الفاصل بين تحديث خلية ورؤية محتواها الجديد، كما يوضّح الشكل 19.4. وتكون هذه مشكلة بصفة خاصة للمستخدمين الذين يحرّرون الصيغ تفاعليًا بتعديل المدخلات ويتوقّعون رؤية النتائج في الوقت الحقيقي.

الشكل 19.4: رسالة التحميل
علاوة على ذلك، ولأن عنصر `` كان له الأبعاد نفسها التي كانت لجدول البيانات، فإن شبكة قياس 100×100 كانت ستنشئ 10,000 عنصر DOM، وهو ما يرهق مورد الذاكرة في المتصفحات، فيقيّد حجم الصفحات أكثر.
ولهذه العيوب، ولئن كان WikiCalc مفيدًا كخادم مستقل يعمل على localhost، إلا أنه لم يكن عمليًا جدًا لدمجه ضمن أنظمة إدارة المحتوى المعتمدة على الويب.
وفي عام 2006، تعاون Dan Bricklin مع Socialtext لبدء تطوير SocialCalc، وهو إعادة كتابة لـ WikiCalc من الصفر بلغة Javascript بالاعتماد على بعض شيفرة Perl الأصلية.
استهدفت هذه إعادة الكتابة عمليات تعاون موزّعة وكبيرة، وسعت إلى توفير مظهر وإحساس أقرب إلى تطبيق سطح مكتب. وقد تضمّنت أهداف التصميم الأخرى:
- القدرة على معالجة مئات الآلاف من الخلايا.
- زمن استجابة سريع لعمليات التحرير.
- سجل تدقيق (audit trail) ومكدّس تراجع/إعادة على جانب العميل.
- استخدام أفضل لـ Javascript و CSS لتوفير وظائف تخطيط كاملة.
- دعم المتصفحات المتعددة، رغم الاستخدام الأوسع لجافاسكربت المتجاوب (responsive Javascript).
وبعد ثلاث سنوات من التطوير وإصدارات تجريبية عديدة، أصدرت Socialtext نسخة SocialCalc 1.0 عام 2009، محقّقةً أهداف التصميم بنجاح. فلننظر الآن إلى معمارية نظام SocialCalc.
19.2. SocialCalc

الشكل 19.5: واجهة SocialCalc
يوضّح الشكل 19.5 و الشكل 19.6 واجهة SocialCalc وأصنافه على الترتيب. ومقارنةً بـ WikiCalc، فقد تقلّص دور الخادم كثيرًا. فمسؤوليته الوحيدة هي الاستجابة لطلبات HTTP GET بتقديم جداول البيانات كاملةً مُسلسَلةً بصيغة الحفظ؛ وبمجرد أن يستلم المتصفح البيانات، تكون جميع الحسابات وتتبّع التغيّرات وتفاعل المستخدم منفَّذة الآن في Javascript.

الشكل 19.6: مخطط أصناف SocialCalc
صُمِّمت مكوّنات Javascript بأسلوب MVC (Model/View/Controller) طبقي، مع تركيز كل صنف على جانب واحد:
- Sheet هو نموذج البيانات، ويمثّل بنية في الذاكرة لجدول بيانات. فهو يحتوي على قاموس يربط الإحداثيات بكائنات Cell، يمثّل كل منها خلية واحدة. أما الخلايا الفارغة فلا تحتاج إلى أي إدخالات، وبذلك لا تستهلك أي ذاكرة على الإطلاق.
- Cell يمثّل محتوى الخلية وتنسيقاتها. وتوضّح الجدول 19.1 بعض الخصائص الشائعة.
- RenderContext ينفّذ طبقة العرض (view)؛ وهو مسؤول عن تصيير الورقة إلى كائنات DOM.
- TableControl هو المتحكّم الرئيسي، ويقبل أحداث الفأرة ولوحة المفاتيح. فحين يتلقّى أحداث عرض مثل التمرير والتحجيم، يحدّث كائن RenderContext المرتبط به. وحين يتلقّى أحداث تحديث تؤثّر في محتوى الورقة، يجدول أوامر جديدة في قائمة أوامر الورقة.
- SpreadSheetControl هو واجهة المستخدم في المستوى الأعلى، وتضمّ أشرطة أدوات وأشرطة حالة ونوافذ حوار ومنتقيات ألوان.
- SpreadSheetViewer واجهة بديلة في المستوى الأعلى توفّر عرضًا تفاعليًا للقراءة فقط.
datatype |
t |
|---|---|
datavalue |
1Q84 |
color |
black |
bgcolor |
white |
font |
italic bold 12pt Ubuntu |
comment |
Ichi-Kyu-Hachi-Yon |
الجدول 19.1: محتوى الخلايا وتنسيقاتها
اعتمدنا نظام كائنات قائم على الأصناف (class-based) في أدنى صوره، مع تركيب/تفويض (composition/delegation) بسيط، من دون أي استخدام للوراثة (inheritance) أو للنماذج الأولية للكائنات (object prototypes). وتوضع جميع الرموز ضمن فضاء الأسماء SocialCalc.* تفاديًا لتعارض الأسماء.
يمرّ كل تحديث على الورقة عبر الطريقة ScheduleSheetCommands، التي تأخذ سلسلة أوامر تمثّل التحرير. (وتوضّح الجدول 19.2 بعض الأوامر الشائعة.) ويمكن للتطبيق المُضمِّن لـ SocialCalc أن يعرّف أوامر إضافية خاصة به، بإضافة دوال استدعاء مسمّاة (named callbacks) إلى الكائن SocialCalc.SheetCommandInfo.CmdExtensionCallbacks، واستخدام الأمر startcmdextension لاستدعائها.
set sheet defaultcolor blue
set A width 100
set A1 value n 42
set A2 text t Hello
set A3 formula A1*2
set A4 empty
set A5 bgcolor green
merge A1:B2
unmerge A1
erase A2
cut A3
paste A4
copy A5
sort A1:B9 A up B down
name define Foo A1:A5
name desc Foo Used in formulas like SUM(Foo)
name delete Foo
startcmdextension UserDefined args
الجدول 19.2: أوامر SocialCalc
19.3. حلقة تنفيذ الأوامر
لتحسين الاستجابة، يؤدّي SocialCalc جميع عمليات إعادة الحساب وتحديثات DOM في الخلفية، بحيث يواصل المستخدم إجراء تغييراته على عدة خلايا بينما يلحق المحرّك بالتغييرات الأسبق الموجودة في قائمة الأوامر.

الشكل 19.7: حلقة تنفيذ أوامر SocialCalc
أثناء تشغيل أمر ما، يضبط الكائن TableEditor راية busy على القيمة true؛ وعندئذٍ تُدفع الأوامر التالية إلى قائمة deferredCommands، بما يضمن ترتيبًا تسلسليًا للتنفيذ. وكما يوضّح مخطط حلقة الأحداث في الشكل 19.7، يواصل كائن Sheet إرسال أحداث StatusCallback لإعلام المستخدم بالحالة الراهنة لتنفيذ الأوامر، عبر كل واحدة من الخطوات الأربع:
- ExecuteCommand: يرسل
cmdstartعند البدء، وcmdendعند انتهاء تنفيذ الأمر. وإذا غيّر الأمر قيمة خلية بشكل غير مباشر، فادخل خطوة Recalc. وإلا، إذا غيّر الأمر المظهر المرئي لخلية واحدة أو أكثر من خلايا الشاشة، فادخل خطوة Render. وإذا لم ينطبق أيٌّ من الأمرين السابقين (مثلًا مع الأمرcopy)، فتجاوز إلى خطوة PositionCalculations. - Recalc (asneeded): يرسل
calcstartعند البدء، وcalcorderكل 100 مللي ثانية عند فحص سلسلة اعتمادية الخلايا، وcalccheckdoneعند انتهاء الفحص، وcalcfinishedعندما تتلقّى جميع الخلايا المتأثرة قيمها المعاد حسابها. وتتبع هذه الخطوة دائمًا خطوة Render. - Render (as needed): يرسل
schedrenderعند البدء، وrenderdoneعندما يُحدَّث عنصر `` بالخلايا المنسَّقة. وتتبع هذه الخطوة دائمًا PositionCalculations. - PositionCalculations: يرسل
schedposcalcعند البدء، وdoneposcalcبعد تحديث أشرطة التمرير ومؤشّر الخلية القابلة للتحرير الحالية والمكوّنات المرئية الأخرى الخاصة بـTableEditor.
ولأن جميع الأوامر تُحفظ فور تنفيذها، فإننا نحصل طبيعيًا على سجل تدقيق لكل العمليات. وتقدّم الطريقة Sheet.CreateAuditString سلسلةً مفصولةً بأسطر جديدة بوصفها سجل التدقيق، بحيث يقع كل أمر في سطر واحد.
كما تنشئ ExecuteSheetCommand أمر تراجع (undo) لكل أمر تنفّذه. فمثلًا، إذا كانت الخلية A1 تحتوي على «Foo» ونفّذ المستخدم الأمر set A1 text Bar، فسيُدفع أمر التراجع set A1 text Foo إلى مكدّس التراجع. وإذا نقر المستخدم على تراجع (Undo)، يُنفَّذ أمر التراجع لاستعادة A1 إلى قيمته الأصلية.
19.4. محرّر الجدول
لننظر الآن إلى طبقة TableEditor. فهي تحسب الإحداثيات على الشاشة الخاصة بـ RenderContext، وتدير أشرطة التمرير الأفقية والرأسية عبر نسختين من TableControl.

الشكل 19.8: نسختا TableControl تُديران أشرطة التمرير
وتختلف طبقة العرض، التي يتولاها الصنف RenderContext، أيضًا عن تصميم WikiCalc. فبدلًا من ربط كل خلية بعنصر ، أصبحنا ننشئ ببساطة عنصرًا ثابت الحجم يتّسع مع المنطقة المرئية في المتصفح، ونملؤه مسبقًا بعناصر ``.
ومع تمرير المستخدم جدول البيانات عبر أشرطة التمرير التي رسمناها بأنفسنا، نحدّث ديناميكيًا قيمة innerHTML للعناصر المرسومة مسبقًا. وهذا يعني أننا لا نحتاج إلى إنشاء أي عناصر أو `` أو إتلافها في حالات شائعة كثيرة، مما يسرّع زمن الاستجابة كثيرًا.
ولأن RenderContext لا يعرض سوى المنطقة المرئية، يمكن أن يكون حجم كائن Sheet كبيرًا إلى أي حدّ دون أن يؤثر ذلك في أدائه.
يحتوي TableEditor أيضًا على كائن CellHandles، الذي ينفّذ قائمة التعبئة/النقل/الانزلاق الدائرية (radial fill/move/slide) المرتبطة بالزاوية السفلية اليمنى من الخلية القابلة للتحرير الحالية، المعروفة باسم ECell، كما يوضّح الشكل 19.9.

الشكل 19.9: الخلية القابلة للتحرير الحالية، المعروفة باسم ECell
يُدار صندوق الإدخال (input box) بواسطة صنفين: InputBox و InputEcho. فأولهما يدير صف التحرير الموجود فوق الشبكة، بينما يعرض الثاني طبقة معاينة تُحدَّث أثناء الكتابة، مُتراكِبة فوق محتوى ECell (الشكل 19.10).

الشكل 19.10: صندوق الإدخال يديره صنفان
عادةً ما لا يحتاج محرّك SocialCalc إلى الاتصال بالخادم إلا عند فتح جدول بيانات للتحرير، وعند حفظه مجددًا إلى الخادم. ولهذا الغرض، تحلّل الطريقة Sheet.ParseSheetSave سلسلةً بصيغة الحفظ إلى كائن Sheet، وتُسلسِل الطريقة Sheet.CreateSheetSave كائن Sheet مرة أخرى إلى صيغة الحفظ.
ويمكن للصيغ أن تشير إلى قيم من أي جدول بيانات بعيد له عنوان URL. ويعيد الأمر recalc جلب جداول البيانات المُشار إليها خارجيًا، ويحلّلها من جديد باستخدام Sheet.ParseSheetSave، ويخزّنها في ذاكرة مؤقتة (cache) بحيث يتمكّن المستخدم من الإشارة إلى خلايا أخرى في جداول البيانات البعيدة نفسها من دون إعادة جلب محتواها.
19.5. صيغة الحفظ
صيغة الحفظ هي صيغة MIME القياسية multipart/mixed، وتتألف من أربعة أجزاء من نوع text/plain; charset=UTF-8، يتضمّن كل جزء نصًا مفصولًا بأسطر جديدة وتُفصل حقول بياناته بنقطتين. والأجزاء هي:
- يعدّد الجزء
metaأنواع الأجزاء الأخرى. - يعدّد الجزء
sheetتنسيق ومحتوى كل خلية، وعرض كل عمود (إن لم يكن افتراضيًا)، والتنسيق الافتراضي للورقة، يلي ذلك قائمة بالخطوط والألوان والحدود المستخدمة في الورقة. - يحفظ الجزء الاختياري
editحالة التحرير فيTableEditor، بما في ذلك الموضع الأخير لـ ECell، وكذلك الأحجام الثابتة لألواح الصفوف/الأعمدة. - يحتوي الجزء الاختياري
auditعلى سجلّ الأوامر التي نُفِّذت في جلسة التحرير السابقة.
فمثلًا، يوضّح الشكل 19.11 جدول بيانات من ثلاث خلايا، فيها القيمة 1874 في A1 بوصفها ECell، والصيغة 2^2*43 في A2، والصيغة SUM(Foo) في A3 معروضة بخط عريض، وتشير إلى النطاق المُسمّى Foo على A1:A2.

الشكل 19.11: جدول بيانات بثلاث خلايا
تبدو صيغة الحفظ المُسلسَلة لجدول البيانات على النحو التالي:
socialcalc:version:1.0
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=SocialCalcSpreadsheetControlSave
--SocialCalcSpreadsheetControlSave
Content-type: text/plain; charset=UTF-8
# SocialCalc Spreadsheet Control Save
version:1.0
part:sheet
part:edit
part:audit
--SocialCalcSpreadsheetControlSave
Content-type: text/plain; charset=UTF-8
version:1.5
cell:A1:v:1874
cell:A2:vtf:n:172:2^2*43
cell:A3:vtf:n:2046:SUM(Foo):f:1
sheet:c:1:r:3
font:1:normal bold * *
name:FOO::A1\cA2
--SocialCalcSpreadsheetControlSave
Content-type: text/plain; charset=UTF-8
version:1.0
rowpane:0:1:14
colpane:0:1:16
ecell:A1
--SocialCalcSpreadsheetControlSave
Content-type: text/plain; charset=UTF-8
set A1 value n 1874
set A2 formula 2^2*43
name define Foo A1:A2
set A3 formula SUM(Foo)
--SocialCalcSpreadsheetControlSave--
صُمِّمت هذه الصيغة لتكون قابلة للقراءة من جانب الإنسان، فضلًا عن كونها سهلة التوليد نسبيًا برمجيًا. وهذا يجعل بالإمكان لإضافة Sheetnode في Drupal أن تستخدم PHP للتحويل بين هذه الصيغة وصيغ جداول البيانات الشائعة الأخرى، مثل Excel (.xls) و OpenDocument (.ods).
والآن، وقد صار لدينا تصوّر جيّد عن كيفية ترابط أجزاء SocialCalc، فلننظر إلى مثالين من العالم الفعلي لتوسيع SocialCalc.
19.6. تحرير النصوص المنسَّقة
المثال الأول الذي سننظر إليه هو تحسين خلايا النص في SocialCalc بترميز الويك، لعرض صيغته المنسَّقة (rich text) داخل محرّر الجدول نفسه (الشكل 19.12).

الشكل 19.12: عرض النصوص المنسّقة في محرّر الجدول
أضفنا هذه الميزة إلى SocialCalc مباشرة بعد إصداره 1.0، للاستجابة إلى الطلب الشائع بإدراج الصور والروابط وتنسيقات النصوص باستخدام صياغة موحّدة. وبما أن Socialtext يمتلك أصلًا منصة ويكي مفتوحة المصدر، كان من الطبيعي أن نُعيد استخدام الصياغة نفسها في SocialCalc أيضًا.
ولتنفيذ ذلك، نحتاج إلى مُصيِّر (renderer) مخصّص لـ textvalueformat الخاص بـ text-wiki، وإلى تغيير الصيغة الافتراضية لخلايا النصوص لاستخدامه.
وما هذا textvalueformat الذي تسأل عنه؟ تابع القراءة.
19.6.1. الأنواع والصيغ
في SocialCalc، لكل خلية datatype (نوع بيانات) و valuetype (نوع قيمة). وخلايا البيانات التي تحمل نصًا أو أرقامًا تقابل أنواع قيم نصية/رقمية، أما خلايا الصيغ ذات datatype="f" فيمكن أن تولّد قيمًا رقمية أو نصية.
وتذكّر أن الكائن Sheet يولّد لغة HTML من كل خلية من خلاياه في خطوة Render. وهو يفعل ذلك بفحص valuetype الخاص بكل خلية: إذا بدأ بالحرف t، فإن الخاصية textvalueformat للخلية تحدّد كيفية التوليد. وإذا بدأ بالحرف n، فيُستخدم بدلًا منها الخاصية nontextvalueformat.
غير أنه إذا لم تكن الخاصية textvalueformat أو nontextvalueformat للخلية معرَّفةً صراحةً، فيُبحث عن صيغة افتراضية انطلاقًا من valuetype الخاص بها، كما يوضّح الشكل 19.13.

الشكل 19.13: أنواع القيم
الدعم لصيغة القيم text-wiki مُرمَّز في SocialCalc.format_text_for_display:
if (SocialCalc.Callbacks.expand_wiki && /^text-wiki/.test(valueformat)) {
// do general wiki markup
displayvalue = SocialCalc.Callbacks.expand_wiki(
displayvalue, sheetobj, linkstyle, valueformat
);
}
وبدلًا من تضمين أداة تحويل الويك إلى HTML داخل format_text_for_display، سنعرّف خطّافًا (hook) جديدًا في SocialCalc.Callbacks. وهذا هو النمط الموصى به في شيفرة SocialCalc بكاملها؛ فهو يحسّن النمطية (modularity) بجعله ممكنًا لتوصيل طرائق مختلفة لتوسيع نصوص الويك، فضلًا عن الحفاظ على التوافق مع التطبيقات المُضمِّنة التي لا ترغب في هذه الميزة.
19.6.2. عرض نصوص الويكي
ثم سنستعين بـ Wikiwyg[1]، وهي مكتبة Javascript توفّر تحويلات ثنائية الاتجاه بين نصوص الويك و HTML.
نعرّف الدالة expand_wiki بأن نأخذ نص الخلية ونمرّره على محلّل نصوص الويك في Wikiwyg وعلى باعث HTML الخاص به:
var parser = new Document.Parser.Wikitext();
var emitter = new Document.Emitter.HTML();
SocialCalc.Callbacks.expand_wiki = function(val) {
// Convert val from Wikitext to HTML
return parser.parse(val, emitter);
}
وتتمثّل الخطوة الأخيرة في جدولة الأمر set sheet defaulttextvalueformat text-wiki مباشرةً بعد تهيئة جدول البيانات:
// We assume there's a <div id="tableeditor"/> in the DOM already
var spreadsheet = new SocialCalc.SpreadsheetControl();
spreadsheet.InitializeSpreadsheetControl("tableeditor", 0, 0, 0);
spreadsheet.ExecuteCommand('set sheet defaulttextvalueformat text-wiki');
وبتجميع ذلك كله، تعمل خطوة Render الآن كما يوضّح الشكل 19.14.

الشكل 19.14: خطوة Render
هذا كل شيء! يدعم SocialCalc المحسَّن الآن مجموعةً غنية من صيغ ترميز الويك:
*bold* _italic_ `monospace`
> indented text
* unordered list
# ordered list
"Hyperlink with label"<http://softwaregarden.com/>
{image: http://www.socialtext.com/static/logo.png}
جرّب إدخال *bold* _italic_ monospace`` في A1، وستراه معروضًا كنص منسَّق (الشكل 19.15).

الشكل 19.15: مثال Wikywyg
19.7. التعاون الفوري
المثال التالي الذي سنستكشفه هو التحرير الفوري متعدّد المستخدمين على جدول بيانات مشترك. قد يبدو ذلك معقّدًا في بادئ الأمر، لكن بفضل التصميم النمطي لـ SocialCalc لا يحتاج الأمر سوى أن يبثّ كل مستخدم متصل أوامرَه إلى بقية المشاركين.
وللتمييز بين الأوامر الصادرة محليًا والأوامر البعيدة، نضيف معاملًا isRemote إلى الطريقة ScheduleSheetCommands:
SocialCalc.ScheduleSheetCommands = function(sheet, cmdstr, saveundo, isRemote) {
if (SocialCalc.Callbacks.broadcast && !isRemote) {
SocialCalc.Callbacks.broadcast('execute', {
cmdstr: cmdstr, saveundo: saveundo
});
}
// …original ScheduleSheetCommands code here…
}
ولم يعد أمامنا سوى تعريف دالة استدعاء مناسبة وهي SocialCalc.Callbacks.broadcast. وبمجرد توفّرها، ستُنفَّذ الأوامر نفسها لدى جميع المستخدمين الموصولين بجدول البيانات نفسه.
وعندما نُفِّذت هذه الميزة أول مرة من أجل مشروع OLPC (One Laptop Per Child[2]) على يد مختبرات Sugar التابعة لـ SEETA[3] عام 2009، بُنيت الدالة broadcast باستخدام نداءات XPCOM إلى D-Bus/Telepathy، وهو الناقل المعياري لشبكات OLPC/Sugar (انظر الشكل 19.16).

الشكل 19.16: تنفيذ OLPC
وقد نجح ذلك إلى حدٍّ معقول، إذ أتاح لحالات XO في شبكة Sugar نفسها أن تتعاون على جدول بيانات SocialCalc مشترك. غير أنه مرتبط على حد سواء بمنصّة المتصفح Mozilla/XPCOM وبمنصّة المراسلة D-Bus/Telepathy.
19.7.1. النقل عبر المتصفحات
ولجعل ذلك يعمل عبر المتصفحات وأنظمة التشغيل، نستخدم إطار Web::Hippie[4]، وهو تجريد عالي المستوى لـ JSON-over-WebSocket مع ربط مريح بـ jQuery، مع جعل MXHR (Multipart XML HTTP Request[5]) آلية النقل الاحتياطية إذا لم يكن WebSocket متاحًا.
وللمتصفحات التي تتوفّر فيها إضافة Adobe Flash ولا تدعم WebSocket أصليًا، نستخدم محاكاة WebSocket عبر Flash من مشروع web_socket.js[6]، وهي غالبًا أسرع وأكثر موثوقية من MXHR. ويوضّح الشكل 19.17 تدفّق العملية.

الشكل 19.17: التدفّق عبر المتصفحات
تُعرَّف دالة SocialCalc.Callbacks.broadcast في جانب العميل على النحو التالي:
var hpipe = new Hippie.Pipe();
SocialCalc.Callbacks.broadcast = function(type, data) {
hpipe.send({ type: type, data: data });
};
$(hpipe).bind("message.execute", function (e, d) {
var sheet = SocialCalc.CurrentSpreadsheetControlObject.context.sheetobj;
sheet.ScheduleSheetCommands(
d.data.cmdstr, d.data.saveundo, true // isRemote = true
);
break;
});
ولئن كان هذا يعمل جيدًا إلى حدٍّ كبير، فهناك مشكلتان متبقيتان يجب معالجتهما.
19.7.2. حل التعارضات
الأولى هي حالة سباق (race condition) في ترتيب الأوامر المنفَّذة: إذا نفّذ المستخدمان A و B في الوقت نفسه عملية تؤثّر في الخلايا نفسها، ثم تسلّما الأمرين المبوَّثين من المستخدم الآخر ونفّذاهما، فسينتهي بهما الحال في حالتين مختلفتين، كما يوضّح الشكل 19.18.

الشكل 19.18: تعارض بسبب حالة السباق
ويمكننا حلّ ذلك بآلية التراجع/الإعادة المدمجة في SocialCalc، كما يوضّح الشكل 19.19.

الشكل 19.19: حل تعارض حالة السباق
العملية المستخدمة لحل التعارض هي كما يلي. عندما يبثّ عميل أمرًا، يضيف هذا الأمر إلى قائمة انتظار (Pending queue). وعندما يستقبل عميل أمرًا، يقارن الأمر البعيد بقائمة الانتظار.
إذا كانت قائمة الانتظار فارغة، فالأمر يُنفَّذ ببساطة بوصفه إجراءً بعيدًا. وإذا طابق الأمر البعيد أمرًا في قائمة الانتظار، فحُذف الأمر المحلي من القائمة.
وإلا، يفحص العميل ما إذا كانت هناك أوامر في الطابور تتعارض مع الأمر المستلم. وإذا وُجدت أوامر متعارضة، يقوم العميل أولًا بتنفيذ Undo لتلك الأوامر ويضع علامات عليها لتنفيذ Redo لاحقًا. وبعد التراجع عن الأوامر المتعارضة (إن وُجدت)، يُنفَّذ الأمر البعيد كالمعتاد.
وعندما يستقبل العميل من الخادم أمرًا موسومًا لإعادة تنفيذه، فسيعيد تنفيذه ثم يحذفه من القائمة.
19.7.3. المؤشّرات البعيدة
وحتى مع معالجة حالات السباق، فإن الكتابة فوق خلية يحرّرها مستخدم آخر حاليًا عن طريق الخطأ تبقى أمرًا غير مثالي. والتحسين البسيط هو أن يبثّ كل عميل موضع مؤشّره إلى بقية المستخدمين، بحيث يرى الجميع الخلايا التي يجري العمل عليها.
ولتنفيذ هذه الفكرة، نضيف معالج broadcast آخر إلى الحدث MoveECellCallback:
editor.MoveECellCallback.broadcast = function(e) {
hpipe.send({
type: 'ecell',
data: e.ecell.coord
});
};
$(hpipe).bind("message.ecell", function (e, d) {
var cr = SocialCalc.coordToCr(d.data);
var cell = SocialCalc.GetEditorCellElement(editor, cr.row, cr.col);
// …decorate cell with styles specific to the remote user(s) on it…
});
ولتمييز الخلية النشطة في جداول البيانات، من الشائع استخدام حدود ملوّنة. غير أن الخلية قد تكون معرِّفة لنفسها الخاصية border، ولأن border أحادي اللون، فإنه لا يمكنه أن يمثّل سوى مؤشّر واحد على الخلية نفسها.
لذلك، في المتصفحات التي تدعم CSS3، نستخدم الخاصية box-shadow لتمثيل عدة مؤشّرات للأقران في الخلية نفسها:
/* Two cursors on the same cell */
box-shadow: inset 0 0 0 4px red, inset 0 0 0 2px green;
يوضّح الشكل 19.20 كيف ستبدو الشاشة مع أربعة أشخاص يحرّرون جدول البيانات نفسه. 
الشكل 19.20: أربعة مستخدمون يحرّرون جدول بيانات واحد
19.8. الدروس المستفادة
سلّمنا SocialCalc 1.0 في 19 أكتوبر 2009، وهي الذكرى الثلاثون للإصدار الأول من VisiCalc. وكانت خبرتي في التعاون مع زملائي في Socialtext تحت إشراف Dan Bricklin بالغة الأهمية بالنسبة لي، وأودّ أن أشارك بعض الدروس التي تعلّمتها خلال تلك الفترة.
19.8.1. مصمّم رئيسي برؤية واضحة
في Bro10، يقرّر Fred Brooks أن بناء الأنظمة المعقّدة يصبح فيه الحوار أكثر مباشرة بكثير إن ركّزنا على مفهوم تصميم متّسق (coherent design concept) بدلًا من التمثيلات المشتقّة. ووفقًا لما يراه Brooks، فإن صياغة مثل هذا المفهوم المتّسق من التصميم يكون من الأفضل الاحتفاظ بها في ذهن شخص واحد:
ولأن السلامة المفاهيمية هي أهمّ سمة من سمات التصميم العظيم، ولأنها تنبع من عقل واحد أو بضعة عقول تعمل uno animo، فإن المدير الحكيم يسلّم كل مهمة تصميم، بثقة وجسارة، إلى مصمّم رئيسي موهوب.
وفي حالة SocialCalc، كان وجود Tracy Ruggles بوصفها مصمّمة تجربة المستخدم الرئيسية (chief user-experience designer) هو المفتاح الذي جعل المشروع ينبثق نحو رؤية مشتركة. ولأن محرّك SocialCalc الكامِن كان شديد المرونة، كان إغراء تمدّد الميزات (feature creep) واقعًا تمامًا. وقد ساعدت قدرة Tracy على التواصل بواسطة الرسوم التخطيطية للتصميم (design sketches) كثيرًا في تقديم الميزات على نحو يبدو بديهيًا للمستخدمين.
19.8.2. الويكات من أجل استمرارية المشروع
قبل أن أنضم إلى مشروع SocialCalc، كان قد مضى على التصميم والتطوير المستمر أكثر من عامين، ومع ذلك تمكّنت من اللحاق بالركب والبدء في المشاركة في أقل من أسبوع، لمجرد أن كل شيء موجود في الويكي. فمن أقدم ملاحظات التصميم إلى أحدث مصفوفة دعم للمتصفحات، وُثِّقت العملية برمتها في صفحات الويك وجداول بيانات SocialCalc.
وقد أوصلني تصفّح مساحة عمل المشروع سريعًا إلى الصفحة نفسها التي وصل إليها غيري، من دون ذلك العبء المعتاد من الإرشاد اليدوي المرتبط عادةً بتأهيل عضو جديد في الفريق.
ولن يكون هذا ممكنًا في مشاريع البرمجيات مفتوحة المصدر التقليدية، حيث يجري معظم الحوار عبر IRC وقوائم البريد الإلكتروني، ولا يُستخدم الويكي (إن وُجد) إلا للتوثيق ولمصادر التطوير. فبالنسبة للواصل حديثًا، فإن إعادة بناء السياق من سجلات IRC غير المهيكلة وأرشيفات البريد أصعب بكثير.
19.8.3. تبنَّ فروق المناطق الزمنية
علّق David Heinemeier Hansson، مطوّر Ruby on Rails، ذات مرة على فائدة الفرق الموزّعة عندما انضم إلى 37signals لأول مرة، قائلًا: «المناطق الزمنية السبع بين كوبنهاغن وشيكاغو كانت في الواقع تعني أننا أنجزنا الكثير مع قلّة المقاطعات.» ومع وجود تسع مناطق زمنية بين تايبيه وبالو ألتو، كان ذلك صحيحًا بالنسبة إلينا أيضًا أثناء تطوير SocialCalc.
وكنّا غالبًا ما نُكمل دورة تغذية راجعة كاملة من التصميم والتطوير وضمان الجودة خلال يوم مدته 24 ساعة، إذ يستغرق كل جانب يوم عمل مدته 8 ساعات لأحد الأشخاص في نهاره المحلي. وقد ألزم هذا الأسلوب غير المتزامن للتعاون بأن نُنتج مُخرَجات وصفية لنفسها (رسوم التصميم، والشيفرة، والاختبارات)، وهو ما حسّن في المقابل ثقتنا ببعضنا بعضًا كثيرًا.
19.8.4. التحسين من أجل المتعة
في المحاضرة الافتتاحية (keynote) التي ألقيتها عام 2006 في مؤتمر CONISLI (Tan06)، لخّصت تجربتي في قيادة فريق موزّع نفّذ لغة Perl 6 في بضع ملاحظات. ومن بينها، هناك عبارات ذات صلة خاصة بالفرق الموزّعة الصغيرة: احرص دائمًا على خارطة طريق (Always have a Roadmap)، و الغفران أفضل من الإذن (Forgiveness > Permission)، و أزل الجمود (Remove deadlocks)، و اطرح الأفكار ولا تطلب الإجماع (Seek ideas, not consensus)، و ارسُم الأفكار بالشيفرة (Sketch ideas with code).
وعند تطوير SocialCalc، أخذنا بالكفاية في توزيع المعرفة بين أعضاء الفريق عبر ملكية تعاونية للشيفرة (collaborative code ownership)، حتى لا يصبح أحدهم اختناقًا حرجًا.
فضلًا عن ذلك، كنّا نُسوّي الخلافات استباقيًا عبر كتابة بدائل فعلية لاستكشاف فضاء التصميم، ولم نخشَ استبدال النماذج الأولية العاملة تمامًا حين يصل تصميم أفضل.
وقد ساعدتنا هذه السمات الثقافية على زرع شعور بالتوقّع والؤلفة على رغم غياب التفاعل المباشر، وأبقينا الأمور السياسية في أدنى حد، وجعلت العمل على SocialCalc متعة كبيرة.
19.8.5. قيادة التطوير باختبارات القصص
قبل انضمامي إلى Socialtext، كنت قد دعتُ إلى نهج «مزج الاختبارات مع المواصفة» (interleave tests with the specification)، كما يتّضح في مواصفة لغة Perl 6[7]، حيث نُنشِئ مواصفة اللغة مرفقةً بمجموعة الاختبارات الرسمية. غير أن Ken Pier و Matt Heusser، وهما فريق ضمان الجودة في SocialCalc، هما اللذان فتحا لي عينيّ حقًا على كيف يمكن الارتقاء بهذا المنهج إلى المستوى التالي، بنقل الاختبارات إلى موضع المواصفة القابلة للتنفيذ (executable specification).
وفي الفصل 16 من GR09، شرح Matt عملية تطويرنا المبنية على اختبارات القصص (story tests) على النحو التالي:
الوحدة الأساسية للعمل هي «قصة»، وهي مستند متطلبات شديد الخفة. وتتضمّن القصة وصفًا موجزًا لميزة، إلى جانب أمثلة لما ينبغي أن يحدث حتى تُعدّ القصة مكتملة؛ ونسمي هذه الأمثلة «اختبارات القبول» (acceptance tests) ونصفها بالإنجليزية المبسّطة. وعند القصّ الأول للقصة، يقوم مالك المنتج بمحاولة أولى بحسن نية لإنشاء اختبارات القبول، ثم يزيد عليها المطوّرون والمختبِرون قبل أن يكتب أي مطوّر سطرًا واحدًا من الشيفرة.
ثم تُترجَم اختبارات القصص هذه إلى wikitests، وهي لغة مواصفة قائمة على الجداول مستوحاة من إطار FIT لـ Ward Cunningham[8]، تقود أطر الاختبارات الآلية مثل Test::WWW::Mechanize[9] و Test::WWW::Selenium[10].
ومن الصعب المبالغة في تقدير فائدة وجود اختبارات القصص بوصفها لغة مشتركة للتعبير عن المتطلبات والتحقق منها. فقد كان لها دور محوري في الحدّ من سوء الفهم، وقد أدّت إلى اختفاء الانحدارات (regressions) من إصداراتنا الشهرية عمليًا.
19.8.6. البرمجيات مفتوحة المصدر مع CPAL
وأخيرًا وليس آخرًا، فإن نموذج البرمجيات مفتوحة المصدر الذي اخترناه لـ SocialCalc يشكّل بحدّ ذاته درسًا مثيرًا للاهتمام.
أنشأت Socialtext رخصة الإسناد العام (Common Public Attribution License)[11] من أجل SocialCalc. واستنادًا إلى رخصة Mozilla العامة (Mozilla Public License)، صُمِّمت CPAL لتتيح للمؤلف الأصلي أن يشترط إظهار إسناد في واجهة مستخدم البرمجية، كما تحتوي بندًا للاستخدام عبر الشبكة يُفعّل أحكام المشاركة بالمثل (share-alike) عندما يُستضاف العمل المشتق في خدمة عبر الشبكة.
وبعد موافقتها من قِبل كلٍّ من مؤسسة البرمجيات مفتوحة المصدر (Open Source Initiative)[12] ومؤسسة البرمجيات الحرة (Free Software Foundation)[13]، رأينا مواقع بارزة مثل Facebook[14] و Reddit[15] تختار إصدار شيفرة منصّتها المصدرية بموجب CPAL، وهو ما يُبشّر كثيرًا.
ولأن CPAL رخصة «نسخ مفتوح ضعيف» (weak copyleft)، فيمكن للمطوّرين دمجها بحرّية مع برمجيات حرة أو مملوكة، ولا يحتاجون سوى إلى إصدار التعديلات على SocialCalc نفسه. وقد مكّن ذلك مختلف المجتمعات من تبنّي SocialCalc وجعله أكثر روعة.
وهناك إمكانيات كثيرة ومثيرة مع هذا المحرّك مفتوح المصدر لجداول البيانات، وإذا استطعت إيجاد طريقة لتضمين SocialCalc في مشروعك المفضّل، فسنكون بالتأكيد سعداء بسماعك.
الحواشي
https://github.com/audreyt/wikiwyg-jshttp://one.laptop.org/http://seeta.in/wiki/index.php?title=Collaboration_in_SocialCalchttp://search.cpan.org/dist/Web-Hippie/http://about.digg.com/blog/duistream-and-mxhrhttps://github.com/gimite/web-socket-jshttp://perlcabal.org/syn/S02.htmlhttp://fit.c2.com/http://search.cpan.org/dist/Test-WWW-Mechanize/http://search.cpan.org/dist/Test-WWW-Selenium/https://www.socialtext.net/open/?cpalhttp://opensource.org/http://www.fsf.orghttps://github.com/facebook/platformhttps://github.com/reddit/reddit
https://github.com/audreyt/wikiwyg-js↩︎http://one.laptop.org/↩︎http://seeta.in/wiki/index.php?title=Collaboration_in_SocialCalc↩︎http://search.cpan.org/dist/Web-Hippie/↩︎http://about.digg.com/blog/duistream-and-mxhr↩︎https://github.com/gimite/web-socket-js↩︎http://perlcabal.org/syn/S02.html↩︎http://fit.c2.com/↩︎http://search.cpan.org/dist/Test-WWW-Mechanize/↩︎http://search.cpan.org/dist/Test-WWW-Selenium/↩︎https://www.socialtext.net/open/?cpal↩︎http://opensource.org/↩︎http://www.fsf.org↩︎https://github.com/facebook/platform↩︎https://github.com/reddit/reddit↩︎