الشفرة ذات الرائحة النتنة

ما هو اعادة تصنيع الشفرة او Code Refactoring  يستخدم هذا المصطلح للإشارة إلى عملية اعادة كتابة الشفرة بطرقة أفضل, إعادة كتابة الشفرة لا تضيف أية مميزات على المميزات التي يراها المستخدم الحالي لنظام او التطبيق و لكنها تضيف الكثير لمن يرغب بتطوير الشفرة و قراءتها. في اغلب الشفرات التي نقوم بكتابتها في كثير من الاحيان تكون المرة الاولى التي كتبة بها الشفرة سيئة فأنت تكون تحت ضغوط التطوير و تحت ضغوط خط النهاية و العميل  و غيرها من الضغوطات التي تجعلك تقوم بكتابة شفرة بشكل سريع متجاهلا ابجديات كتابة الشفرات.

الشفرة ذات الرائحة النتنة

هو مصطلح أطلق في اواخر التسعينات تقريبا Code Smell حيث الفكرة من هذا المصلح هو عبارى عن مجموعة من الشواهد و الدلائل  التي تدل على ان شفرة ما يحب أن يتم إعادة تصنيعها أو كتابتها من جديد, سأذكر هنا أهمها على الطلاق:
 الشفرة المكررة:
هي أحد أكثر المشاكل التي يتعرض لها المطور في كل سطر يكتبه تقريبا و هي عملية تكرار الشفرة, إذا تعرض إلى شفرة تعتقد أنك ستستخدمها في جزء اخر من المروع من الأفضل دائما أن تقوم بإعادة كتابة الشفرة الخاصة بك بشكل يضمن عدم تكرار الشفرة, على سبيل المثال عندما يكون لديك سطر معين تستخدمه في طريقتين أو اكثر عندها ستحتاج إلى كتابة هذا الجزء من الشفرة في طريقة مستقلة فكر مثلا في حالات مثل:
   1: this.UserName = this.TextBoxUserName.Text;

حيث هي من أكثر الحالات التي تتكرر بها الشفرة فكر بشكل جدي اذا كنت سوق تستخدم هذه الشفرة في اكثر من جزء لتعين المتغير UserName من الصندوق النص و فجأه تغيرت المتطلبات من اجل اضافة Term قبل تخزين القيمة في المتغير UserName عندها ستطر إلى البحث عن هذا البحث في جميع أجزاء هذه الشفرة دون استثناء و تعديلها سطرا سطر.
  • الطرق الطويلة
الطرف الطويلة هي اكثر الاشارات و الدلائل على أن شفرة ما سيئة و تحتاج إلى إعادة كتابة من جديد, هناك قاعدة تقول يجب أن لا تتجاوز طرقتك 10 أسطر فقط اذا كانت الطريقة تحتوي على سطور كثيرة فهذا يعني انها تقوم بهام اكثر من الازم , خذ على سبيل المثال الطريقة التي تعيد كائن ما اي كان هذا الكائن من قاعدة البيانات فإن الاجزاء المتعلقة بفتح الاتصال و تنفيذ جملة الاستعلام و قراءة السجلات كل هذه الخطوات ليست من مهام الطريقة الاساسية إنما هي امور مساعدة لها, أن العمل الرئيسي الذي تقوم به هذه العملية هو المرور على سجلات من استعلام معين و تحويلها إلى كائن X. 

و السؤال المطروح هنا لماذا تعتبر الطرق الطويل عيبا يجب التخلص منه و السبب بسيط جدا لأنها تحاج المزيد من الوقت في القراءة و الفهم و هي صعبة و معقدة جدا فيما يخص صيانتها أضف إلى مشكلة اخرى اختراقها لأحد قوانين كتابة الشفرات و هو قانون الاستقلالية الذي ينص صراحة على أن كل طريقة و كائن و تصنيف و واجهة عمل واحد فقط لا غير.  لمعالجة هذه المشكلة يمنكن تقسيم الطريقة الطويلة إلى اجزاء أصغر بمسميات أكثر توضيحا فهذا من شأنها تسهيل قراءة شفرتك و توضيحها أكثر بالإضافة.
  • التصنيفات الطويلة
التصنيفات الطويلة هي نفس المشكلة مع الطرق الطويلة فيها حمقاء للغاية و تحوي على الكثير من الاسطر و الشفرة و تحتقر و تتجاهل تمام مبادئ البرمجة الكائنية أضف قوانين كتابتها مثل الاستقلالية و الوراثة و غيرها. يجب دائما أن لا يتجاوز عدد اسطر كتابة الشفرة 100 سطر فقط لا غير, يمكنك تقسيم المهام المتعلقة بتصنيف معين إلى مجموعة من التصنيفات التي تحمل نفس الاسم مثلا مشتقة من بعضها البعض يمكنك مثلا تطبيق ما يعرف بـ NTier Application لتقسيم المهام أو اتباع اي اسلوب Design Pattern متعارف عليه.  أن التصنيفات الطويلة التي تحتاج إلى العديد من المشيدات و العديد الطرق بداخلها عادة تكون هي سبب الكثير من المشاكل التي لا يمكن حصرها و ذلك لسبب بسيط جدا لان هناك اجزأ كثيرة من الشفرة تم تكرارها.
  • قائمة طويلة من ( Parameter )
اذا كانت الطريقة التي تقوم بكتابتها تستقبل أكثر من 3 معاملات فربما تحتاج ان تحول هذه المتغيرات إلى كائن بدلا عن متغيرات عادية, و الفكرة في الامر أن تمريه على شكل كائن من شأنه ان يكون اكثر وضوحا مقارنة مع تمريره على شكل متغيرات , لماذا قائمة طويلة من الـ Parameters يعتبر مشكلة , السبب بسيط جدا للنظر إلى الطريقة التالية:


   1: public void SendFileToServer(string fileName, string filePath,
   2:                              string createdBy, DateTime createdDate, 
   3:                             FileType fileType, byte[] content)
   4: {
   5: }
و قرانها بـهذه الشفرة :
   1: public void SendFileToServer(File file)
   2: {
   3: }
  • اسالني عن التعديل
اطلب من المطور ان يقوم بتعديل شيء ما في المتطلبات , مثلا اذا كان المطور يقوم بتطوير شفرة تهتم بفكرة معينة و عند تنفيذ هذه الشفرة يعيد المطور قيمة معينة اذا كان الاجراء صحيحة و اخرى اذا كان الاجراء خاطئا مثلا خطئ في ادخال البيانات, إن التعديل الذي تغرب به في هذه الحالية هو فقط رمي Exception بدلا من اعادة كائن , فمن المنطق أن يتم تعديل جزء واحد في الشفرة و هو جزء الـ Try –Catch و لكن اذا قمت بتعديل اكثر من جزء في مثالي هذا فهذا يعني ان شفرتي بها شيء من الخطأ و يجب ان يتم اعادة توزيعها, يجب أن تتمتع التطبيق بقدر كافي من المرونة لمساح لها بتعديل.

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


Capture

image


ارسال الملفات بين خدمات WCF

مرحبا إن ارسال الملفات تعتبر من المهام الأكثر انتشارا تقريبا في خدمات الويب , هذه المتطلبات على سبيل المثال عندما تحتاج أن تقوم برسال ملفات من منظمة إلى اخرى طبعا هناك حلولا كثيرة لهذه المشكلة , على سبيل المثال استخدام  JMS و المشكلة في JMS أنه احمق و يحتاج للكثير من الوقت و الاعدادات أضف إلى استهلاكه الكثير من موارد الجهاز,   فهو حل مكلف مقارنة بالمهمة البسيطة التي تريد عملها الامر أشبه بـ استخدام دبابة لصيد حمامة !!.

المشكلة التي واجهتني بسيطة لدي تطبيق معين يقوم هذا التطبيق بتوليد ملفات حيث طلب مني ارسال هذه الملفات إلى عميل أخر كنت قد حللت هذه المشكلة سابقا باستخدام File Tunnel , و عندما اردنا الذهاب لتجربة رفض العميل استخدام هذه الطريقة كون منصة العمل لديه لا تدعم نهائيا Windows , لذلك كان الحل المقترح هو بناء Web Service  من طرفهم و يجب عليا انا ان اقوم باستخدام هذه الخدمة من أجل ارسال الملفات.

المشكلة التي طرأت بأن هذا التطبيق الذي يقوم بتوليد الملفات يمكن لأكثر من جهة استخدام هذه الملفات بمعنى أخر سيكون لدي الجهة X بخدمة ويب مختلف عن الجهة Y هذا يعني أنني بحاجة لكتابة تطبيق مختلف لكل جهة معينة !! , و لأنني من الأشخاص الاكثر كسلا على وجه الارض و اكثر كلمة استخدمها في حياتي هي " مشووووار " قررات كتابة حل  يحقق هذا ثلاثة نقاط :

  • استقلالية كل جهة عن الاخرى في عملية نقل الملفات بحيث يمكن تعين مجلد لكل جهة و بالتالي اضمن في حالة الفشل لجهة معينة عدم فشل الاخرى و ايضا عدم اختلاط الملفات مع بعضها البعض.
  • لا تكرار لشفرة نهائيا يذكر على الاطلاق مهما اختلف العميل :- ).
  • اذا اتى عميل جديد مستقبلا لن أحتاج سوى إلى نسخ و لصق و تعديل الاعدادات فقط و هي مهمة يستطيع عملها مطور التطبيق و ليس مطور الربط :-) .

رائع حسنا لنبدأ بالعمل, في البداية لديك الفكرة التالية , يقوم التطبيق المستهدف بتوليد ملفات معينة في مجلد خاص و من هنا تأتي مهمة الربط في سحب هذه الملفات و ارسالها لدى العميل الذي سيوفر بدوره خدمة ويب تستقبل اسم الملف و Byte[] كمحتوى لهذا الملف.

اذا الخطوة الأولى هو تحديد المجلد الذي نود مراقبتها و من ثم انشاء كائن من النوع Folder Watcher لمراقبة هذا المجلد, الشفرة:

   1: private string folderToWatchPath = string.Empty;
   2:  
   3: internal FolderWatcher()
   4: {
   5:     this.folderToWatchPath = ConfigManager.GetStringValue("FolderToWatch", true);
   6:     this.StartWatchingFolder();
   7: }
   8:  
   9: internal void StartWatchingFolder()
  10: {
  11:     FileSystemWatcher fileWatcher = new FileSystemWatcher(this.folderToWatchPath);
  12:     fileWatcher.Created += this.FileWatcher_Created;
  13:     fileWatcher.EnableRaisingEvents = true;
  14: }

في الشفرة التي في الأعلى اقوم بالبحث عن خاصية FolderToWatch في ملف الـ app.config اذا وجدتها سأبدأ بعملية المراقبة للمجلد , و عملية المراقبة بسيطة حيث عندما يتم توليد ملف جديد في هذا المجلد أقوم بعملية Fire للحدث FileWatcher_Create , الشفرة:



   1: internal void FileWatcher_Created(object sender, FileSystemEventArgs e)
   2: {
   3:     if (this.OnFileCreated != null)
   4:     {
   5:         if (this.IsFileLocked(e.FullPath) == false)
   6:         {
   7:             FileToSend file = new FileToSend() { Content = File.ReadAllBytes(e.FullPath), Name = e.Name, CreateDate = DateTime.Now, Path = e.FullPath };
   8:             this.OnFileCreated(file);
   9:         }
  10:     }
  11: }

و تحقيقا لمبدأ الاستقلالية لن أقوم مباشرة هنا بإرسال الملف حيث أن هذا التنصيف ستكون مهتمة الوحيدة هو مراقبة مجلد كما هو واضح بالتسمية, الخطوة التالية عند اطلاق الحدث FileWatcher_Create اقوم مباشرة باطلاق الحدث OnNewFileCreated و هو حدث خاص بتصنيف FolderWatcher الذي نعمل عليه حاليا ’ الشفرة:



   1: internal delegate void OnNewFileCreated(FileToSend file);
   2:  
   3: internal event OnNewFileCreated OnFileCreated;
   4:  
   5: internal void FileWatcher_Created(object sender, FileSystemEventArgs e)
   6: {
   7:     if (this.OnFileCreated != null)
   8:     {
   9:         if (this.IsFileLocked(e.FullPath) == false)
  10:         {
  11:             FileToSend file = new FileToSend() { Content = File.ReadAllBytes(e.FullPath), Name = e.Name, CreateDate = DateTime.Now, Path = e.FullPath };
  12:             this.OnFileCreated(file);
  13:         }
  14:     }
  15: }

يقوم هذا الحدث مباشرة بتحقق من ان الملف لا توجد عليه أية عمليات اخرى من النظام, ثم يقوم بعملية Fire للحدث OnFileCreated و الذي يستقبل الكائن FileToSend ممرا له خصائص و بيانات الملف الذي تم إنشائه.



   1: namespace FileSenderBase
   2: {
   3:     using GeniusesCode.Helper;
   4:     using System;
   5:     using System.IO;
   6:  
   7:     internal class FileToSend
   8:     {
   9:         internal string Name { get; set; }
  10:  
  11:         internal string Path { get; set; }
  12:  
  13:         internal byte[] Content { get; set; }
  14:  
  15:         internal DateTime CreateDate { get; set; }
  16:  
  17:         internal void Delete()
  18:         {
  19:             if (File.Exists(this.Path))
  20:             {
  21:                 File.Delete(this.Path);
  22:             }
  23:         }
  24:  
  25:         internal void MoveToFailedFolder()
  26:         {
  27:             string directory = new FileInfo(this.Path).Directory.FullName;
  28:             string failedFolderName = ConfigManager.GetStringValue("FailedFolderName");
  29:             this.CreateFailedFolderIfIsNotExists(directory, failedFolderName);
  30:             this.DeleteFileIfExists(directory, failedFolderName);
  31:             File.Move(this.Path, directory + "\\" + failedFolderName + "\\" + this.Name);
  32:         }
  33:  
  34:         private void DeleteFileIfExists(string directory, string failedFolderName)
  35:         {
  36:             if (File.Exists(directory + "\\" + failedFolderName + "\\" + this.Name) == true)
  37:             {
  38:                 File.Delete(directory + "\\" + failedFolderName + "\\" + this.Name);
  39:             }
  40:         }
  41:  
  42:         private void CreateFailedFolderIfIsNotExists(string directory, string failedFolderName)
  43:         {
  44:             if (Directory.Exists(new FileInfo(this.Path).Directory.FullName + "\\" + failedFolderName + "\\") == false)
  45:             {
  46:                 Directory.CreateDirectory(directory + "\\" + failedFolderName + "\\");
  47:             }
  48:         }
  49:     }
  50: }

الكائن FileToSend بسيط جدا و لا يحتوي على اية أشاء خاصة , لديه الطريقة Delete حيث يقوم بحذف نفسه و الطريقة MoveToFailedFolder و التي تقوم بنقل الملف إلى مجلد Failed Folder عند فشل الملف في عملية الارسال.


الخطو التالية الأن هو تمرير بيانات الخدمة الخاصة بالمنظمة X بيانات رابط الـ WSDL للخدمة و اسم الطريقة في الخدمة لأنني سأعطي الحرية للمنظمة بختيار الاسم الذي يرغبون بدلا عن اجبارهم باسم معين أيضا اسم المستخدم و كلمة المرور ان احتاج الامر لذلك, الشفرة:



   1: namespace FileSenderBase
   2: {
   3:     using GeniusesCode.Helper;
   4:  
   5:     internal class OrganizationService
   6:     {
   7:         internal OrganizationService()
   8:         {
   9:             this.ServiceLink = ConfigManager.GetStringValue("ServiceLink");
  10:             this.MethodName = ConfigManager.GetStringValue("MethodName");
  11:             this.IsAuthenticationRequired = ConfigManager.GetBooleanValue("isAuthenticationRequired", false);
  12:             if (this.IsAuthenticationRequired)
  13:             {
  14:                 this.UserName = ConfigManager.GetStringValue("UserName");
  15:                 this.Password = ConfigManager.GetStringValue("Password");
  16:             }
  17:         }
  18:  
  19:         internal bool IsAuthenticationRequired { get; private set; }
  20:  
  21:         internal string Password { get; private set; }
  22:  
  23:         internal string ServiceLink { get; private set; }
  24:  
  25:         internal string UserName { get; private set; }
  26:  
  27:         internal string MethodName { get; private set; }
  28:     }
  29: }

الخطوة التالية الأن بعد أن حصلت على الملف و بيانات الخدمة أحتاج لراسل البيانات إلى و ذلك عن طريق التصنيف ServiceCaller و الذي يحتوي على الطريقة PushFile و التي تستقبل بيانات منظمة و ايضا بيانات الملف الذي ترغب بإرساله , الشفرة:



   1: namespace FileSenderBase
   2: {
   3:     using DynamicLibrary;
   4:     using GeniusesCode.Helper;
   5:  
   6:     internal class ServiceCaller
   7:     {
   8:         internal void PushFile(OrganizationService organizationService, FileToSend file)
   9:         {
  10:             DynamicProxyFactory factory = new DynamicProxyFactory(organizationService.ServiceLink);
  11:             var endPoints = factory.Endpoints.GetEnumerator();
  12:             endPoints.MoveNext();
  13:             DynamicProxy proxy = factory.CreateProxy(endPoints.Current);
  14:             if (organizationService.IsAuthenticationRequired)
  15:             {
  16:                 proxy.CallMethod(organizationService.MethodName, file.Name, file.Content, organizationService.UserName, organizationService.Password);
  17:             }
  18:             else
  19:             {
  20:                 proxy.CallMethod(organizationService.MethodName, file.Name, file.Content);
  21:             }
  22:         }
  23:     }
  24: }

لاحظ انني هنا استخدام Dynamic Proxy  و هو مكتبة  تقوم بعمل تحليل لخدمة الويب دون الحاجة إلى أضافة إلى المشروع نهائيا, و بالتالي لن تحتاج نهائيا إلى عملية اعادة كتابة المشروع مع كل عميل مختلف.


الخطوة الاخير الأن نأتي لربط بين هذه التصنيفات الثلاثة و هو التصنيف الذي سيكون واجهة المشروع حيث ان التطبيقات التي تستخدم المكتبة لن ترى نهائيا التصنيفات الثلاثة اعلاه كون أن جميعها internal و القصد من ذلك هو تبسيط الفكرة تمام و الاعتماد كليا على ملف الـ app.config , اسميت التصنيف بـ SendFileInFolder , بمجرد إنشاء كائن من هذا التصنيف تقوم الشفرة مباشرة بإنشاء كائن من Folder Watcher و في حالت اطلاق الحدث OnFileCreated اقوم مباشرة ه الطريقة SendFile و التي تستقبل الكائن SendFile المرر من الحدث OnFileCreated , في الطريقة SendFile اقوم بإنشاء كائن جديد من OrganizationService و الذي بدوره سيقوم بقراءة ملف الـ app.config و تحديد بيانات الخدمة, اخيرا اقوم كائن ServiceCaller و استدعي الطريقة PushFile مررا لها كائن المنظمة و الملف كما رئينا سابقا, الشفرة:



   1: namespace FileSenderBase
   2: {
   3:     using GeniusesCode.Helper;
   4:  
   5:     public class SendFileInFolder
   6:     {
   7:         public SendFileInFolder()
   8:         {
   9:             FolderWatcher folderWatcher = new FolderWatcher();
  10:             folderWatcher.OnFileCreated += this.FolderWatcher_OnFileCreated;
  11:         }
  12:  
  13:         public delegate void OnFileStartSend(string status);
  14:  
  15:         public event OnFileStartSend OnFileStartSendEvent;
  16:  
  17:         private void FolderWatcher_OnFileCreated(FileToSend file)
  18:         {
  19:             FireFileStartSendEvent("File: " + file.Name + " with size: " + file.Content.Length + " Start sending");
  20:             this.SendFile(file);
  21:         }
  22:  
  23:         private bool SendFile(FileToSend file)
  24:         {
  25:             try
  26:             {
  27:                 OrganizationService orqanizationService = new OrganizationService();
  28:                 new ServiceCaller().PushFile(orqanizationService, file);
  29:                 file.Delete();
  30:                 FireFileStartSendEvent("File: " + file.Name + " with size: " + file.Content.Length + " was Send Successfully");
  31:                 return true;
  32:             }
  33:             catch (System.Exception ex)
  34:             {
  35:                 FireFileStartSendEvent("Failed sending  " + file.Name + " with size: " + file.Content.Length);
  36:                 file.MoveToFailedFolder();
  37:                 new Logger(ex, false);
  38:                 return false;
  39:             }
  40:         }
  41:  
  42:         private void FireFileStartSendEvent(string status)
  43:         {
  44:             if (this.OnFileStartSendEvent != null)
  45:             {
  46:                 this.OnFileStartSendEvent(status);
  47:             }
  48:         }
  49:     }
  50: }

الأن نأتي للاستخدام الحقيقة , صدق أو لا تصدق غير مهم :- ), المهم أن بثلاثة اسطر أستطيع ارسال الملف الى أي خدمة اريد تقريبا , الشفرة:



   1: static void Main(string[] args)
   2:      {
   3:          SendFileInFolder sendFileInFolder = new SendFileInFolder();
   4:          sendFileInFolder.OnFileStartSendEvent += sendFileInFolder_OnFileStartSendEvent;
   5:          Console.ReadLine();
   6:      }

أعدادات ملف app.confg



   1: <appSettings>
   2:   <add key="FolderToWatch" value="C:\Users\ALGHABBAN\Desktop\Demo"/>
   3:   <add key="MethodName" value="PutData"/>
   4:   <add key="ServiceLink" value="http://localhost:28787/FileReceiver.svc?wsdl"/>
   5:   <add key="FailedFolderName" value="Failed"/>
   6: </appSettings>

يمنك تثبيت الكتبة عن طريقة Nuget :


image


فيديو يشرح طريقة الاستخدام :