شرح مبادئ SOLID - المبدأ الثاني Open-Closed Principle

0

ينص هذا المبدأ على أن الوحدات Modules مثل الكلاسات ينبغي أن تكون مفتوحة على التوسيع Open for extension ومغلقة في وجه التعديل Closed for modification.
هذا الكلام باختصار يعني لو عندك كلاس ستحتاج إلى إضافة بعض الأمور إليها، فإنه من الجيد أن تستطيع القيام بعملية الإضافة عبر توسيع الفئة من خلال آلية الوراثة Inheritance مثلا، وليس عبر التعديل المباشر في السورس كود.
لنفترض في مشروعنا أن العميل طلب منا أن نضيف في البرنامج خاصية التحويل إلى النوع الست عشري Hexadecimal، في الحالة العادية سندخل مباشرة إلى الوظيفة Convert ونضيف case جديدة إلى switch statement وكذلك إضافة قيمة جديدة إلى enum الخاصة بالأنواع كما يلي:
    public enum BaseType
    {
        Binary = 2,
        Octal = 8,
        Hexadecimal = 16,
        None = 0
    }

وشكل الوظيفة Convert كالآتي (سنكتب الجزء الذي يهمنا فقط):

NumberConverter.cs:

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

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

                case BaseType.Hexadecimal:
                    result = DecimalNumber.ToString("X");
                    break;

                default:
                    result = "No base found!";
                    break;
            }
الآن قد يبدو أننا جبنا الذيب من ذيلو كما يقول إخوتنا في مصر، لكن في الحقيقة نحن وضعنا حلا ترقيعيا على الرغم من أنه يؤدي المطلوب، لسبب بسيط هو أننا خرقنا مبدأ Open-Closed Principle وقمنا بتعديل Behavior الخاص بالوظيفة Convert دون اللجوء إلى عملية التوسيع Extension وإنما من خلال التعديل المباشر.
وقد تسألني في انتشاء: ومالو يا عم؟ ماهو شغال كده زي الفل!
وطبعا سؤالك بريء لأنه يليق بالوضعية البرمجية التي نشتغل عليها والتي تتصف بالبساطة، لكن في المشاريع الكبرى التي ستكون من العاملين فيها إن شاء الله، الأمور لا تمشي بهذا الشكل، لأنك قد تقوم ببناء API وترسلها إلى عميل لا يعرف عنها سوى Contract، أو ترسلها له مجمعة على شكل DLL Assembly، وبالتالي لا يستطيع أن يعدل على الكود بشكل مباشر، فأنت حينها مطالب بشدة أن تجعل فئاتك قابلة للتوسيع لأن التعديل المباشر ليس متاحا في كل الأوقات، أضف إلى ذلك خطورة هذه الممارسة التي قد تؤثر سلبا على الكود الذي تم اختباره فتؤدي عملية تعديل سلوك الوظيفة إلى إفشال Unit Tests الخاصة بها.
هذا مجرد وجه من عدة وجوه لإبراز أهمية استعمال هذا المبدأ في تصميمك البرمجي، لذلك وجب أن تأخذه بعين الاعتبار وبجدية إن كنت تنوي أن ترقى بمسارك المهني وتتعامل مع عملاء من طينة الكبار (حلو التعبير ده).

لماذا نحتاج إلى مبدأ الفتح والإغلاق OCP
نحتاج إلى تطبيق هذا المبدأ من أجل جعل وحداتنا البرمجية More extensible and maintainable، لأن فتحها على التوسيع يسمح بالإضافة إليها دون التأثير على ماهو موجود فيها، على عكس التعديل المباشر الذي قد يضر بأجزاء كانت شغالة، وقد يؤدي إلى تعطيل الاختبارات الأحادية الخاصة بالوظيفة المعدلة.

متى نحتاج إلى مبدأ الفتح والإغلاق OCP
في الحقيقة لست ملزما بأن تحمل هم هذا المبدأ في كل كلاس تقوم بكتابتها، وإنما استحضره في الحالات التي ترى أن الكلاس الذي ستقوم بكتابته من الممكن أن يتطلب تعديلات مستقبلية.

كيف نطبق مبدأ الفتح والإغلاق OCP
الآن دعنا نعود إلى مشروعنا، ونحاول أن نطبق Open Closed Principle لنفهمه بوضوح، قبل ذلك حاول أن تفكر معي في حل محتمل يناسب وضعيتنا الحالية.
اقتراحي الذي أتقدم به وأرجو أن يعجبك هو أن ننشئ كلاس مجرد abstract class نسميه Converter أو واجهة نسميها IConverter ونقوم بوضع العناصر التي من الممكن أن نعيد تعريفها فيها، ثم نرث منها الكلاسات التي سنحتاجها مثل BinaryConverter و OctalConverter وكذلك النوع الجديد الذي نريد إضافته وهو HexadecimalConverter، فإذا احتجنا أن نعدل على سلوك الوظيفة Convert في المستقبل نأتي بكل بساطة ونرث من النوع Converter ونضع Behavior الذي يناسبنا.
إذن بتطبيق ما ذكرنا، فإن الكلاس المجرد Converter قد يكون كما يلي:

Converter.cs:

    public abstract class Converter
    {
        public int DecimalNumber { get; set; }

        public Converter(int decimalNumber)
        {
            DecimalNumber = decimalNumber;
        }

        public abstract string Convert();
    }

الكلاس بكل بساطة يقوم باستقبال قيمة DecimalNumber عبر المشيد ويسندها إلى property الخاصة به، ثم يعرف وظيفة مجردة اسمها Convert.
الآن شكل الكلاسات التي سترث من هذا الكلاس المجرد ستكون كما يلي:

BinaryConverter.cs:

    public class BinaryConverter : Converter
    {
        public BinaryConverter(int decimalNumber)
            : base(decimalNumber)
        {

        }
        public override string Convert()
        {
            return $"The result is: {System.Convert.ToString(DecimalNumber, 2)}";
        }
    }








OctalConverter.cs:

    public class OctalConverter : Converter
    {
        public OctalConverter(int decimalNumber)
    : base(decimalNumber)
        {

        }

        public override string Convert()
        {
            return $"The result is: {System.Convert.ToString(DecimalNumber, 8)}";
        }
    }

والآن نستطيع أن نضيف كلاس جديد للتحويل إلى النوع الست عشري دون الحاجة إلى تغيير سلوك الوظيفة Convert إذ يكفي أن نعمل Extend للكلاس الرئيسي Converter كما يلي:

HexadecimalConverter.cs:

    public class HexadecimalConverter : Converter
    {
        public HexadecimalConverter(int decimalNumber) : base(decimalNumber)
        {
        }

        public override string Convert()
        {
            return $"The result is: {DecimalNumber.ToString("X")}";
        }
    }

بالنسبة لشكل الوظيفة Convert في الكلاس NumberConverter سيصبح بالشكل التالي:

NumberConverter.cs:

        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, 16):");
            var baseType = (BaseType)Reader.ReadInteger();

            string result = String.Empty;

            switch (baseType)
            {
                case BaseType.Binary:
                    var binaryConverter = new BinaryConverter(DecimalNumber);
                    result = binaryConverter.Convert();
                    break;

                case BaseType.Octal:
                    var octalConverter = new OctalConverter(DecimalNumber);
                    result = octalConverter.Convert();
                    break;

                case BaseType.Hexadecimal:
                    var hexadecimalConverter = new HexadecimalConverter(DecimalNumber);
                    result = hexadecimalConverter.Convert();

                    break;

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

            Logger.Log(result);

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

        }

أعتقد صار جليا عندك أهمية مبدأ الفتح والإغلاق بعد أن قدمنا لهذا الفصل بشرح نظري مركز وأردفناه بمثال عملي موجز يبين أهمية المبدأ وكيفية استعماله.

خلاصة مبدأ الفتح والإغلاق OCP
يشير إلينا مبدأ الفتح والإغلاق Open Closed Principle بضرورة الابتعاد عن التعديل المباشر لسلوك الوظائف Methods behavior لما لذلك من أثر سلبي تقدم بيانه، وإنما الواجب أن نعطي للكلاسات القدرة على أن تصبح Extensible، بحيث عند الحاجة إلى التعديل نلجأ إلى عملية إنشاء أنواع جديدة وليس إلى تغيير السورس كود الخاص بالنوع.

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

أضف تعليق