إعادة هندسة البرمجيات: من السباغيتي إلى التصميم النظيف
نشرت: 2022-03-11هل يمكنك إلقاء نظرة على نظامنا؟ الشخص الذي كتب البرنامج لم يعد موجودًا وقد واجهنا عددًا من المشكلات. نحن بحاجة إلى شخص ما للنظر في الأمر وتنظيفه من أجلنا.
يعرف أي شخص كان يعمل في هندسة البرمجيات لفترة معقولة من الوقت أن هذا الطلب الذي يبدو بريئًا غالبًا ما يكون بداية مشروع "به كارثة مكتوبة في كل مكان". يمكن أن يكون توريث رمز شخص آخر كابوسًا ، خاصةً عندما يكون التصميم سيئًا ويفتقر إلى التوثيق.
لذلك عندما تلقيت مؤخرًا طلبًا من أحد عملائنا لإلقاء نظرة على تطبيق خادم الدردشة socket.io الحالي (المكتوب بلغة Node.js) وتحسينه ، كنت حذرًا للغاية. لكن قبل الترشح للتلال ، قررت على الأقل الموافقة على إلقاء نظرة على الكود.
لسوء الحظ ، فإن النظر في المدونة يؤكد فقط مخاوفي. تم تنفيذ خادم الدردشة هذا كملف جافا سكريبت واحد كبير. إن إعادة هندسة هذا الملف الأحادي الأحادي في برنامج مصمم بشكل نظيف ويمكن صيانته بسهولة سيكون تحديًا بالفعل. لكني أستمتع بالتحدي ، لذلك وافقت.
نقطة البداية - استعد لإعادة الهندسة
يتكون البرنامج الحالي من ملف واحد يحتوي على 1200 سطر من التعليمات البرمجية غير الموثقة. ييكيس. علاوة على ذلك ، كان من المعروف أنه يحتوي على بعض الأخطاء وأن به بعض مشكلات الأداء.
بالإضافة إلى ذلك ، كشف فحص ملفات السجل (دائمًا ما يكون مكانًا جيدًا للبدء عند وراثة رمز شخص آخر) عن مشكلات تسرب الذاكرة المحتملة. في مرحلة ما ، تم الإبلاغ عن أن العملية تستخدم أكثر من 1 جيجابايت من ذاكرة الوصول العشوائي.
نظرًا لهذه المشكلات ، أصبح من الواضح على الفور أن الكود بحاجة إلى إعادة تنظيم ووحدة نمطية قبل محاولة تصحيح منطق الأعمال أو تحسينه. وتحقيقا لهذه الغاية ، تضمنت بعض القضايا الأولية التي يتعين معالجتها ما يلي:
- هيكل الكود. لم يكن للكود بنية حقيقية على الإطلاق ، مما يجعل من الصعب التمييز بين التكوين والبنية التحتية ومنطق الأعمال. لم يكن هناك أساسًا نمطية أو فصل للمخاوف.
- كود فائض. تم تكرار بعض أجزاء الكود (مثل رمز معالجة الأخطاء لكل معالج حدث ، ورمز إجراء طلبات الويب ، وما إلى ذلك) عدة مرات. لا يعد تكرار الكود شيئًا جيدًا أبدًا ، مما يجعل الحفاظ على الكود أكثر صعوبة وأكثر عرضة للأخطاء (عندما يتم إصلاح أو تحديث الشفرة الزائدة في مكان ما ولكن ليس في مكان آخر).
- القيم المشفرة. احتوت الشفرة على عدد من القيم المشفرة (نادرًا ما تكون جيدة). إن القدرة على تعديل هذه القيم من خلال معلمات التكوين (بدلاً من طلب تغييرات على القيم المشفرة في التعليمات البرمجية) ستزيد من المرونة ويمكن أن تساعد أيضًا في تسهيل الاختبار وتصحيح الأخطاء.
- تسجيل. كان نظام التسجيل أساسيًا جدًا. سيُنشئ ملف سجل عملاقًا واحدًا كان من الصعب تحليله أو تحليله.
الأهداف المعمارية الرئيسية
في عملية البدء في إعادة هيكلة الكود ، بالإضافة إلى معالجة المشكلات المحددة المحددة أعلاه ، أردت أن أبدأ في معالجة بعض الأهداف المعمارية الرئيسية التي (أو على الأقل يجب أن تكون) مشتركة في تصميم أي نظام برمجي . وتشمل هذه:
- قابلية الصيانة. لا تكتب أبدًا برامج تتوقع أن تكون الشخص الوحيد الذي سيحتاج إلى صيانتها. ضع في اعتبارك دائمًا مدى سهولة فهم شفرتك لشخص آخر ، ومدى سهولة تعديلها أو تصحيحها.
- التمدد. لا تفترض أبدًا أن الوظيفة التي تقوم بتنفيذها اليوم هي كل ما ستحتاج إليه على الإطلاق. صمم برامجك بطرق يسهل توسيعها.
- نمطية. وظائف منفصلة إلى وحدات منطقية ومميزة ، لكل منها غرضها ووظيفتها الواضحة.
- قابلية التوسع. ينفد صبر مستخدمي اليوم بشكل متزايد ، ويتوقعون أوقات استجابة فورية (أو على الأقل قريبة من فورية). يمكن أن يتسبب الأداء الضعيف وزمن الانتقال المرتفع في فشل التطبيق الأكثر فائدة في السوق. كيف سيكون أداء برنامجك مع زيادة عدد المستخدمين المتزامنين ومتطلبات النطاق الترددي؟ يمكن أن تساعد تقنيات مثل الموازاة وتحسين قاعدة البيانات والمعالجة غير المتزامنة في تحسين قدرة نظامك على البقاء مستجيبًا ، على الرغم من زيادة الحمل ومتطلبات الموارد.
إعادة هيكلة القانون
هدفنا هو الانتقال من ملف شفرة مصدر mongo أحادي إلى مجموعة معيارية من المكونات المهيكلة بشكل نظيف. يجب أن تكون الشفرة الناتجة أسهل في الحفاظ عليها وتحسينها وتصحيحها.
بالنسبة لهذا التطبيق ، قررت تنظيم الكود في المكونات المعمارية المتميزة التالية:
- app.js - هذه نقطة دخولنا ، سيتم تشغيل الكود الخاص بنا من هنا
- التكوين - هذا حيث توجد إعدادات التكوين الخاصة بنا
- ioW - "مجمّع IO" الذي سيحتوي على منطق IO (والأعمال)
- تسجيل الدخول - جميع الرموز المتعلقة بالتسجيل (لاحظ أن بنية الدليل ستتضمن أيضًا مجلد
logs
جديدًا يحتوي على جميع ملفات السجل) - package.json - قائمة تبعيات الحزمة لـ Node.js
- node_modules - جميع الوحدات التي تتطلبها Node.js
لا يوجد سحر في هذا النهج المحدد ؛ يمكن أن يكون هناك العديد من الطرق المختلفة لإعادة هيكلة الكود. لقد شعرت شخصيًا أن هذه المنظمة كانت نظيفة بما فيه الكفاية ومنظمة جيدًا دون أن تكون شديدة التعقيد.
الدليل الناتج وتنظيم الملف مبين أدناه.
تسجيل
تم تطوير حزم التسجيل لمعظم بيئات التطوير واللغات الحالية ، لذلك من النادر في الوقت الحاضر أن تحتاج إلى "إنشاء إمكانية التسجيل الخاصة بك".
نظرًا لأننا نعمل مع Node.js ، فقد اخترت log4js-node ، وهو في الأساس نسخة من مكتبة log4js للاستخدام مع Node.js. تحتوي هذه المكتبة على بعض الميزات الرائعة مثل القدرة على تسجيل عدة مستويات من الرسائل (تحذير ، خطأ ، وما إلى ذلك) ويمكن أن يكون لدينا ملف متجدد يمكن تقسيمه ، على سبيل المثال ، على أساس يومي ، لذلك لا يتعين علينا ذلك التعامل مع الملفات الضخمة التي ستستغرق الكثير من الوقت لفتحها ويصعب تحليلها وتحليلها.
لأغراضنا ، قمت بإنشاء غلاف صغير حول عقدة log4js لإضافة بعض القدرات الإضافية المحددة المطلوبة. لاحظ أنني اخترت إنشاء غلاف حول عقدة log4js والذي سأستخدمه بعد ذلك في التعليمات البرمجية الخاصة بي. يؤدي هذا إلى ترجمة تنفيذ إمكانات التسجيل الموسعة هذه في مكان واحد وبالتالي تجنب التكرار والتعقيد غير الضروري في التعليمات البرمجية الخاصة بي عند استدعاء التسجيل.
نظرًا لأننا نعمل مع I / O ، وسيكون لدينا العديد من العملاء (المستخدمين) الذين سينتجون عدة اتصالات (مآخذ) ، أريد أن أكون قادرًا على تتبع نشاط مستخدم معين في ملفات السجل ، وأريد أيضًا معرفة مصدر كل إدخال في السجل. لذلك أتوقع أن يكون لدي بعض إدخالات السجل فيما يتعلق بحالة التطبيق ، وبعض إدخالات خاصة بنشاط المستخدم.
في كود غلاف التسجيل الخاص بي ، يمكنني تعيين معرف المستخدم والمآخذ ، مما سيسمح لي بتتبع الإجراءات التي تم تنفيذها قبل وبعد حدث خطأ. سيسمح لي برنامج تضمين التسجيل أيضًا بإنشاء مسجلات مختلفة بمعلومات سياقية مختلفة يمكنني تمريرها إلى معالجات الأحداث حتى أعرف مصدر إدخال السجل.
يتوفر رمز غلاف التسجيل هنا.
إعدادات
غالبًا ما يكون من الضروري دعم التكوينات المختلفة للنظام. يمكن أن تكون هذه الاختلافات إما اختلافات بين بيئات التطوير والإنتاج ، أو حتى بناءً على الحاجة إلى عرض بيئات العملاء المختلفة وسيناريوهات الاستخدام.
بدلاً من طلب تغييرات في التعليمات البرمجية لدعم ذلك ، تتمثل الممارسة الشائعة في التحكم في هذه الاختلافات في السلوك عن طريق معلمات التكوين. في حالتي ، كنت بحاجة إلى القدرة على الحصول على بيئات تنفيذ مختلفة (التدريج والإنتاج) ، والتي قد تكون لها إعدادات مختلفة. أردت أيضًا التأكد من أن الكود الذي تم اختباره يعمل جيدًا في كل من التدريج والإنتاج ، وإذا كنت بحاجة إلى تغيير الكود لهذا الغرض ، فسيؤدي ذلك إلى إبطال عملية الاختبار.

باستخدام متغير بيئة Node.js ، يمكنني تحديد ملف التكوين الذي أريد استخدامه لتنفيذ معين. لذلك قمت بنقل جميع معلمات التكوين التي تم ترميزها مسبقًا إلى ملفات التكوين ، وقمت بإنشاء وحدة تكوين بسيطة تقوم بتحميل ملف التكوين المناسب بالإعدادات المطلوبة. لقد صنفت أيضًا جميع الإعدادات لفرض درجة معينة من التنظيم على ملف التكوين ولتسهيل التنقل.
فيما يلي مثال على ملف التكوين الناتج:
{ "app": { "port": 8889, "invRepeatInterval":1000, "invTimeOut":300000, "chatLogInterval":60000, "updateUsersInterval":600000, "dbgCurrentStatusInterval":3600000, "roomDelimiter":"_", "roomPrefix":"/" }, "webSite":{ "host": "mysite.com", "port": 80, "friendListHandler":"/MyMethods.aspx/FriendsList", "userCanChatHandler":"/MyMethods.aspx/UserCanChat", "chatLogsHandler":"/MyMethods.aspx/SaveLogs" }, "logging": { "appenders": [ { "type": "dateFile", "filename": "logs/chat-server", "pattern": "-yyyy-MM-dd", "alwaysIncludePattern": false } ], "level": "DEBUG" } }
تدفق التعليمات البرمجية
لقد أنشأنا حتى الآن بنية مجلد لاستضافة الوحدات النمطية المختلفة ، وأعددنا طريقة لتحميل معلومات خاصة بالبيئة ، وأنشأنا نظام تسجيل ، لذلك دعونا نرى كيف يمكننا ربط جميع الأجزاء معًا دون تغيير رمز العمل المحدد.
بفضل هيكلنا المعياري الجديد للرمز ، فإن app.js
بنقطة الدخول بسيط بما فيه الكفاية ، ويحتوي على رمز التهيئة فقط:
var config = require('./config'); var logging = require('./logging'); var ioW = require('./ioW'); var obj = config.getCurrent(); logging.initialize(obj.logging); ioW.initialize(config);
عندما حددنا هيكل الكود الخاص بنا ، قلنا أن مجلد ioW
سيحتوي على الكود المرتبط بالأعمال و socket.io. على وجه التحديد ، ستحتوي على الملفات التالية (لاحظ أنه يمكنك النقر فوق أي من أسماء الملفات المدرجة لعرض كود المصدر المقابل):
-
index.js
- يعالج تهيئة واتصالات socket.io بالإضافة إلى اشتراك الحدث ، بالإضافة إلى معالج أخطاء مركزي للأحداث -
eventManager.js
- يستضيف كل المنطق المتعلق بالأعمال (معالجات الأحداث) -
webHelper.js
- الطرق المساعدة لتنفيذ طلبات الويب. -
linkedList.js
- فئة الأداة المساعدة لقائمة مرتبطة
أعدنا تشكيل الكود الذي يطلب الويب ونقلناه إلى ملف منفصل ، وتمكنا من الاحتفاظ بمنطق أعمالنا في نفس المكان وعدم تعديله.
ملاحظة مهمة واحدة: في هذه المرحلة ، لا يزال eventManager.js
يحتوي على بعض الوظائف المساعدة التي يجب حقًا استخراجها في وحدة منفصلة. ومع ذلك ، نظرًا لأن هدفنا في هذا التمرير الأول كان إعادة تنظيم الكود مع تقليل التأثير على منطق الأعمال ، وهذه الوظائف المساعدة مرتبطة بشكل معقد جدًا بمنطق الأعمال ، فقد اخترنا تأجيل ذلك لتمرير لاحق في تحسين تنظيم الشفرة.
نظرًا لأن Node.js غير متزامن بحكم التعريف ، فإننا غالبًا ما نواجه بعضًا من عش الفئران من "جحيم رد الاتصال" مما يجعل من الصعب بشكل خاص التنقل في الشفرة وتصحيحها. لتجنب هذا المأزق ، في تطبيقي الجديد ، استخدمت نمط الوعود واستفد بشكل خاص من بلوبيرد وهي مكتبة وعود لطيفة وسريعة للغاية. ستتيح لنا الوعود أن نكون قادرين على اتباع الكود كما لو كان متزامنًا وكذلك توفير إدارة الأخطاء وطريقة نظيفة لتوحيد الاستجابات بين المكالمات. يوجد عقد ضمني في الكود الخاص بنا مفاده أن كل معالج حدث يجب أن يقدم وعدًا حتى نتمكن من إدارة معالجة الأخطاء المركزية وتسجيلها.
ستعيد جميع معالجات الأحداث وعدًا (سواء أجرت استدعاءات غير متزامنة أم لا). مع وجود هذا في مكانه الصحيح ، يمكننا مركزية معالجة الأخطاء وتسجيلها ونتأكد من أنه إذا كان لدينا خطأ غير معالج داخل معالج الأحداث ، فسيتم اكتشاف هذا الخطأ.
function execEventHandler(socket, eventName, eventHandler, data){ var sLogger = logging.createLogger(socket.id + ' - ' + eventName); sLogger.info(''); eventHandler(socket, data, sLogger).then(null, function(err){ sLogger.error(err.stack); }); };
في مناقشتنا للتسجيل ، ذكرنا أن كل اتصال سيكون له مسجل خاص به مع معلومات سياقية فيه. على وجه التحديد ، نحن نربط معرف المقبس واسم الحدث بالمسجل عندما نقوم بإنشائه ، لذلك عندما نقوم بتمرير هذا المسجل إلى معالج الأحداث ، سيحتوي كل سطر في السجل على تلك المعلومات:
var sLogger = logging.createLogger(socket.id + ' - ' + eventName);
نقطة أخرى تستحق الذكر فيما يتعلق بمعالجة الحدث: في الملف الأصلي ، كان لدينا استدعاء دالة setInterval
كان داخل معالج الحدث لحدث اتصال socket.io ، وقد حددنا هذه الوظيفة على أنها مشكلة.
io.on('connection', function (socket) { ... Several event handlers .... setInterval(function() { try { var date = Date.now(); var tmp = []; while (0 < messageHub.count() && messageHub.head().date < date) { var item = messageHub.remove(); tmp.push(item); } ... Post Data to an external web service... } catch (e) { log('ERROR: ex: ' + e); } }, CHAT_LOGS_INTERVAL); });
يقوم هذا الرمز بإنشاء مؤقت بفاصل زمني محدد (في حالتنا كان دقيقة واحدة) لكل طلب اتصال فردي نحصل عليه. لذلك ، على سبيل المثال ، إذا كان لدينا في أي وقت من الأوقات 300 مقبس على الإنترنت ، فسيكون لدينا 300 جهاز توقيت يعمل كل دقيقة. المشكلة في هذا ، كما ترى في الكود أعلاه ، هي أنه لا يوجد استخدام للمقبس ولا أي متغير تم تعريفه ضمن نطاق معالج الأحداث. المتغير الوحيد الذي يتم استخدامه هو متغير messageHub
يتم الإعلان عنه على مستوى الوحدة النمطية مما يعني أنه هو نفسه لجميع الاتصالات. لذلك ليست هناك حاجة على الإطلاق إلى مؤقت منفصل لكل اتصال. لذلك قمنا بإزالة هذا من معالج حدث الاتصال وقمنا بتضمينه في كود التهيئة العام الخاص بنا ، والذي في هذه الحالة هو وظيفة initialize
.
أخيرًا ، أثناء معالجتنا للردود في webHelper.js
، أضفنا معالجة لأي استجابة غير معروفة والتي ستسجل المعلومات والتي ستكون مفيدة بعد ذلك في عملية التصحيح:
if (!res || !res.d || !res.d.IsValid){ logger.debug(sendData); logger.debug(data); reject(new Error('Request failed. Path ' + params.path + ' . Invalid return data.')); return; }
الخطوة الأخيرة هي إعداد ملف تسجيل للخطأ القياسي لـ Node.js. سيحتوي هذا الملف على أخطاء لم تتم معالجتها والتي ربما فاتتنا. لتعيين عملية العقدة في Windows (ليست مثالية ولكنك تعلم ...) كخدمة ، نستخدم أداة تسمى nssm والتي تحتوي على واجهة مستخدم مرئية تتيح لك تحديد ملف إخراج قياسي وملف خطأ قياسي ومتغيرات بيئية.
حول أداء Node.js
Node.js هي لغة برمجة ذات ترابط واحد. من أجل تحسين قابلية التوسع ، هناك العديد من البدائل التي يمكننا استخدامها. هناك وحدة مجموعة العقدة أو مجرد إضافة المزيد من عمليات العقد ووضع nginx فوق تلك لتقوم بإعادة التوجيه وموازنة التحميل.
في حالتنا ، على الرغم من ذلك ، نظرًا لأن كل عملية فرعية لمجموعة العقدة أو عملية عقدة سيكون لها مساحة ذاكرة خاصة بها ، فلن نتمكن من مشاركة المعلومات بين هذه العمليات بسهولة. لذلك في هذه الحالة بالذات ، سنحتاج إلى استخدام مخزن بيانات خارجي (مثل redis) من أجل الحفاظ على مآخذ التوصيل عبر الإنترنت متاحة للعمليات المختلفة.
خاتمة
مع كل هذا في مكانه الصحيح ، حققنا تنظيفًا كبيرًا للشفرة التي تم تسليمها إلينا في الأصل. لا يتعلق الأمر بجعل الكود مثاليًا ، بل يتعلق بإعادة هندسته لإنشاء أساس معماري نظيف يسهل دعمه وصيانته ، ويسهل تصحيح الأخطاء ويبسطها.
بالالتزام بمبادئ تصميم البرامج الرئيسية التي تم تعدادها سابقًا - قابلية الصيانة والتوسعة والنمطية والقابلية للتوسع - قمنا بإنشاء وحدات وبنية رمز تحدد بوضوح ونقاء مسؤوليات الوحدة المختلفة. لقد حددنا أيضًا بعض المشكلات في التنفيذ الأصلي التي أدت إلى استهلاك كبير للذاكرة أدى إلى تدهور الأداء.
آمل أن تكون قد استمتعت بالمقال ، اسمح لي أن أعرف إذا كان لديك المزيد من التعليقات أو الأسئلة.