الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

مبدأ الانفتاح والانغلاق (ocp)

بدأه HassanAlattas في 14 أغسطس 2009 · 0 رد · 1,385 مشاهدة · في Microsoft Visual C#.NET
مشاركة: واتساب X فيسبوك تيليجرام
#1

مبدأ الانفتاح والانغلاق

The Open-Closed Principle - OCP

مكونات البرنامج (الفئات ،الوحدات، الدوال ...الخ)

يجب ان تكون منفتحة على التوسيع ومنغلقة على التعديل

(Bertrand Meyer)

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

وصف مبدأ OCP

لأي وحدة (module) تتفق مع مبدأ OCP خاصيتان:

- مفتوحة على التوسيع: فكلما تغيرت المتطلبات ، كان بإمكاننا ان نوسع الوحدة بالسلوكيات الجديدة التي تلبي تلك المتطلبات.

- منغلقة على التعديل: فتوسيع سلوك الوحدة لن يؤثر على الكود المصدري او الملف الثنائي الخاص بها.

قد يبدو ان الخاصيتين تناقض احداها الأخرى ، فالطريقة الطبيعية لتوسيع الوحدة هي عن طريق تغيير الكود الخاص بها، كذلك فإننا نعتقد ان الوحدة التي لا تتغير، لن يتغير سلوكها ايضاً.

إذن كيف يكون بالإمكان تعديل سلوك الوحدات بدون تعديل الكود المصدر لها؟ وبدون تعديل الوحدة كيف يمكننا تعديل ما تقوم به؟

الجواب يكمن في التجريد، ففي أي لغة كائنية التوجه مثل C# اوغيرها ، يكون بالإمكان تعريف تجريدات ثابتة تمثل مجموعة من السلوكيات التي لا تزال غير محددة حتى الآن ، وهذه التجريدات عبارة عن فئات مجردة (abstract classes) ، يمكن أشتقاق فئات فرعية منها لتمثيل تلك السلوكيات.كما يمكننا تعريف الوحدات التي تقوم بمعالجة تلك التجريدات ، ومثل هذه الوحدات يمكن ان تكون منغلقة على التعديل حيث انها تعتمد على تجريدات ثابتة لا تتغير (او نادرا ما تتغير)، كذلك فإن سلوكها قابل للتمدد عن طريق الاشتقاق من تلك التجريدات.

الشكل التالي يعرض تصميم بسيط لا يتفق مع مبدأ OCP . حيث ان كلا الفئتين الزبون (Client) و الخادم (Server) هي فئات مادية (concrete= قابلة للتمثيل). وبما ان فئة الزبون تستخدم فئة الخادم مباشرة ، فإن علينا ان نقوم بالتعديل فيها في كل مرة نريد لها ان تستخدم خادم آخر، وهذا يخالف مبدأ OCP.

post-173477-1250266470_thumb.gif

ويبين الشكل التالي التصميم المكافئ الذي يتفق مع مبدأ OCP باستخدام "اسلوب التصميم: الإستراتيجية" (Strategy pattern).

حيث قمنا بتعريف الواجهة المجردة ClientInterface وجعلنا فئة الزبون تتعامل مع ذلك التجريد. والنتيجة انه يمكن للزبون ان يتعامل مع كائنات فئة الخادم او اي فئة أخرى مشتقة من ClientInterface، بدون الحاجة إلى تعديل فئة الزبون.

post-173477-1250266575_thumb.gif

يعرض الشكل التالي خيارا آخر بأسلوب الدالة القالب (Template method pattern) ، ففئة البوليصة لديها بعض الدوال المادية (concrete functions) والعامة التي تمثل البوليصة بنفس الطريقة مع فئة الزبون ، وبنفس الطريقة ايضاً، فإن البوليصة تعرف بعض الوظائف التي تحتاج إليها على شكل واجهة مجردة (لاحظ ServiceFunction). لكن هذه المرة فإن الواجهة المجردة هي جزء من فئة البوليصة نفسها وتكون في لغة مثل C# على شكل دوال مجردة (abstract methods) يتم تحقيقها بواسطة الفئات الفرعية ، لهذا فإن السلوك المحدد داخل البوليصة يمكن ان يتمدد او يعدل من خلال هذه الفئات الفرعية.

post-173477-1250266676_thumb.gif

هذين الأسلوبين هما أشهر طريقتين لإرضاء مبدأ OCP ، حيث يمثل كل اسلوب طريقة واضحة لفصل الدوال عن تفاصيل وأسلوب تحقيقها.

برنامج الأشكال

تخيل انه مطلوب منا عمل برنامج يكون قادرا على رسم الدوائر والمربعات في بيئة رسوميات معينة. الدوائر والمربعات يجب ان ترسم وفق ترتيب معين. بحيث يتم إنشاء قائمة من الدوائر والمربعات وفق اي ترتيب معين ، وعلى البرنامج ان يقوم بالمرور على القائمة ويرسم كل دائرة او مربع بداخلها.

حل يخرق مبدأ OCP:

يعرض الكود التالي حل لمثل هذه المشكلة ، حيث قمنا بتعريف سجل لتمثيل الدائرة وسجل آخر لتمثيل المربع ، ايضاً قمنا بعمل الدالة DrawAllShapes التي تقوم بالمرور على قائمة من الأشكال وتفحص نوع كل عنصر فيها ثم تقوم باستدعاء دالة الرسم المناسبة (DrawSquare او DrawCircle).

public struct Circle
{
	public double itsRadius;
	public Point itsCenter;
}

public struct Square
{
	public double itsSide;
	public Point itsTopLeft;
}

public void DrawAllShapes(ArrayList list)
{
	foreach (object s in list)
	{
		if (s is Square)
		{
			DrawSquare((Square)s);
		}
		else if (s is Circle)
		{
			DrawCircle((Circle)s);
		}
	}
}
public void DrawCircle(Circle c) { }
public void DrawSquare(Square s) { }

لاحظ ان الدالة DrawAllShapes لا تتفق مع مبدأ OCP لأنها غير منغلقة على الأشكال الجديدة التي يمكن إضافتها لاحقاً، فعلى سبيل المثال إذا أردنا لها ان تكون قادرة على رسم قائمة من الأشكال التي تتضمن المثلث ، فالسبيل الوحيد لتحقيق ذلك هو عن طريق التعديل فيها. ونفس الشئ سوف يتكرر كلما أردنا ان نقوم بإضافة شكل جديد آخر إلى التطبيق.

بالتأكيد، هذا البرنامج ما هو إلا مثال بسيط ، بينما البرامج الحقيقية ستحوي الكثير من الدوال التي تقوم كل منها بوظيفة معينة متعلقة بالشكل مثل دوال التحريك ، التكبير ، الحذف ، النسخ ، اللصق...الخ ، وسوف نجد ان عبارة if/else التي استخدمناها في DrawAllShapes قد كتبت بطريقة مشابهه في كثير من تلك الدوال.

هذا يعني ان إضافة شكل جديد يتطلب منا ان نبحث عن تلك الأماكن وان نقوم بإضافة الكود الخاص بالشكل الجديد.

والعملية لا تتم بهذه البساطة، فغالبا ان عبارات if/else او switch لن تكون بالشكل البسيط الموضح في DrawAllShapes ، فالشروط يمكن ان تكون مركبة ومحتوية على بعض العوامل الأخرى (منطقية ، حسابية ...الخ)

، ايضاً قد نجد ان هناك دوال يمكن تنفيذها على المربع و الدائرة بنفس الطريقة وبالتالي فإنها لا تحتاج إلى عبارات if/else او switch. ما يعني إن عملية إيجاد وفهم كل الأماكن التي يجب ان نضيف إليها الشكل الجديد ليست سهلة إطلاقا.

وحتى اذا استطعنا ان نغير في كل عبارات if/else، فالمسئلة لا تنتهي عند هذا الحد، فعلينا ان نقوم بإعادة بناء وتحزيم ونشر البرنامج مرة أخرى ، ويمكنك ان تتخيل مقدار الوقت والجهد المطلوب لذلك.

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

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

- جامد (rigid) لأنه يصعب التغيير فيه.

- هش (fragile) لأنه قابل للكسر بصورة غير متوقعة – تخيل ماذا سيحدث لو أننا نسينا التغيير في جزء يستخدم عبارة if؟.

- ساكن (immobile) بمعنى انه غير قابل لإعادة الاستخدام بسهولة – فلا يمكن استخدام الدالة DrawAllShapes بدون ان نحضر معها المربع والدائرة

حل يتفق مع OCP :

الكود التالي يعرض الحل الذي يتلاءم مع مبدأ OCP لنفس مشكلة المربع والدائرة ، وفيه قمنا بإنشاء الواجهة Shape تحتوي على الدالة Draw ، تقوم فئة المربع وفئة الدائرة بتحقيقها.

public interface Shape
{
	void Draw();
}
public class Square : Shape
{
	public void Draw()
	{
		//draw a square
	}
}
public class Circle : Shape
{
	public void Draw()
	{
		//draw a circle
	}
}

public void DrawAllShapes(IList shapes)
{
	foreach (Shape shape in shapes)
		shape.Draw();
}

من خلال هذا الحل ، وعندما تأتي المتطلبات الخاصة بإضافة المثلث ، فما علينا إلا ان نضيف فئة خاصة بالشكل الجديد تحقق الواجهة Shape. بدون الحاجة إلى تعديل الدالة DrawAllShapes بأي شكل من الأشكال.

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

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

ايضاً هذا التصميم ليس بالجامد ، فلا حاجة إلى تعديل الكود او إعادة بناء وترجمة اي وحدة من الوحدات السابقة.

واخيراً، فإن هذا الحل ليس ساكن ، فيمكن إعادة استخدام DrawAllShapes بواسطة اي تطبيق بدون الحاجة إلى احضار المربع او الدائرة .

بمعنى إن هذا الحل لا يبدي اي علامة من علامات التصميم السئ الذي ذكرناها سابقاً.

ومثل هذا البرنامج في مجمله يتلائم مع مبدأ OCP ، حيث انه يتغير عن طريق إضافة كود جديد، بدلا من تعديل الكود الموجود. ويغنينا من القيام بسلسلة التغيرات التي يفرضها البرنامج الآخر الذي لا يتلائم مع مبدأ OCP . والمطلوب يتمثل في إضافة فئة جديدة والقيام ببعض التغيرات اللازمة والمتعلقة بـالدالة main من اجل إنشاء الكائنات الجديدة.

كل شئ جيد حتى الآن، لكن ماذا سيحدث على الدالة DrawAllShapes في الحل الأخير، إذا تقرر بان الدوائر يجب ان ترسم قبل المربعات. هذه الدالة ليست منغلقة على مثل هذا التغيير. ولا بد من التعديل عليها بحيث يتم تحقيق ذلك السلوك المطلوب.

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

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

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

بهذا نعرف ان ثمن الملائمة مع مبدأ OCP مرتفع ، فالتجريد يحتاج إلى مزيد من الوقت والجهد، كما انه يزيد من تعقيد البرنامج . ومما لا شك فيه ان هناك حد اقصى لما يمكن ان يتحمله المطورين.

في النهاية سنستنج انه من الأفضل ان نطبق OCP فقط على التغيرات المحتملة فعلا.

لكن كيف نعرف ما هي التغيرات المحتملة فعلاً؟ الجواب ان علينا القيام بالبحث المناسب ،ان نسئل الأسئلة المناسبة وان نستخدم خبرتنا وحواسنا المعتادة، وبعد كل ذلك فإننا ننتظر حصول التغيرات!

قديما ، كنا نقوم بعمل الكثير من التجريدات ، معتقدين بإنها تمثل خطاطيف يمكن ان تحمينا من التغيرات التي نفكر في حدوثها، كان هذا يشعرنا بإن برامجنا سوف تكون اكثر مرونة.

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

الفكرة هنا ان نطبق المثل الذي يقول:

"Fool me once, shame on you. Fool me twice, shame on me."

"ان تخدعني مرة فقط فهذا عار عليك ، ان تخدعني مرتين فهذا عار علي"

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

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

لهذا قد نرغب على تحفيز التغيرات ، من خلال استخدام طرق مثل:

- كتابة الأختبارات اولاً ، حيث ان الأختبار يعتبر شكل من اشكال استعمال البرنامج ، وبكتابة الأختبارت اولاً نحن نجبر النظام على ان يكون قابل للاختبار ، والتغيير في قابلية الأختبار لن تدهشنا لاحقاً، فلدينا التجريدات التي وضعناها في النظام لهذا الغرض ، وغالباً ما سوف نجد بان كثير من هذه التجريدات قد حمتنا من بعض الأنواع من التغيرات لاحقاً(قد ترغب في ان تبحث اكثر في هذا الموضوع).

- ان نستخدم دورة تطوير قصيرة جدا: ايام بدل الأسابيع

- ان نقوم بتطوير الوظائف قبل ان نطور البنية التحتية للبرنامج، وان نقوم دائما بعرضها على المهتمين.

- ان نقوم باطلاق البرمجيات باكرا، بحيث نجعلها امام عملائنا ومستخدمينا باسرع ما يمكن

استخدام التجريد من اجل الأنغلاق:

بما اننا اخذنا الرصاصة الأولى المتمثلة في في رغبة المستخدم في ان ترسم الدوائر قبل المربعات، فإننا نريد الآن ان نحمي انفسنا من تغيرات مستقبلية من هذا النوع.

كيف يمكننا ان نغلق الدالة DrawAllShapes على التغيير المتعلق بترتيب الأشكال؟ هنا يجب ان نتذكر دائما ان الأنغلاق يقوم اساساً على التجريد، لذا فإن علينا ان نقوم بعمل "تجريد للترتيب" يحمينا من اي طريقة ترتيب يتفق عليها.

ان خطة الترتيب تقضي بانه لو كان لدينا كائنين ، فإنه بالإمكان تحديد ايهما يجب ان يرسم اولا. تقدم لغة C# مثل هذا التجريد من خلال الواجهة IComparable التي تحتوي على دالة واحدة فقط CompareTo . هذه الدالة تأخذ وسيطا واحد ، وتعيد القيمة -1 إذا كان الكائن المستدعى اقل من الكائن الوسيط، و القيمة 0 إذا كان الكائن المستدعى يساوي الكائن الوسيط، والقيمة 1 إذا كان الكائن المستدعى اكبر من الكائن الوسيط.

الكود التالي يبين شكل الواجهة Shape عندما ترث IComparable

public interface Shape : IComparable
{
  void Draw();
}

يعطينا هذا معنى لترتيب الأشكال ومعنى لرسمها وفق الترتيب المناسب ، لكن عملية التجريد لم تنتهي حتى الآن ، إذ ان على كل فئة فرعية من الشكل ان تحقق الدالة CompareTo من اجل تحديد الترتيب المطلوب. اذن كيف يكون هذا، وما هو نوع الكود الذي قد نكتبه في Circle.CompareTo للتأكد من ان الدوائر سوف تأتي قبل المربعات؟ تأمل التالي:

public class Circle : Shape
{
public int CompareTo(object o)
{
 if(o is Square)
  return -1;
 else
  return 0;
}
}

يجب ان يكون جلياً ان هذه الدالة وكل الدوال الأخرى المقابلة في بقية الأشكال، لا تتلائم مع مبدأ OCP . ولا توجد طريقة لكي نجعلها منغلقة على الأشكال الجديدة المشتقة. وفي كل مرة نقوم بإنشاء شكل جديد فإن كل دوال CompareTo سوف تحتاج إلى التغيير.

بالطبع، لن يكون لهذا اي قيمة إذا لم يكن هناك اشتقاق لأشكال جديدة ، لكن في المقابل ، إذا كان علينا إنشاء اشكال جديدة بشكل متكرر فإن هذا سوف يكون متعب إلى اقصى الحدود ، ومرة اخرى نكون قد اخذنا الرصاصة الأولى وعلينا نحمي انفسنا من مثيلاتها.

استخدام البيانات لتحقيق الأنغلاق:

إذا كان علينا ان نغلق الأشكال المشتقة من ان تعرف بعضها البعض، فإنه بامكاننا ان نضع بيانات الترتيب داخل Hashtable ونستخدمها على الوجه التالي:

public class ShapeComparer : IComparer
{
private static Hashtable priorities = new Hashtable();
static ShapeComparer()
{
priorities.Add(typeof(Circle), 1);
priorities.Add(typeof(Square), 2);
}
private int PriorityFor(Type type)
{
if(priorities.Contains(type))
return (int)priorities[type];
else
return 0;
}
public int Compare(object o1, object o2)
{
int priority1 = PriorityFor(o1.GetType());
int priority2 = PriorityFor(o2.GetType());
return priority1.CompareTo(priority2);
}
}
public void DrawAllShapes(ArrayList shapes)
{
shapes.Sort(new ShapeComparer());
foreach(Shape shape in shapes)
shape.Draw();
}

بهذا الأسلوب نكون قد نجحنا في اغلاق الدالة DrawAllShapes على مسئلة الترتيب بشكل عام ، وسوف يعمل البرنامج سواء قمنا بإضافة اشكال جديدة او تغيرت خطة الترتيب فيما بعد( كأن يفرض المستخدم ان المربعات يجب تأتي اولاً).

ان الشئ الوحيد الغير منغلق على مسئلة ترتيب الأشكال المتنوعة هو ShapeComparer والذي يمكن وضعه داخل وحدة خاصة به (اسمبلي في هذه الحالة) وبشكل منفصل عن بقية الوحدات، بحيث ان التغيير فيه لن يؤثر على اي وحدة اخرى.

في الختام:

بشكل او بآخر يعتبر مبدأ الأنغلاق والأنفتاح (OCP) جوهر التصميم بالكائنات الموجهة OO. والأمتثال لهذا المبدأ تنتج الفوائد المرجوة من تلك التقنية OO وهي: المرونة، قابلية إعادة الأستخدام، وقابلية الصيانة.

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

عوضا عن هذا علينا ان نقوم بعمل التجريدات لتلك الأجزاء في البرنامج التي تتغير بشكل متكرر . فمقاومة التجريدات السابق لأوانها لا يقل اهمية عن عمل التجريدات نفسها.

=================

من كتاب : Martin C. Robert (2006) Agile Principles, Patterns, and Practices in C#. Prentice Hall

روابط متعلقة:

- The Open-Closed Principle

- Open/closed principle - Wikipedia

1

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

مواضيع مشابهة