مبدأ المسؤولية الفردية: وصفة لقانون عظيم
نشرت: 2022-03-11بغض النظر عما نعتبره رمزًا رائعًا ، فإنه يتطلب دائمًا جودة واحدة بسيطة: يجب أن يكون الرمز قابلاً للصيانة. المسافة البادئة المناسبة ، وأسماء المتغيرات الأنيقة ، وتغطية الاختبار بنسبة 100٪ ، وما إلى ذلك يمكن أن تأخذك فقط حتى الآن. أي كود لا يمكن صيانته ولا يمكن أن يتكيف مع المتطلبات المتغيرة بسهولة نسبية هو رمز فقط ينتظر أن يصبح قديمًا. قد لا نحتاج إلى كتابة رمز رائع عندما نحاول إنشاء نموذج أولي أو إثبات مفهوم أو منتج قابل للتطبيق على الأقل ، ولكن في جميع الحالات الأخرى يجب علينا دائمًا كتابة كود يمكن صيانته. هذا شيء يجب اعتباره جودة أساسية لهندسة وتصميم البرمجيات.
في هذه المقالة ، سأناقش كيف يمكن لمبدأ المسؤولية الفردية وبعض التقنيات التي تدور حوله أن يمنحك هذه الجودة بالذات. تعد كتابة التعليمات البرمجية الرائعة فنًا ، ولكن يمكن أن تساعد بعض المبادئ دائمًا في منح أعمال التطوير الاتجاه الذي تحتاجه للتوجه نحو إنتاج برامج قوية وقابلة للصيانة.
النموذج هو كل شيء
تقريبًا كل كتاب عن بعض أطر MVC (MVP أو MVVM أو M ** الأخرى) مليء بأمثلة على التعليمات البرمجية السيئة. تحاول هذه الأمثلة إظهار ما يجب أن يقدمه إطار العمل. لكنهم ينتهي بهم الأمر أيضًا إلى تقديم نصائح سيئة للمبتدئين. أمثلة مثل "لنفترض أن لدينا ORM X هذا لنماذجنا ، محرك القالب Y لوجهات نظرنا وسيكون لدينا وحدات تحكم لإدارتها بالكامل" لا تحقق شيئًا سوى وحدات التحكم العملاقة.
على الرغم من أنه دفاعًا عن هذه الكتب ، إلا أن الأمثلة تهدف إلى إظهار السهولة التي يمكنك من خلالها البدء في إطار عملهم. ليس المقصود منها تعليم تصميم البرامج. لكن القراء الذين يتابعون هذه الأمثلة يدركون ، بعد سنوات فقط ، كيف يؤدي وجود أجزاء متجانسة من التعليمات البرمجية في مشروعهم إلى نتائج عكسية.
النماذج هي قلب تطبيقك. إذا كانت لديك نماذج منفصلة عن باقي منطق التطبيق الخاص بك ، فستكون الصيانة أسهل بكثير ، بغض النظر عن مدى تعقيد تطبيقك. حتى بالنسبة للتطبيقات المعقدة ، يمكن أن يؤدي التنفيذ الجيد للنموذج إلى كود معبر للغاية. ولتحقيق ذلك ، ابدأ بالتأكد من أن النماذج الخاصة بك لا تفعل إلا ما يفترض أن تفعله ، ولا تشغل نفسها بما يفعله التطبيق المبني حولها. علاوة على ذلك ، لا يتعلق الأمر بطبقة تخزين البيانات الأساسية: هل يعتمد تطبيقك على قاعدة بيانات SQL ، أم أنه يخزن كل شيء في ملفات نصية؟
مع استمرارنا في هذه المقالة ، ستدرك مدى أهمية الكود في فصل الاهتمام.
مبدأ المسؤولية الفردية
ربما تكون قد سمعت عن مبادئ SOLID: المسؤولية الفردية ، والمغلقة المفتوحة ، واستبدال liskov ، وفصل الواجهة وانعكاس التبعية. يمثل الحرف الأول ، S ، مبدأ المسؤولية الفردية (SRP) ولا يمكن المبالغة في أهميته. بل أود أن أزعم أنه شرط ضروري وكافي للتعليمات البرمجية الجيدة. في الواقع ، في أي كود مكتوب بشكل سيئ ، يمكنك دائمًا العثور على فئة لديها أكثر من مسؤولية - form1.cs أو index.php الذي يحتوي على بضعة آلاف من أسطر التعليمات البرمجية ليس شيئًا نادرًا ما يحدث لنا جميعًا ربما رأوها أو فعلوها.
دعنا نلقي نظرة على مثال في C # (ASP.NET MVC و Entity framework). حتى لو لم تكن مطورًا لـ C # ، مع بعض خبرة OOP ، ستتمكن من متابعتها بسهولة.
public class OrderController { ... public ActionResult CreateForm() { /* * View data preparations */ return View(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } using (var context = new DataContext()) { var order = new Order(); // Create order from request context.Orders.Add(order); // Reserve ordered goods …(Huge logic here)... context.SaveChanges(); //Send email with order details for customer } return RedirectToAction("Index"); } ... (many more methods like Create here) }
هذه فئة OrderController معتادة ، تظهر طريقة الإنشاء الخاصة بها. في وحدات التحكم مثل هذه ، غالبًا ما أرى حالات يتم فيها استخدام فئة الترتيب نفسها كمعامل طلب. لكني أفضل استخدام فئات الطلب الخاصة. مرة أخرى ، SRP!
لاحظ في مقتطف الكود أعلاه كيف أن وحدة التحكم تعرف الكثير عن "تقديم طلب" ، بما في ذلك على سبيل المثال لا الحصر تخزين كائن الطلب ، وإرسال رسائل البريد الإلكتروني ، وما إلى ذلك. وهذا ببساطة عدد كبير جدًا من الوظائف لفئة واحدة. لكل تغيير بسيط ، يحتاج المطور إلى تغيير رمز وحدة التحكم بالكامل. وفقط في حالة احتياج وحدة تحكم أخرى إلى إنشاء أوامر ، في كثير من الأحيان ، يلجأ المطورون إلى نسخ الكود ولصقه. يجب أن يتحكم المتحكمون في العملية الكلية فقط ، وليس في الواقع استيعاب كل جزء من منطق العملية.
لكن اليوم هو اليوم الذي نتوقف فيه عن كتابة أدوات التحكم العملاقة هذه!
دعنا أولاً نستخرج كل منطق الأعمال من وحدة التحكم وننقله إلى فئة OrderService:
public class OrderService { public void Create(OrderCreateRequest request) { // all actions for order creating here } } public class OrderController { public OrderController() { this.service = new OrderService(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } this.service.Create(request); return RedirectToAction("Index"); }
بعد القيام بذلك ، لا تقوم وحدة التحكم الآن إلا بما يُفترض القيام به: التحكم في العملية. إنه يعرف فقط طرق العرض وفئات OrderService و OrderRequest - أقل مجموعة من المعلومات المطلوبة لأداء وظيفته ، وهي إدارة الطلبات وإرسال الردود.
بهذه الطريقة نادرًا ما تغير رمز وحدة التحكم. لا يزال من الممكن تغيير المكونات الأخرى مثل طرق العرض وكائنات الطلب والخدمات لأنها مرتبطة بمتطلبات العمل ، ولكن ليس بوحدات التحكم.
هذا ما يدور حوله SRP ، وهناك العديد من التقنيات لكتابة التعليمات البرمجية التي تلبي هذا المبدأ. أحد الأمثلة على ذلك هو حقن التبعية (وهو أمر مفيد أيضًا لكتابة كود قابل للاختبار).
حقن التبعية
من الصعب تخيل مشروع كبير يعتمد على مبدأ المسؤولية الفردية دون حقن التبعية. دعونا نلقي نظرة على فئة OrderService مرة أخرى:
public class OrderService { public void Create(...) { // Creating the order(and let's forget about reserving here, it's not important for following examples) // Sending an email to client with order details var smtp = new SMTP(); // Setting smtp.Host, UserName, Password and other parameters smtp.Send(); } }
هذا الرمز يعمل ، لكنه ليس مثاليًا تمامًا. لفهم كيفية عمل فئة OrderService بطريقة الإنشاء ، يجب عليهم فهم تعقيدات SMTP. ومرة أخرى ، يعد النسخ واللصق هو السبيل الوحيد لتكرار استخدام SMTP أينما دعت الحاجة. ولكن مع القليل من إعادة البناء ، يمكن أن يتغير ذلك:
public class OrderService { private SmtpMailer mailer; public OrderService() { this.mailer = new SmtpMailer(); } public void Create(...) { // Creating the order // Sending an email to client with order details this.mailer.Send(...); } } public class SmtpMailer { public void Send(string to, string subject, string body) { // SMTP stuff will be only here } }
أفضل بكثير بالفعل! ولكن لا يزال فصل OrderService يعرف الكثير عن إرسال البريد الإلكتروني. يحتاج بالضبط إلى فئة SmtpMailer لإرسال البريد الإلكتروني. ماذا لو أردنا تغييره في المستقبل؟ ماذا لو أردنا طباعة محتويات البريد الإلكتروني المرسل إلى ملف سجل خاص بدلاً من إرسالها فعليًا في بيئة التطوير لدينا؟ ماذا لو أردنا اختبار وحدة خدمة OrderService لدينا؟ دعنا نواصل إعادة البناء من خلال إنشاء واجهة IMailer:

public interface IMailer { void Send(string to, string subject, string body); }
سيقوم SmtpMailer بتنفيذ هذه الواجهة. أيضًا ، سيستخدم تطبيقنا حاوية IoC ويمكننا تهيئتها بحيث يتم تنفيذ IMailer بواسطة فئة SmtpMailer. يمكن بعد ذلك تغيير OrderService على النحو التالي:
public sealed class OrderService: IOrderService { private IOrderRepository repository; private IMailer mailer; public OrderService(IOrderRepository repository, IMailer mailer) { this.repository = repository; this.mailer = mailer; } public void Create(...) { var order = new Order(); // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.) this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); } }
الآن نحن نحصل على مكان ما! انتهزت هذه الفرصة لإجراء تغيير آخر أيضًا. تعتمد خدمة OrderService الآن على واجهة IOrderRepository للتفاعل مع المكون الذي يخزن جميع طلباتنا. لم يعد يهتم بكيفية تنفيذ تلك الواجهة وما هي تقنية التخزين التي تعمل عليها. تحتوي فئة OrderService الآن على رمز فقط يتعامل مع منطق عمل الأمر.
بهذه الطريقة ، إذا وجد أحد المختبرين شيئًا ما يتصرف بشكل غير صحيح مع إرسال رسائل البريد الإلكتروني ، يعرف المطور بالضبط المكان الذي يبحث فيه: فئة SmtpMailer. إذا كان هناك خطأ ما في الخصومات ، فإن المطور ، مرة أخرى ، يعرف أين يبحث: OrderService (أو في حالة احتضان SRP عن ظهر قلب ، فقد يكون رمز فئة DiscountService).
العمارة المدفوعة بالحدث
ومع ذلك ، ما زلت لا أحب طريقة OrderService.Create:
public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); }
لا يعد إرسال بريد إلكتروني جزءًا من تدفق إنشاء الطلب الرئيسي. حتى إذا فشل التطبيق في إرسال البريد الإلكتروني ، فلا يزال يتم إنشاء الطلب بشكل صحيح. تخيل أيضًا موقفًا يتعين عليك فيه إضافة خيار جديد في منطقة إعدادات المستخدم يتيح لهم إلغاء الاشتراك من تلقي بريد إلكتروني بعد تقديم الطلب بنجاح. لدمج هذا في فئة OrderService ، سنحتاج إلى تقديم تبعية ، IUserParametersService. أضف الترجمة إلى المزيج ، ولديك تبعية أخرى ، ITranslator (لإنتاج رسائل بريد إلكتروني صحيحة باللغة التي يختارها المستخدم). العديد من هذه الإجراءات غير ضرورية ، لا سيما فكرة إضافة هذه التبعيات العديدة وينتهي بها الأمر مع مُنشئ لا يتناسب مع الشاشة. لقد وجدت مثالًا رائعًا على ذلك في قاعدة بيانات Magento (نظام إدارة محتوى شهير للتجارة الإلكترونية مكتوب بلغة PHP) في فصل يحتوي على 32 تبعيات!
في بعض الأحيان يكون من الصعب معرفة كيفية فصل هذا المنطق ، وربما يكون فصل Magento ضحية لإحدى تلك الحالات. هذا هو سبب إعجابي بالطريقة المبنية على الأحداث:
namespace <base namespace>.Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }
عندما يتم إنشاء طلب ، بدلاً من إرسال بريد إلكتروني مباشرة من فئة OrderService ، يتم إنشاء فئة الحدث الخاص OrderCreated وإنشاء حدث. سيتم تكوين معالجات حدث في مكان ما في التطبيق. سيرسل أحدهم بريدًا إلكترونيًا إلى العميل.
namespace <base namespace>.EventHandlers { public class OrderCreatedEmailSender : IEventHandler<OrderCreated> { public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator) { // this class depend on all stuff which it need to send an email. } public void Handle(OrderCreated event) { this.mailer.Send(...); } } }
تم وضع علامة على فئة OrderCreated على أنها قابلة للتسلسل عن قصد. يمكننا التعامل مع هذا الحدث على الفور ، أو تخزينه بشكل متسلسل في قائمة انتظار (Redis أو ActiveMQ أو أي شيء آخر) ومعالجته في عملية / سلسلة رسائل منفصلة عن تلك التي تتعامل مع طلبات الويب. في هذه المقالة ، يشرح المؤلف بالتفصيل ماهية البنية التي تعتمد على الأحداث (يرجى عدم الالتفات إلى منطق الأعمال داخل OrderController).
قد يجادل البعض بأنه من الصعب الآن فهم ما يحدث عند إنشاء الأمر. لكن هذا لا يمكن أن يكون أبعد عن الحقيقة. إذا كنت تشعر بهذه الطريقة ، فما عليك سوى الاستفادة من وظائف IDE الخاصة بك. من خلال إيجاد جميع استخدامات فئة OrderCreated في IDE ، يمكننا رؤية جميع الإجراءات المرتبطة بالحدث.
ولكن متى يجب علي استخدام حقن التبعية ومتى يجب علي استخدام نهج يحركه الحدث؟ ليس من السهل دائمًا الإجابة على هذا السؤال ، ولكن هناك قاعدة بسيطة واحدة قد تساعدك في استخدام حقن التبعية لجميع أنشطتك الرئيسية داخل التطبيق ، والنهج المستند إلى الأحداث لجميع الإجراءات الثانوية. على سبيل المثال ، استخدم Dependecy Injection مع أشياء مثل إنشاء طلب داخل فئة OrderService باستخدام IOrderRepository ، وتفويض إرسال البريد الإلكتروني ، وهو أمر لا يمثل جزءًا مهمًا من تدفق إنشاء الطلب الرئيسي ، إلى بعض معالج الأحداث.
خاتمة
بدأنا بجهاز تحكم ثقيل للغاية ، فئة واحدة فقط ، وانتهى بنا المطاف بمجموعة متقنة من الفصول. تتضح مزايا هذه التغييرات تمامًا من الأمثلة. ومع ذلك ، لا تزال هناك طرق عديدة لتحسين هذه الأمثلة. على سبيل المثال ، يمكن نقل طريقة OrderService.Create إلى فئة خاصة بها: OrderCreator. نظرًا لأن إنشاء الأمر هو وحدة مستقلة لمنطق الأعمال يتبع مبدأ المسؤولية الفردية ، فمن الطبيعي أن يكون لها فئة خاصة بها مع مجموعة التبعيات الخاصة بها. وبالمثل ، يمكن تنفيذ كل من إزالة الطلب وإلغاء الأمر في فصولهم الخاصة.
عندما كتبت كودًا شديد الاقتران ، شيء مشابه للمثال الأول في هذه المقالة ، فإن أي تغيير بسيط في المتطلبات يمكن أن يؤدي بسهولة إلى العديد من التغييرات في أجزاء أخرى من الكود. يساعد SRP المطورين على كتابة التعليمات البرمجية المنفصلة ، حيث يكون لكل فئة وظيفتها الخاصة. إذا تغيرت مواصفات هذه الوظيفة ، يقوم المطور بإجراء تغييرات على تلك الفئة المحددة فقط. من غير المرجح أن يؤدي التغيير إلى كسر التطبيق بالكامل حيث يجب أن تستمر الفئات الأخرى في أداء وظيفتها كما كان من قبل ، ما لم يتم كسرها بالطبع في المقام الأول.
قد يبدو تطوير الكود مقدمًا باستخدام هذه التقنيات واتباع مبدأ المسؤولية الفردية مهمة شاقة ، لكن الجهود ستؤتي ثمارها بالتأكيد مع نمو المشروع واستمرار التطوير.