شرح مبادئ SOLID - المبدأ الأول Single Responsibility Principle

0

أول مبدأ من مبادئ SOLID هو مبدأ Single Responsibility الذي يقول لنا بأن كل وحدة Module عليها أن تقوم فقط بمسؤولية Responsibilty واحدة لاغير.
ويقول نص المبدأ:

Classes should have a single responsibility and thus only a single reason to change.

نص المبدأ هو نفس الكلام الذي ذكرناه، باستثناء الفقرة الأخيرة التي تبين أن سبب تغير الكلاس ينبغي أن يكون واحدا فقط وهو نفس فكرة المسؤولية الواحدة، حيث الكلاس الذي يقوم بمسؤولية واحدة فقط سيكون هنالك سبب واحد فقط ليتغير.

لماذا نحتاج إلى مبدأ المسؤولية الواحدة SRP
تقسيم فئات المشروع إلى فئات صغيرة، محددة، تقوم بعمل واضح يجعل المشروع سهل القراءة والفهم Understandable، قابل للصيانة في أي وقت Maintainable، وكذلك قابل للاختبار Testable، لأن كل كلاس عبارة عن كتلة واحدة تحتوي على الوحدات Units الخاصة بها والتي يمكننا إجراء اختبارات أحادية Unit Tests عليها.
بالإضافة إلى ما تقدم فإن الفئة التي تقوم بمسؤولية واحدة أكثر موثوقية من الفئة التي تقوم بعدة مهام، ثم لأن فصل المهام Separation of concerns يجعل المشروع قويا والمشاكل قابلة للسيطرة لأنها مرتبطة بوحدات صغيرة يسهل التحكم فيها، ناهيك عن أن تطبيق مبدأ المسؤولية الواحدة يجعل الكلاس أكثر تماسكا More cohesive.

متى نحتاج إلى مبدأ المسؤولية الواحدة SRP
حينما تجد أن كلاس معين يقوم بأكثر من مسؤولية، كأن تجد كلاس يقوم بقراءة البيانات وبحفظها في قاعدة البيانات، وبتسجيل الأنشطة Logging، وبالتحقق من سلامة البيانات Validation، وغيرها من المهام الأخرى، فهذا إنذار لضرورة تقسيم هذا الكلاس إلى كلاسات صغيرة مركزة ومتماسكة Focused and Cohesive classes.

كيف نطبق مبدأ المسؤولية الواحدة SRP
في مشروعنا الذي يقوم بالتحويل من النوع العشري إلى قواعد رقمية أخرى، نجد الكلاس الرئيسي التالي:

NumberConverter.cs:

namespace SolidSample
{
    public enum BaseType
    {
        Binary = 2,
        Octal = 8,
        None = 0
    }

    public class NumberConverter
    {
        public int DecimalNumber { get; set; }

        public void Convert()
        {
            Console.WriteLine("Program is starting...");

            Console.WriteLine("Enter the number to convert:");
            DecimalNumber = int.Parse(Console.ReadLine());

            Console.WriteLine("Enter the base type (Ex: 2,8):");
            var baseType = (BaseType)int.Parse(Console.ReadLine());

            string result = String.Empty;

            switch (baseType)
            {
                case BaseType.Binary:
                    result = System.Convert.ToString(DecimalNumber, 2);
                    break;

                case BaseType.Octal:
                    result = System.Convert.ToString(DecimalNumber, 8);
                    break;

                default:
                    result = "No base found!";
                    break;
            }

            Console.WriteLine($"The result is: {result} ");

            Console.WriteLine("Program is ending..");

        }

    }
}

في الأول أنشأنا enum تحتوي على أنواع الأنظمة التي نريد التحويل إليها، ومبدئيا عندنا النوع الثنائي Binary، والنوع الثماني Octal، والقيمة الثالثة في حال لم يقم المستخدم باختيار أحد النوعين.
بعد ذلك عندنا الكلاس الرئيسي NumberConverter الذي يحتوي على الخصائص التالية:
DecimalNumber: وهو القيمة الرقمية الأساسية التي سنعمل على تحويلها
Base: وهو نوع النظام الرقمي الذي نريد التحويل إليه
Result: وهي النتيجة التي نريد طباعتها بعد القيام بعملية التحويل

ثم انتقلنا إلى الوظيفة الجوهرية Convert التي تقوم بطلب إدخال القيمة الرقمية من المستخدم، وكذلك رمز النظام المراد التحويل إليه إما الثنائي أو الثماني، ثم بعد ذلك تقوم بفرز الحالات حسب نوع النظام الذي اختاره المستخدم وتجري عملية التحويل، ثم تطبع النتيجة في شاشة الكونسول.
لتجربة الوظيفة Convert، يمكننا أن نأتي إلى الكلاس الرئيسي Program.cs ونكتب ما يلي:

Program.cs:

    class Program
    {
        static void Main(string[] args)
        {
            NumberConverter converter = new NumberConverter();

            converter.Convert();
        }
    }

عند التنفيذ سيكون تسلسل البرنامج كما يلي:
 

من الجانب العملي، فالبرنامج يشتغل على الشكل المطلوب، لكن على مستوى التصميم Design فقد تكون لاحظت معي أن الكلاس NumberConverter يقوم بعدة مسؤوليات كالتالي:
يقوم بمسؤولية عرض الرسائل في شاشة الكونسول
يقوم بمسؤولية قراءة البيانات من شاشة الكونسول
يقوم بتسجيل الأنشطة Logging
يقوم بعملية التحويل وهي مسؤوليته الأساسية
لذلك علينا أن نكلفه فقط بمسؤولية التحويل وأن نضع المسؤوليات الأخرى في فئات مستقلة لنكون بذلك قد طبقنا مبدأ المسؤولية الواحدة Single Responsibility Principle بنجاح.
سنقوم بإنشاء كلاس نسميها Logger دورها هو طباعة الرسائل في شاشة الكونسول، وقد نتساهل حاليا ونترك لها أيضا مسؤولية تسجيل الأنشطة Logging لسبب بسيط هو أن المسؤوليتين تشتركان في نفس العمل وهو طباعة الرسائل على شاشة الكونسول وبالتالي وفي إطار تنفيذ مبدأ DRY الذي يختصر Don’t Repeat Yourself والذي ينص على أهمية تفادي تكرار الكود الذي يؤدي نفس الغرض، سنسمح للكلاس بالقيام بالمسؤوليتين معا.
الكلاس التي ستقوم بطباعة النصوص وتسجيل الأنشطة كما يلي:

Logger.cs:

    public class Logger
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

والكلاس الثاني سنكلفه بقراءة البيانات من شاشة الكونسول ومحتواه كما يلي:

Reader.cs:

    public class Reader
    {
        public int ReadInteger()
        {
            return int.Parse(Console.ReadLine());
        }
    }


الآن سيصبح شكل الكلاس NumberConverter كالآتي:

NumberConverter.cs:

    public class NumberConverter
    {
        public int DecimalNumber { get; set; }

        public Logger Logger { get; set; } = new Logger();
        public Reader Reader { get; set; } = new Reader();

        public void Convert()
        {
            Logger.Log("Program is starting...");

            Logger.Log("Enter the number to convert:");
            DecimalNumber = Reader.ReadInteger();

            Logger.Log("Enter the base type (Ex: 2,8):");
            var baseType = (BaseType)Reader.ReadInteger();

            string result = String.Empty;

            switch (baseType)
            {
                case BaseType.Binary:
                    result = System.Convert.ToString(DecimalNumber, 2);
                    break;

                case BaseType.Octal:
                    result = System.Convert.ToString(DecimalNumber, 8);
                    break;

                default:
                    result = "No base found!";
                    break;
            }

            Logger.Log(result);

            Logger.Log("Program is ending..");

        }
    }

الآن من الناحية المنهجية صرنا أكثر انتظاما، والكود الذي قمنا بكتابته سهل القراءة، مقسم حسب المهام، حيث كل كلاس تؤدي مسؤوليتها الخاصة بها، وبالتالي قمنا بتطبيق مبدأ المسؤولية الواحدة بنجاح.

خلاصة مبدأ المسؤولية الواحدة SRP
على الكلاس الواحد أن يقوم بمسؤولية واحدة فقط لاغير.

لا يوجد تعليقات

أضف تعليق