شرح مبادئ SOLID - المبدأ الخامس والأخير Dependency Inversion Principle

0

حتى أختصر عليك فصولا طوالا ومحاضرات كثيرة سأقول لك: هذا المبدأ يهدف بالأساس إلى إضعاف ارتباط الكلاسات مع بعضها البعض، بحيث إذا قررنا في المستقبل أن نستبدل كلاس بكلاس أو أن نعدل على كلاس فلن تتأثر الكلاسات الأخرى المرتبطة به.
مبدأ DIP مبدأ كثير التردد نظرا لارتباطه بنموذج Depenedency Injection Pattern والذي بدوره يعتبر حلا من الحلول المتاحة لتفعيل مبدأ DIP.
لننطلق من الصفر مع المبدأ، سنبدأ بنصه الأصلي الذي يقول:

High-level modules should not depend on low-level modules, both should depend on abstractions. Abstractions should not depend on details, details should depend on abstractions. 


لنفهم هذين البندين بالتفصيل سنبدأ بتحرير المصطلحات، ماهو المقصود بالوحدات العليا High-level modules والوحدات الدنيا Low-level modules؟ وماهو المقصود ب abstraction و details؟ علما أن المفهومين الأخيرين سبق وأن شرحناهما في بداية الكتاب ويمكنك العودة إليهما.
حينما تكون عندنا وحدتان مرتبطتان مع بعضهما البعض، فالواحدة التي تحتاج إلى الأخرى هي الوحدة العليا High-level module، والوحدة الأخرى هي Low-level module.
يعني لو عندنا الفئتين التاليتين:

ClassA.cs:

    public class ClassA
    {
        public void MethodA()
        {
            // Do something
        }
    }



ClassB.cs:

    public class ClassB
    {
        ClassA a;

        public void MethodB()
        {
            a = new ClassA();
            a.MethodA();
        }
    }

لاحظ أن الكلاس ClassB تحتاج إلى object من الكلاس ClassA أي أن هنالك ترابط بينهما.
الكلاس ClassB هي التي تحتاج إلى الارتباط، إذن فهي وحدة عليا High-level module.
الكلاس ClassA لا تحتاج إلى أي ارتباط مع الكلاس ClassB، إذن فالكلاس ClassB هي الوحدة الدنيا Low-level module.
النص الأول لمبدأ DIP يقول بأن الوحدات العليا لا ينبغي أن تكون مرتبطة بالوحدات الدنيا، بل كلتاهما ينبغي أن ترتبطا بالتجريد Abstraction، وذكرنا في أول الكتاب أن التجريد نقصد به الفئات المجردة Abstract classes والواجهات Interfaces.
وسنرى كيف نقوم بذلك بالتفصيل إن شاء الله.
النص الثاني لمبدأ DIP يقول بأن التجريد لا ينبغي أن يهتم بالتفاصيل، بل هذه الأخيرة من ينبغي أن تكون مرتبطة بالتجريد، بمعنى أن الواجهات أو الفئات المجردة لا ينبغي أن تكون مرتبطة بكلاسات واقعية Concrete classes، بل العكس هو الذي ينبغي أن يكون.

لماذا نحتاج إلى مبدأ انعكاس التبعية DIP
نحتاج إلى مبدأ DIP من أجل إضعاف الارتباطات بين الكلاسات فيما يعرف ب Loose coupling، لأن الارتباطات القوية بين الكلاسات تؤثر سلبا على مستوى الاختبار خاصة الاختبارات الأحادية Unit Tests، وعلى مستوى الصيانة Maintainability، حيث يصعب صيانة الكود الذي يتوفر على عدة ارتباطات، ناهيك عن مشاكل صعوبة فهم الكود Understandability ومشاكل توسيع الكود وإضافة خصائص جديدة إليه Extensibility، فلكي نتمكن من الاستفادة من كل هذه المزايا علينا أن نجعل كودنا موافقا لمبدأ Dependency Inversion.

متى نحتاج إلى مبدأ انعكاس التبعية DIP
حينما تجد في مشروعك كلاسات مرتبطة مع بعضها البعض بعيدا عن آليات التجريد abstraction كالواجهات والفئات المجردة.
حينما تجد أن الكلاسات التي تكتبها تأثرت بالارتباطات dependencies لدرجة أنك لم تعد قادرا على كتابة اختبارات أحادية تفي بالغرض.
حينما تجد أن مشروعك مرتبط أكثر بالتفاصيل details بحيث لو فكرت في المستقبل أن تستعمل أنواعا جديدة وجدت الأمر مرهقا جدا.
باختصار، إذا كنت تنوي أن تبني مشروعا قويا متماسكا تشتغل عليه على المدى الطويل أو القصير دون أن تجده يضع أمامك العراقيل، فقم بتصميم مشروعك وفق مبادئ SOLID وعلى رأسها مبدأ Dependency Inversion Principle.

كيف نطبق مبدأ انعكاس التبعية DIP
لنفهم هذا المبدأ بشكل جيد، سنعود إلى مشروعنا، وتحديدا إلى الكلاس الأساسي NumberConverter وشاهد أعلى الكلاس ستجد أننا صرحنا عن كائنين من النوع Logger و Reader كما يلي:
NumberConverter.cs:

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

طبعا استنسخنا هذين النوعين لنتمكن من استعمالهما في الكلاس الحالي، لكن هنالك مشكلة هنا.
الكلاس الحالي NumberConverter أصبح مرتبطا بشكل كبير Tightly coupled بالفئتين Logger و Reader، مما يعني أن عندنا مشاكل في التصميم وأننا انتهكنا مبدأ DIP الذي يقضي بأن الوحدات العليا وفي حالتنا هذه هي الكلاس NumberConverter لا ينبغي أن تكون مرتبطة بالوحدات الدنيا وفي حالتنا هذه هما Logger و Reader، هذا من جهة ومن جهة أخرى ينص نفس المبدأ على أن كل من الطرفين ينبغي أن يكون مرتبطا بالتجريد Abstraction مثل Interfaces و Abstract classes.
لذلك سنقوم بإنشاء الواجهات اللازمة لنرتبط بها بدل أن نرتبط بالأنواع الواقعية، والبداية مع الواجهة ILogger وهذا محتواها:

ILogger.cs:

    public interface ILogger
    {
        void Log(string message);
    }


الآن سنعود إلى الكلاس Logger ونقوم بتطبيق الواجهة أعلاه عليها:

Logger.cs:

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

نفس الكلام مع الكلاس Reader سننشئ لها واجهة نسميها IReader ونقوم بتطبيقها كما يلي:

IReader.cs:

    public interface IReader
    {
        int ReadInteger();
}

Reader.cs:

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

جميل جدا، هكذا نكون قد ربطنا الوحدات الدنيا Low-level modules مع Abstraction، بقي أن نقوم بربط الوحدات العليا High-level module في حالتنا هذه NumberConverter مع Abstraction أيضا.
ثم سنقوم بتمرير الأنواع اللازمة لهذا التجريد عبر مشيد الفئة Constructor ليتم حقنها في الكلاس، وهذا الحقن هو ما يسمى ب Dependency Injection حيث نقوم بحقن الأنواع الواقعية لتعمل مكان الأنواع المجردة في كلاس معين.
لندخل الآن إلى الكلاس NumberConverter ونستبدل التصريحين التاليين:

NumberConverter.cs:

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

ونضع مكانهما التصريحين الجديدين اللذان يجعلان الكلاس مرتبطا بالتجريد:

NumberConverter.cs:

        public ILogger Logger { get; set; }
        public IReader Reader { get; set; }

ثم بعد ذلك سنقوم بتمرير قيم هذين الكائنين عبر مشيد الكلاس كما يلي:

NumberConverter.cs:

        public NumberConverter(ILogger logger, IReader reader)
        {
            Logger = logger;
            Reader = reader;
        }

بهذه الكيفية نكون قد قطعنا أشواطا مهمة في تفعيل مبدأ Dependency Inversion Principle عبر تطبيق نموذج Dependency Injection Pattern، وأرجو أن تكون قد أدركت الفرق بينهما.
الآن لو أتينا لتجربة الكلاس NumberConverter فنحن مطالبون بتقديم الأنواع الفعلية Concrete types ليتم حقنها على شكل ارتباطات عبر مشيد الكلاس كما يلي:

Program.cs:

        var converter =  new NumberConverter(new Logger(), new Reader());

الجميل في العملية، أننا لو أردنا أن نستبدل الكلاس Reader بكلاس أخرى تقرأ البيانات من قاعدة بيانات أو من Web Service أو من جهاز قارئ الباركود أو غيره، فإننا لن نغير الكود الخاص بالكلاس NumberConverter وإنما سنقوم بإنشاء نوع جديد يطبق الواجهة IReader ثم نقوم باستعماله مكان القارئ الأصلي هكذا:

Program.cs:

        var converter =  new NumberConverter(new Logger(), new BarcodeReader());

وهذه إحدى مميزات مبدأ DIP.
وتعالوا بنا نستعرضها باستفاضة، وهذه المرة سنقوم بإنشاء كلاس جديد يطبق الواجهة ILogger ودوره هو تخزين الأنشطة التي تحدث على مستوى التطبيق في ملف نصي Text File، لذلك دعنا نسمي الكلاس الجديد TextFileLogger، وهذا هو محتواه:


TextFileLogger.cs:

    public class TextFileLogger : ILogger
    {
        public void Log(string message)
        {
            using (StreamWriter writer = File.AppendText("logFile.txt"))
            {
                writer.WriteLine(message);
            }
        }
    }

الكلاس باختصار يقوم بتخزين القيمة النصية message في ملف نصي باسم logFile.txt من خلال استعمال StreamWriter الذي يسمح بإنشاء الملفات والكتابة فيها.
الآن سنأتي إلى مكان إنشاء كائن من الكلاس NumberConverter ونستبدل Logger ب TextFileLogger كما يلي:

Program.cs:

var converter =  new NumberConverter(new TextFileLogger(), new Reader());

عند تنفيذ البرنامج سيتم إنشاء ملف logFile.txt وسيتم تسجيل بعض الأنشطة فقط وهذا هو محتواه:
لعلك ستلاحظ معي أنه تم تسجيل رسالتين فقط، وذلك بسبب أن Logger نستعمله كمسجل للأنشطة وفي نفس الوقت من أجل عرض المعلومات على شاشة الكونسول مما أحدث خللا ببرنامجنا.
لذلك سنقوم بإنشاء نسخة جديدة من Logger خاصة بعملية عرض الرسائل على Console، لعمل ذلك تعالوا بنا إلى الكلاس NumberConverter وفي مكان التصريح عن الكائنات سنكتب ما يلي:

NumberConverter.cs:

        public ILogger Logger { get; set; }
        public IReader Reader { get; set; }
        public ILogger Writer { get; set; }

ثم نقوم بتمرير قيمة الكائن عبر المشيد:

NumberConverter.cs:

        public NumberConverter(ILogger logger, IReader reader, ILogger writer)
        {
            Logger = logger;
            Reader = reader;
            Writer = writer;
        }

ثم نقوم بتعديل محتوى الوظيفة Convert لتصبح كما يلي:

NumberConverter.cs:

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

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

            Logger.Log($"The number to convert is: {DecimalNumber}");

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

            Logger.Log($"The selected base type is: {baseType.ToString() }");

            var type = ConverterFactory.Create(baseType, DecimalNumber);

            Logger.Log($"using the converter: {type.GetType() }");

            string result = type.Convert();

            Writer.Log(result);

            Logger.Log($"The result is: {result} ");

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

        }

الآن صار عندنا نسختين من نفس النوع Logger، النسخة الأولى اسمها Logger ودورها عمل Logging من أجل تسجيل الأنشطة في ملف نصي، والنسخة الثانية اسمها Writer ودورها طباعة المعلومات على شاشة الكونسول.
الآن كود استدعاء كائن من النوع NumberConverter سيكون كما يلي:

Program.cs:

            var converter = new NumberConverter(new TextFileLogger(),
                                                new Reader(),
                                                new Logger());

عند التنفيذ سيتم تشغيل البرنامج كما يلي:
وبالمقابل سيتم تخزين الأنشطة في ملف نصي باسم logFile مخزن في مجلد bin/Debug/netcoreapp، وهذا هو المحتوى المخزن:
إلى هنا نكون قد انتهينا من شرح مبادئ SOLID.
بقي تعديل طفيف سنقوم به إن شاء الله قبل ختم هذا الكتاب، وهو الكلاس Reader الذي لو عدنا إليه سنجد محتواه كالآتي:

Reader.cs:

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

الوظيفة ReadInteger وظيفة ساذجة، لأن المسكينة تعتقد أن المستخدم سيدخل حتما قيمة رقمية، لكن ماذا لو خذلها وقام بإدخال قيمة نصية مثلا، الجواب ما ترى أسفله لا ما تسمع:
حصل استثناء من نوع FormatException بسبب أننا أدخلنا قيمة نصية بدل قيمة رقمية فتوقف البرنامج، وهذه نصيحة عابرة: لا تثق أبدأ بالمدخلات مهما كان مصدرها، عليك دائما ب Validation.
الآن سنعدل على الكلاس Reader ليمكننا تفادي مثل هذه المشاكل، وهذا هو محتواها الجديد:

Reader.cs:

    public class Reader : IReader
    {
        public Reader(ILogger logger)
        {
            Logger = logger;
        }

        public ILogger Logger { get; }

        public int ReadInteger()
        {
            try
            {
                string value = Console.ReadLine();

                return int.Parse(value);
            }
            catch (Exception)
            {
                Logger.Log("The entered value is invalid.");

                return 0;
            }

        }
    }
الآن صار الكلاس يقتنص الأخطاء وفي حال حصولها سيعرض الرسالة:
The entered value is invalid.
ستلاحظ أن الكلاس Reader أصبح يحتاج إلى النوع ILogger الذي استعملناه من أجل عرض رسالة الخطأ أعلاه في شاشة الكونسول.
الآن لكي يعمل التطبيق بنجاح، سنعود إلى الكلاس Program.cs ونقوم بتغيير صيغة إنشاء object من الكلاس NumberConverter كما يلي:

Program.cs:

            var consoleLogger = new Logger();

            var converter = new NumberConverter(new TextFileLogger(),
                                                new Reader(consoleLogger),
                                                consoleLogger);

استعملنا نفس Logger في الكلاس NumberConverter و Reader لأننا نطبع الرسائل في شاشة كونسول واحدة.

خلاصة مبدأ انعكاس التبعية DIP
مبدأ انعكاس التبعية Dependency Inversion Principle يهدف بالأساس إلى تمكيننا من بناء مشاريع قوية قابلة للصيانة Maintainable، قابلة للاختبار Testable، قابلة للتوسيع Extensible، سهلة الفهم عند قراءتها Understandable، وهو مبدأ مهم جدا من مبادئ التصميم والذي يقضي باعتماد آلية التجريد Abstraction بدل التعامل المباشر مع تفاصيل الأنواع Details وذلك بغرض إضعاف الارتباط Loose coupling.




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

أضف تعليق