استخدام ال Interface وال Abstract Class يضيفان طبقة من ال Abstraction في الكود، والفروقات التي بينهم هي التي تحدد أي Abstraction سوف تفضلها للمشكلة التي تريد حلها.
في هذه السلسلة سوف نوضح ما يلي:
- الفرق بين ال Abstract Class وال Interface (المقالة الحالية)
- دور ال Interface في قابلية الصيانة Maintainbility
- عمل ال Repository باستخدام ال Interface
- ما هو ال Dynamic Factory
هذه المقالة سوف تجيب عن السؤال الشائع “ما هو الفرق بين ال Abstract Class وال Interface” وسوف نأخذ مثال ويتم برمجته بعدة طرق: الطريقة التقليدية باستخدام الدوال Procedures ثم الانتقال لل Classes واستخدام ال Abstraction بأنواعها الثلاثة سواء الوراثة من الكلاس العادي Concrete class أو الوراثة من Abstract Class أو استخدام ال interface، ومن ثم يتم النظر لتلك الحلول ومشاهدة الانسب للمشكلة، وأخيراً يتم طرح الفروق بين ال Abstract class وال Interface ومتى تستخدم كل منهم.
ملاحظة سوف نستخدم لغة سي# في توضيح الأمثلة، لكن اذا كنت مبرمج جافا أو اي لغة كائنية OO أخرى، فلا تقلق يمكنك التكملة في المقال وستعي كل المفاهيم بكل سهولة. وسنوضح قدر الامكان أي فروقات بين اللغتين في المقالة اذا وجدت.
ما هو ال Interface
ببساطة هو Abstract type لا يوجد فيه أي Implementation أو Data فقط تصاريح للدوال Declarations. وأفضل طريقة للتفكير في ال interface هو أنه عقد contract وله أعضاء members مثل ال methods وال Properties (ال Properties هي في السي# ويمكن اعتبار وجودها في ال Interface على أنه بمثابة تصريح لمتغير)، وعندما يقوم أي كلاس أخر بعمل Implements لذلك العقد فيجب عليه أن يقوم بتطبيقه بالكامل ويعرف كل ما هو موجود في ذلك ال interface
لاحظ أن كل الأعضاء سوف تكون public تلقائياً (بفكرة العقد فلا يمكن أن يكون هناك عقد غير معروف بنوده، وأن جميع بنوده يجب أن تكون معروفة public).
المثال الأول
سوف نبدأ بمثال عملي بسيط نوضح فيه المفاهيم والأساسيات بين ال Interface مع ال Abstract class والكلاس العادي Concrete (سوف نطلق على أي class عادي ليس ب Abstract بالاسم Concrete class)، وليكن لدينا برنامج لحساب مساحة ومحيط المربع من خلال ادخال طول الضلع وتقوم بإظهار المحيط والمساحة.
بالطبع هذا الموضوع ليس في الرياضيات (واطمئن لن نكمل كثيراً في هذا المثال) وانما هو مجرد بداية توضيحية، وقوانين حساب المحيط والمساحة للمربع كالتالي:
- مساحة المربع = طول الضلع في نفسه
- محيط المربع = 4× طول الضلع
بمعنى أن المدخل سوف يكون طول الضلع، والمطلوب منك كتابة برنامج يقرأ المدخل ومن ثم يطبع المساحة والمحيط.
بالطبع أي مبرمج سوف يبدأ من ال mainويكتب كود بسيط يطبق تلك المعادلات، كما في الشكل:
static void Main(string[] args) { // Triangle int numberOfSides = 3; // Calculate Area Console.Write("Enter Side Length: "); int sideLength = int.Parse(Console.ReadLine()); double area = sideLength * sideLength * Math.Sqrt(3) / 4; Console.WriteLine("Area: " + area); // Calculate Perimeter double permieter = numberOfSides * sideLength; Console.WriteLine("Permieter: " + permieter); Console.ReadKey(); }
تعديل 1: الآن لو طلبنا من اضافة خاصية جديدة لهذا البرنامج وهي حساب محيط ومساحة المثلث متساوي الأضلاع ايضاً من خلال معرفه طول أحد الأضلاع، من خلال المعادلة التالية:
- مساحة المثلث = حيث a هي طول الضلع.
- محيط المثلث = 3 * طول الضلع
قد تكتب نفس الكود التالي:
static void Main(string[] args) { // Triangle int numberOfSides = 3; // Calculate Area Console.Write("Enter Side Length: "); int sideLength = int.Parse(Console.ReadLine()); double area = sideLength * sideLength * Math.Sqrt(3) / 4; Console.WriteLine("Area: " + area); // Calculate Perimeter double permieter = numberOfSides * sideLength; Console.WriteLine("Permieter: " + permieter); Console.ReadKey(); }
إلى هنا الأمر جميل وأنت تعمل بطريقة اجرائية Procedure أو لنقل بأن برنامج بسيط للغاية ولا يخرج من كونه Toys Example.
لاحظ أن الفرق بين الكودين (الأول والثاني) وهو يكون فقط في الأسطر (4 و9) وبقية الكود هو كود متكرر. ودائماً في حال هناك تكرار في الكود فعليك أن تعرف أن هناك تصميم غير جيد، وأن الكود قد يقع في مشاكل عديدة مع أي تغيير أو اضافة تحدث.
والتغييرات دائماً ما تحصل، قد تتساءل كيف سيحصل التغيير في هذا الكود البسيط؟ الجواب كما حدث في تعديل 1 حيث تم اضافة المثلث فقد يمكن اضافة اشكال اخرى، أو ايضاً يمكن أن يحدث في عدة أماكن أخرى، مثلاً طريقة الإدخال، فبدلاً من أنك تدخل طول الضلع من لوحة المفاتيح، سوف يطلب منك أن تقرأ طول الضلع من ملف خارجي به وصف لأشكال هندسية مثل مربع ومستطيل ومثلث ونريد أن تقوم بحساب المساحة والمحيط لكل شكل موجود في الملف.
تعديل 2: الآن طلب أن يتم قراءة معلومات الأشكال من ملف خارجي (سواء كان XML أو أي ملف بأي صيغة) المهم به تعريف تلك الاشكال وعليك أن تقرأها جميعاً من ذلك الملف ومن ثم تحسب المساحة والمحيط.
هذه اللحظة سوف تحتاج لأن تتعامل مع المفهوم Concept بدلاً من أن تتعامل مع متغيرات مثلاً numberOfSide وarea فالأفضل أن تتعامل مع المفهوم نفسه (الشكل الهندسي) سواء كان Triangle أو Square وبالتالي تستطيع أن ترجع من دالة القراءة من الملف مجموعه من الأشكال Shapes، وهذه أولى فوائد ال Object Oriented حيث تستطيع تمثيل المفاهيم بطريقة أفضل بدلاً من التعامل مع متغيرات.
لكن المشكلة أنك الأن سوف تحتاج لأن ترجع جميع الأشكال في الملف مع بعضها، والا سوف تقوم بعمل دالة لإرجاع المثلثات، ودالة لإرجاع المربعات والخ، ولقد تم اخبارك أن هذه الاشكال سوف يتم عمل مجموعه من العمليات عليها فيما بعد مثلاً عرضها على الشاشة أو تطبيق مجموعه من الحسابات بها. لذلك يجب أن تستخدم مفهوم موحد Abstraction يشمل هذين الشكلين وبقية الأشكال في المجموعة.
لتطبيق مفهوم الشكل Abstraction قد تقوم بأكثر من طريقة:
- عمل Concrete class(كلاس عادي) يمثل الشكل مثلاً نسميه Regular Polygon(يمكنك تسميته بأي اسم) ومن ثم كلاس المربع والمثلث يرثان Inheritingمن ذلك الكلاس.
- عمل Abstract class يمثل الشكل Regular Polygonومن ثم والمربع والمثلث يرثان منه
- عمل Interface اسمه Regular Polygon ويقوم المربع والمثلث باستخدامه Implement
سوف نتناول هذه الحلول ولنرى ماهي الفوائد والعيوب في كل حل منهم.
تطبيق ال Abstraction باستخدام الكلاس العادي Concrete Class
لاحظ الكود التالي يقوم بعمل class يمثل الشكل وفيه عدد الأضلاع وطول الضلع، بالإضافة إلى دالة حساب المحيط والتي هي مشتركة بين المثلث والمربع، اما دالة حساب المساحة فطالما الشكل غير محدد فسوف يتم عمل throw لل Exception في حالة تم استدعاء هذه الدالة.
public class ConcreteRegularPolygon { public int NumberOfSides { get; set; } public int SideLength { get; set; } public ConcreteRegularPolygon(int sides, int length) { NumberOfSides = sides; SideLength = length; } public double GetPerimeter() { return NumberOfSides * SideLength; } public virtual double GetArea() { throw new NotImplementedException(); } }
الآن نقوم بتعريف الكلاس Square ولاحظ أنه يرث من الشكل ويقوم بعمل override للدالة المساحة.
public class Square : ConcreteRegularPolygon { public Square(int length) : base(4, length) { } public override double GetArea() { return SideLength * SideLength; } }
نفس الأمر مع الكلاس ال Triangle:
public class Triangle : ConcreteRegularPolygon { public Triangle(int length) : base(3, length) { } public override double GetArea() { return SideLength * SideLength * Math.Sqrt(3) / 4; } }
لننظر الآن إلى استخدام تلك الكلاسات في الدالة الرئيسية، ولتجربة كلاس المربع Square:
static void Main(string[] args) { Square square = new Square(5); Console.WriteLine("Area: " + square.GetArea()); Console.WriteLine("Permieter: " + square.GetPerimeter()); Console.ReadKey(); }
النتيجة سوف تكون صحيحة بعد تشغيل الكود، سوف تجد انه قام بحساب المساحة والمحيط وتم طباعتهم على الشاشة.
الذي يهم في هذه الطريقة هو أن الدالة GetArea في الكلاس (الأب) ConcreteRegularPolygon يقوم بعمل throw exception في حال تم استدعائها، وبالتالي إذا قمت بعمل الكلاس المثلث Triangle ونسيت اعادة تعريف هذه الدالة فسوف تحصل على ال Exception.
للننظر إلى ال class التالي وهو تعريف آخر للمثلث ونسى المبرمج تعريف الدالة GetArea، وتم استخدامه:
class Triangle : ConcreteRegularPolygon { public Triangle(int length) : base(3, length) { } static void Main(string[] args) { Triangle sequare = new Triangle(5); Console.WriteLine("Area: " + sequare.GetArea()); Console.WriteLine("Permieter: " + sequare.GetPerimeter()); Console.ReadKey(); } }
في حال قمت بتشغيل الكود أعلاه سوف تحصل على ال Exception
بالطبع هذا ال Exception نحن الذي كتبناه في كلاس الأب ولكن بسبب أن الابن لم يقوم بتغييره أي عمل Override (والمترجم لم يجبره على ذلك) لذلك حدث هذا الخطأ.
فيما يلي سوف نستعرض الحل الثاني في ال Abstraction وهو يحل المشكلة السابقة (المترجم لم يجبر المبرمج على اعادة التعريف) ويجبرك على أن تعيد التعريف override وإلا فلن تتم ترجمة الكود.
تطبيق ال Abstraction باستخدام ال Abstract Class
للتذكير فإن ال Abstract Class هو الكلاس الذي يحتوي على دالة أو أكثر abstract ولا يمكن عمل كائن منه. الكود التالي يعرض تعريف الشكل بطريقة ال Abstract.
public abstract class AbstractRegularPolygon { public int NumberOfSides { get; set; } public int SideLength { get; set; } public AbstractRegularPolygon(int sides, int length) { NumberOfSides = sides; SideLength = length; } public double GetPerimeter() { return NumberOfSides * SideLength; } public abstract double GetArea(); }
هذا مشابه لل Concrete Class لكن فقط الاختلاف في ال GetArea حيث يوجد تعريف فقط Declaration ولا يوجد body في الكود، أي هي abstract ويجب عمل الكلاس abstract (بالطبع هذا يعني أنه لا يمكن عمل كائن من هذا class).
كلاس المربع والمثلث سوف يكونا متشابهين وبالطبع سوف يلزمك المترجم أن تقوم بعمل تعريف للدالة GetArea وإلا فسوف تحصل على خطأ في الترجمة
public class Square : AbstractRegularPolygon { public Square(int length) : base(4, length) { } public override double GetArea() { return SideLength * SideLength; } } public class Triangle : AbstractRegularPolygon { public Triangle(int length) : base(3, length) { } public override double GetArea() { return SideLength * SideLength * Math.Sqrt(3) / 4; } }
الكود في ال main سوف يكون مشابه ولا يوجد عليه تغيير:
static void Main(string[] args) { Triangle triangle = new Triangle(5); Console.WriteLine("Area: " + triangle.GetArea()); Console.WriteLine("Permieter: " + triangle.GetPerimeter()); Console.ReadKey(); }
جرب أن تقوم بوضع تعليق على أي من دوال ال GetArea في كلاس المثلث أو المربع (على اعتبار أنك نسيت كتابتها) وستجد رسالة الخطأ واضحة، كما يلي:
هكذا بطريقة ال Abstract class فإن المترجم يجبرك على عمل override لكل ال Abstract methodsوإلا فسوف تحصل على خطأ اثناء عملية الترجمة Compile Time Error.
تطبيق ال Abstraction باستخدام ال Interface
هذه المرة سوف نستخدم ال interface في نفس المثال، وهو مشابه لل abstract class ولكن بدون Implementation (فقط يوضح العقد Contract)، والكود التالي يعرض ال Interface:
public interface IRegularPolygon { int NumberOfSides { get; set; } int SideLength { get; set; } double GetPerimeter(); double GetArea(); }
من ال Convention في سي# أن تبدأ ال Interface بالحرف I، ومن العادات الجيدة أن تتبعه في تسميتك، لذلك قمنا بتسميته IRegularPolygon
لاحظ أن:
- ال Interface فقط يحتوي على declaration بدون Implementation، وحتى المتغيرين هم فقط declaration وليس تعريف لمتغير property ويجب أن تكون في الكلاس الذي يطبق ذلك ال Interface
- عدم وجود أي Access Modifier حيث كل ال Members هم public تلقائياً، وإذا حاولت اضافه أي Modifier سوف تحصل على خطأ في الترجمة
مثال على كلاس المربع الذي يستخدم ذلك ال Interface
public class Square : IRegularPolygon { public int NumberOfSides { get; set; } public int SideLength { get; set; } public Square(int length) { NumberOfSides = 4; SideLength = length; } public double GetPerimeter() { return NumberOfSides * SideLength; } public double GetArea() { return SideLength * SideLength; } }
لاحظ الآن وجود المتغيرات Automatic Property وهي ال Implementation لما هو موجود، بالإضافة إلى الدوال ايضاً.
مثال ال main أيضاً هو نفسه (كالمثال السابق في ال Abstract class أو الاول) ولا يحتاج لأي تغيير مع الأمثلة السابقة.
ملاحظة: إذا حاولت أن لا تطبق كل الدوال الموجودة في ال Interface سوف تحصل على خطأ وقت الترجمة ايضاً (مثل طريقة ال Abstract class).
لنعد لتعديل رقم 2 وهو دالة جلب جميع الأشكال، فسواء قمت بأي طريقة من ال Abstraction السابقة فيمكنك أن تقوم بتطبيق تلك الدالة بسهولة كما يلي:
private static IRegularPolygon[] ExtractShapes() { // Read from external sources and return the polygons // parsing the file and extract the shapes return new IRegularPolygon[] { new Sequare(3), new Triangle(4), new Sequare(5) }; }
طبعاً بغض النظر عن طريقة ال Parsing والتعامل مع الملف، المهم هو انه استطعنا جلب جميع الاشكال من تلك الدالة، وفي المثال اعلى تم استخدام Interface ك Abstraction. لكن مرة أخرى أي طريقة منهم سوف تفي بالغرض مكانها.
الاختيار بين Concrete Class و Abstract Class و Interface
حتى هذه اللحظة نكون قد عرضنا الحلول الثلاثة لنفس المشكلة والسؤال الآن من تختار فيما بينهم؟
- ال concrete class لا تلزمك بعمل Implementation (وقد تحصل على خطأ على حسب الكود الموجود)
- ال abstract class وال Interface تلزمك بكتابة التعريف Implementation والا سوف تحصل على خطأ وقت الترجمة
طبعاً أن تجد الأخطاء وقت الترجمة أفضل وأسهل لك بكثير من أن تجدها وقت التشغيل، ولذلك فال Concrete Classes ك Abstraction لا يفضل أن يستخدم، ويبقى الفرق بين ال Abstract class وال Interface
الفرق بين ال Abstract class وال Interface
والفرق يكمن في هذه النقاط:
- ال abstract قد تحتوي على كود Implementation، بينما ال Interface لا تحتوي على كود وانما تحتوي على تصاريح declaration
- عندما تستخدم ال abstractفإن كل الكلاسات تستطيع الوراثة منه ولغة البرمجة سي# وجافا تسمح بالوراثة فقط من كلاس واحد، لكن ال interface تستطيع عمل Implements لأكثر من interface
- الأعضاء members في ال abstract class قد تحتوي على Access Modifier، بينما في ال Interface كلها تعتبر Public
- ال abstract class تحتوي على دوال بناء وهدم ومتغيرات، بينما ال interface لا تحتوي على دالة بناء ولا هدم ولا متغيرات
أول فرقين سوف يعطيك سهولة القرار فيمن سوف تستخدم في تصميمك، فميزة ال Abstract Class سوف يحتوي على الكود الذي تتشارك فيه جميع الأبناء، وهذا مفيد في حالة كان جميع الأبناء سوف يأخذوا نفس ال Implementation لكن مشكلته أنك لا تستطيع أن ترث من أكثر من كلاس واحد لذلك قد لا يجدي معك إذا كان كذلك قد يرث من شيء آخر.
بعكس ال Interface التي يمكن أن تطبق على أكثر من Class، ولكنها بدون Implementation.
نقطة أخرى مهمة في اختيار ال Interface ك Abstraction هي أن الوراثة لا تجدي في كثير من الحالات ويفضل ال Composition عليها (ما الفرق بين ال Composition و ال Aggregation)، واحياناً تكون خيار خاطئ في حال لم تحقق الشرط IS-A.
في مثالنا الاول (مثال الأشكال) وجدنا أن استخدام ال interface أو ال abstract class يعطي نفس النتيجة، وقد تجد أن ال Abstract مناسب هنا لأن هناك تشارك في الكود (حساب المساحة) بين المثلث والمربع.
خلاصة
- ال Interface هي Contract بدون Implementation وتحتوي على Public Members
- المترجم يلزم جميع مستخدمي هذا ال Interface بتطبيق كل ال Declaration
- تمت المقارنة بين كل من ال Interface وال Abstract Class ومعرفة ميزة كل منهم.
في مقالات قادمة سوف تعرف استخدامات انسب لل interface وهكذا سوف يكون لديك كل المعرفة لكي تتخذ القرار المناسب باختيار ال interface وال abstract class.