إتقان OOP: دليل عملي للوراثة والواجهات وفئات الملخص
نشرت: 2022-03-10بقدر ما أستطيع أن أقول ، من غير المألوف العثور على محتوى تعليمي في مجال تطوير البرمجيات والذي يوفر مزيجًا مناسبًا من المعلومات النظرية والعملية. إذا كنت سأخمن السبب ، أفترض أن السبب هو أن الأفراد الذين يركزون على النظرية يميلون إلى الدخول في التدريس ، والأفراد الذين يركزون على المعلومات العملية يميلون إلى الحصول على أموال لحل مشاكل معينة ، باستخدام لغات وأدوات معينة.
هذا ، بالطبع ، تعميم واسع ، لكن إذا قبلناه بإيجاز من أجل الحجج ، فسيستتبع ذلك أن العديد من الأشخاص (ليس كل الناس بأي حال) الذين يتولون دور المعلم ، يميلون إلى أن يكونوا فقراء أو غير قادرين تمامًا. لشرح المعرفة العملية ذات الصلة بمفهوم معين.
في هذه المقالة ، سأبذل قصارى جهدي لمناقشة ثلاث آليات أساسية ستجدها في معظم لغات البرمجة الشيئية (OOP): الوراثة ، والواجهات (المعروفة أيضًا باسم البروتوكولات ) ، والفئات المجردة . بدلاً من إعطائك تفسيرات شفهية تقنية ومعقدة لماهية كل آلية ، سأبذل قصارى جهدي للتركيز على ما يفعلونه ومتى يتم استخدامه.
ومع ذلك ، قبل أن أخاطبهم بشكل فردي ، أود أن أناقش بإيجاز ما يعنيه إعطاء تفسير سليم من الناحية النظرية ، ولكنه غير مفيد عمليًا. آمل أن تتمكن من استخدام هذه المعلومات لمساعدتك في التدقيق في الموارد التعليمية المختلفة ولتجنب لوم نفسك عندما لا تكون الأمور منطقية.
درجات مختلفة من المعرفة
معرفة الأسماء
يمكن القول إن معرفة اسم شيء ما هو أكثر أشكال المعرفة ضحالة. في الواقع ، يكون الاسم مفيدًا بشكل عام فقط إلى الحد الذي يتم فيه استخدامه بشكل شائع من قبل العديد من الأشخاص للإشارة إلى نفس الشيء و / أو يساعد في وصف الشيء. لسوء الحظ ، كما اكتشف أي شخص قضى وقتًا في هذا المجال ، يستخدم العديد من الأشخاص أسماء مختلفة لنفس الشيء (مثل الواجهات والبروتوكولات ) ، أو نفس الأسماء لأشياء مختلفة (مثل الوحدات والمكونات ) ، أو الأسماء التي تعتبر مقصورة على فئة معينة. نقطة السخف (مثل موناد ). في النهاية ، الأسماء هي مجرد مؤشرات (أو مراجع) للنماذج العقلية ، ويمكن أن تكون ذات درجات متفاوتة من الفائدة.
لجعل هذا المجال أكثر صعوبة في الدراسة ، سأخاطر بتخمين أن كتابة الكود بالنسبة لمعظم الأفراد (أو على الأقل كانت) تجربة فريدة جدًا. الأمر الأكثر تعقيدًا هو فهم كيفية تجميع هذه الشفرة في النهاية في لغة الآلة ، وتمثيلها في الواقع المادي كسلسلة من النبضات الكهربائية تتغير بمرور الوقت. حتى لو استطاع المرء أن يتذكر أسماء العمليات والمفاهيم والآليات المستخدمة في برنامج ما ، فليس هناك ما يضمن أن النماذج العقلية التي يخلقها المرء لمثل هذه الأشياء تتوافق مع نماذج فرد آخر ؛ ناهيك عما إذا كانت دقيقة من الناحية الموضوعية.
لهذه الأسباب ، إلى جانب حقيقة أنه ليس لدي ذاكرة جيدة بشكل طبيعي للمصطلحات ، فإنني أعتبر الأسماء هي الجانب الأقل أهمية في معرفة شيء ما. هذا لا يعني أن الأسماء غير مجدية ، لكنني تعلمت في الماضي العديد من أنماط التصميم وظفتها في مشاريعي ، فقط لأتعرف على أشهر الأسماء المستخدمة بشكل شائع ، أو حتى بعد سنوات.
معرفة التعاريف والمقارنات الشفهية
التعريفات اللفظية هي نقطة البداية الطبيعية لوصف مفهوم جديد. ومع ذلك ، كما هو الحال مع الأسماء ، يمكن أن تكون بدرجات متفاوتة من الفائدة والملاءمة ؛ الكثير من ذلك اعتمادًا على الأهداف النهائية للمتعلم. المشكلة الأكثر شيوعًا التي أراها في التعريفات اللفظية هي المعرفة المفترضة عادةً في شكل المصطلحات.
لنفترض على سبيل المثال ، أنني كنت سأشرح أن الخيط يشبه إلى حد كبير العملية ، باستثناء أن الخيوط تحتل نفس مساحة العنوان لعملية معينة. بالنسبة لشخص على دراية بالعمليات ومساحات العناوين ، فقد ذكرت بشكل أساسي أنه يمكن ربط الخيوط بفهمهم للعملية (أي أنها تمتلك العديد من الخصائص نفسها) ، ولكن يمكن تمييزها بناءً على خاصية مميزة.
بالنسبة إلى شخص لا يمتلك تلك المعرفة ، فأنا في أحسن الأحوال لم أفهم أي معنى ، وفي أسوأ الأحوال جعلت المتعلم يشعر بأنه غير ملائم بطريقة ما لعدم معرفة الأشياء التي افترضت أنه يجب أن يعرفها. في الإنصاف ، هذا أمر مقبول إذا كان على المتعلمين لديك حقًا امتلاك مثل هذه المعرفة (مثل تدريس طلاب الدراسات العليا أو المطورين ذوي الخبرة) ، لكنني أعتبر ذلك بمثابة فشل ذريع في القيام بذلك في أي مادة تمهيدية.
غالبًا ما يكون من الصعب جدًا تقديم تعريف شفهي جيد لمفهوم ما عندما لا يشبه أي شيء آخر رآه المتعلم من قبل. في هذه الحالة ، من المهم جدًا للمعلم أن يختار تشبيهًا من المحتمل أن يكون مألوفًا لدى الشخص العادي ، وكذلك ذو صلة بقدر ما ينقل العديد من نفس الصفات للمفهوم.
على سبيل المثال ، من المهم جدًا لمطور البرامج أن يفهم ما يعنيه عندما تكون الكيانات البرمجية (أجزاء مختلفة من البرنامج) مرتبطة بإحكام أو غير مترابطة . عند بناء سقيفة حديقة ، قد يعتقد النجار الصغير أنه من الأسرع والأسهل تجميعها معًا باستخدام المسامير بدلاً من البراغي. هذا صحيح حتى النقطة التي يرتكب فيها الخطأ ، أو يتطلب التغيير في تصميم سقيفة الحديقة إعادة بناء جزء من السقيفة.
في هذه المرحلة ، فإن قرار استخدام المسامير لإحكام ربط أجزاء الحديقة معًا ، جعل عملية البناء ككل أكثر صعوبة ، ومن المحتمل أن تكون أبطأ ، واستخراج المسامير بمطرقة يهدد بإتلاف الهيكل. على العكس من ذلك ، يمكن أن تستغرق البراغي وقتًا إضافيًا لتجميعها ، ولكن من السهل إزالتها وتشكل خطرًا ضئيلًا في إتلاف الأجزاء القريبة من السقيفة. هذا ما أعنيه بربط غير محكم . بطبيعة الحال ، هناك حالات تحتاج فيها حقًا إلى مسمار ، لكن هذا القرار يجب أن يسترشد بالتفكير النقدي والخبرة.
كما سأناقش بالتفصيل لاحقًا ، هناك آليات مختلفة لربط أجزاء من البرنامج معًا والتي توفر درجات متفاوتة من الاقتران ؛ تمامًا مثل المسامير والبراغي . في حين أن القياس الذي أجريته قد يكون قد ساعدك في فهم معنى هذا المصطلح المهم للغاية ، إلا أنني لم أعطيك أي فكرة عن كيفية تطبيقه خارج سياق بناء كوخ حديقة. يقودني هذا إلى أهم نوع من المعرفة ، والمفتاح لفهم عميق للمفاهيم الغامضة والصعبة في أي مجال من مجالات البحث ؛ على الرغم من أننا سنلتزم بكتابة التعليمات البرمجية في هذه المقالة.
المعرفة في الكود
في رأيي ، فيما يتعلق بتطوير البرمجيات بشكل صارم ، يأتي الشكل الأكثر استيرادًا لمعرفة المفهوم من القدرة على استخدامه في كود التطبيق العملي. يمكن الحصول على هذا الشكل من المعرفة ببساطة عن طريق كتابة الكثير من التعليمات البرمجية وحل العديد من المشكلات المختلفة ؛ لا يلزم تضمين أسماء المصطلحات والتعريفات الشفهية.
في تجربتي الخاصة ، أذكر حل مشكلة الاتصال بقاعدة بيانات بعيدة وقاعدة بيانات محلية من خلال واجهة واحدة (ستعرف ما يعنيه ذلك قريبًا إذا لم تكن قد فعلت ذلك بالفعل) ؛ بدلاً من العميل (أيًا كانت الفئة التي تتحدث إلى الواجهة ) الذي يحتاج إلى استدعاء جهاز التحكم عن بعد والمحلي (أو حتى قاعدة بيانات الاختبار) بشكل صريح. في الواقع ، لم يكن لدى العميل أي فكرة عما وراء الواجهة ، لذلك لم أكن بحاجة إلى تغييرها بغض النظر عما إذا كانت تعمل في تطبيق إنتاج أو بيئة اختبار. بعد حوالي عام من حل هذه المشكلة ، صادفت المصطلح "نموذج الواجهة" ، ولم يمض وقت طويل على مصطلح "نموذج المستودع" ، وهما اسمان يستخدمهما الأشخاص للحل الموصوف مسبقًا.
كل هذه المقدمة تهدف إلى إلقاء الضوء على بعض العيوب التي غالبًا ما تحدث في شرح موضوعات مثل الميراث والواجهات والفئات المجردة . من بين الثلاثة ، من المحتمل أن يكون الميراث هو الأبسط للاستخدام والفهم. في تجربتي كطالب برمجة ومعلم ، فإن الاثنين الآخرين يمثلان دائمًا مشكلة بالنسبة للمتعلمين ما لم يتم إيلاء اهتمام خاص لتجنب الأخطاء التي تمت مناقشتها سابقًا. من الآن فصاعدًا ، سأبذل قصارى جهدي لجعل هذه الموضوعات بسيطة كما ينبغي ، ولكن ليس أبسط.
ملاحظة على الأمثلة
لكوني أكثر طلاقة في تطوير تطبيقات Android للأجهزة المحمولة ، سأستخدم أمثلة مأخوذة من هذا النظام الأساسي حتى أعلمك عن إنشاء تطبيقات واجهة المستخدم الرسومية في نفس الوقت الذي يتم فيه تقديم ميزات لغة Java. ومع ذلك ، لن أخوض في الكثير من التفاصيل بحيث يجب أن تكون الأمثلة غير مفهومة من قبل شخص لديه فهم سريع لـ Java EE أو Swing أو JavaFX. هدفي النهائي في مناقشة هذه المواضيع هو مساعدتك على فهم ما تعنيه في سياق حل مشكلة في أي نوع من التطبيقات.
أود أيضًا أن أحذرك ، عزيزي القارئ ، أنه في بعض الأحيان قد يبدو أنني فلسفيًا ومتحذقًا دون داعٍ بشأن كلمات معينة وتعريفاتها. والسبب في ذلك هو وجود أساس فلسفي عميق مطلوب لفهم الفرق بين الشيء الملموس (الحقيقي) والشيء المجرد (أقل تفصيلاً من الشيء الحقيقي). ينطبق هذا الفهم على أشياء كثيرة خارج مجال الحوسبة ، ولكن من الأهمية بمكان أن يفهم أي مطور برامج طبيعة التجريدات . على أي حال ، إذا فشلت كلماتي ، نأمل ألا تكون الأمثلة في الكود.
الميراث والتنفيذ
عندما يتعلق الأمر بإنشاء تطبيقات بواجهة مستخدم رسومية (GUI) ، يمكن القول إن الوراثة هي أهم آلية لتمكين إنشاء تطبيق بسرعة.
على الرغم من وجود فائدة أقل فهمًا لاستخدام الميراث لتتم مناقشتها لاحقًا ، إلا أن الفائدة الأساسية هي مشاركة التنفيذ بين الفئات . كلمة "تنفيذ" هذه ، على الأقل لأغراض هذه المادة ، لها معنى مميز. لإعطاء تعريف عام للكلمة في اللغة الإنجليزية ، قد أقول إن تنفيذ شيء ما ، هو جعله حقيقيًا .
لإعطاء تعريف تقني خاص بتطوير البرمجيات ، قد أقول إن تنفيذ جزء من البرنامج ، هو كتابة أسطر محددة من التعليمات البرمجية التي تفي بمتطلبات البرنامج المذكور. على سبيل المثال ، لنفترض أنني أكتب عملية الجمع : private double sum(double first, double second){
private double sum(double first, double second){ //TODO: implement }
المقتطف أعلاه ، على الرغم من أنني قمت بكتابة نوع الإرجاع ( double
) وإعلان الطريقة الذي يحدد الوسيطات ( first, second
) والاسم الذي يمكن استخدامه لاستدعاء الطريقة المذكورة ( sum
) ، فقد لم تنفذ . من أجل تنفيذه ، يجب إكمال جسم الطريقة على النحو التالي:
private double sum(double first, double second){ return first + second; }
بطبيعة الحال ، لن يتم تجميع المثال الأول ، لكننا سنرى للحظات أن الواجهات هي طريقة يمكننا من خلالها كتابة هذه الأنواع من الوظائف غير المنفذة دون أخطاء.
الوراثة في جافا
من المفترض ، إذا كنت تقرأ هذا المقال ، فقد استخدمت الكلمة الأساسية extends
Java مرة واحدة على الأقل. آليات هذه الكلمة الرئيسية بسيطة وغالبًا ما يتم وصفها باستخدام أمثلة للقيام بأنواع مختلفة من الحيوانات أو الأشكال الهندسية ؛ يمتد Dog
Cat
إلى Animal
، وهكذا دواليك. سأفترض أنني لست بحاجة إلى شرح نظرية النوع البدائي لك ، لذلك دعونا ندخل مباشرة إلى الفائدة الأساسية للوراثة في Java ، عبر الكلمة الأساسية extends
.
يعد إنشاء تطبيق "Hello World" المستند إلى وحدة التحكم في Java أمرًا بسيطًا للغاية. بافتراض أنك تمتلك Java Compiler ( javac ) و runtime environment ( jre ) ، يمكنك كتابة فئة تحتوي على وظيفة رئيسية مثل:
public class JavaApp{ public static void main(String []args){ System.out.println("Hello World"); } }
يعد إنشاء تطبيق واجهة المستخدم الرسومية في Java على أي من منصاتها الرئيسية تقريبًا (Android ، Enterprise / Web ، Desktop) ، مع القليل من المساعدة من IDE لإنشاء رمز هيكلي / معياري لتطبيق جديد ، أمرًا سهلاً نسبيًا أيضًا بفضل extends
الكلمة.
لنفترض أن لدينا تخطيط XML يسمى activity_main.xml
(نحن عادةً نبني واجهات مستخدم بشكل إعلاني في Android ، عبر ملفات Layout) تحتوي على TextView
(مثل ملصق نصي) يسمى tvDisplay
:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </FrameLayout>
لنفترض أيضًا أننا نود أن يقول tvDisplay
"Hello World!" للقيام بذلك ، نحتاج ببساطة إلى كتابة فصل دراسي يستخدم الكلمة الأساسية extends
لترث من فئة Activity
:
import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((TextView)findViewById(R.id.tvDisplay)).setText("Hello World"); }
يمكن تقدير تأثير وراثة تنفيذ فئة Activity
بشكل أفضل من خلال إلقاء نظرة سريعة على الكود المصدري الخاص به. أشك بشدة في أن Android كان سيصبح النظام الأساسي للهاتف المحمول المهيمن إذا احتاج المرء إلى تنفيذ حتى جزء صغير من أكثر من 8000 سطر ضروري للتفاعل مع النظام ، فقط لإنشاء نافذة بسيطة بها بعض النصوص. الميراث هو ما يسمح لنا بعدم الاضطرار إلى إعادة بناء إطار عمل Android ، أو أي منصة تعمل معها ، من البداية.
يمكن استخدام الميراث في التجريد
بقدر ما يمكن استخدامه لمشاركة التنفيذ عبر الفئات ، فإن الوراثة سهلة الفهم نسبيًا. ومع ذلك ، هناك طريقة مهمة أخرى يمكن من خلالها استخدام الوراثة ، والتي ترتبط من الناحية المفاهيمية بالواجهات والفئات المجردة التي سنناقشها قريبًا.
إذا سمحت ، افترض للحظة التالية أن التجريد ، المستخدم بالمعنى الأكثر عمومية ، هو تمثيل أقل تفصيلاً لشيء ما . بدلاً من تحديد ذلك بتعريف فلسفي مطول ، سأحاول أن أشير إلى كيفية عمل التجريدات في الحياة اليومية ، وبعد ذلك بوقت قصير سأناقشها صراحةً من حيث تطوير البرمجيات.
لنفترض أنك تسافر إلى أستراليا ، وتدرك أن المنطقة التي تزورها تستضيف كثافة عالية بشكل خاص من ثعابين تايبان الداخلية (يبدو أنها سامة جدًا). قررت استشارة ويكيبيديا لمعرفة المزيد عنها من خلال النظر إلى الصور والمعلومات الأخرى. من خلال القيام بذلك ، فأنت الآن تدرك تمامًا نوعًا معينًا من الثعابين لم تره من قبل.
التجريدات ، والأفكار ، والنماذج ، أو أي شيء آخر تريد تسميته ، هي تمثيلات أقل تفصيلاً لشيء ما. من المهم أن تكون أقل تفصيلاً من الأشياء الحقيقية لأن الثعبان الحقيقي يمكن أن يعضك ؛ الصور الموجودة على صفحات ويكيبيديا لا تفعل ذلك عادةً. تعتبر التجريدات مهمة أيضًا لأن كل من أجهزة الكمبيوتر والأدمغة البشرية لديها قدرة محدودة على تخزين المعلومات والتواصل معها ومعالجتها. إن وجود تفاصيل كافية لاستخدام هذه المعلومات بطريقة عملية ، دون شغل مساحة كبيرة في الذاكرة ، هو ما يجعل من الممكن لأجهزة الكمبيوتر والأدمغة البشرية على حد سواء حل المشكلات.
لربط هذا بالوراثة ، يمكن استخدام جميع الموضوعات الرئيسية الثلاثة التي أناقشها هنا كتجريدات أو آليات للتجريد . لنفترض أنه في ملف تخطيط تطبيق "Hello World" الخاص بنا ، قررنا إضافة ImageView
و Button
و ImageButton
:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageButton android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
افترض أيضًا أن نشاطنا قد نفذ View.OnClickListener
للتعامل مع النقرات:
public class MainActivity extends Activity implements View.OnClickListener { private Button b; private ImageButton ib; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... b = findViewById(R.id.imvDisplay).setOnClickListener(this); ib = findViewById(R.id.btnDisplay).setOnClickListener(this); iv = findViewById(R.id.imbDisplay).setOnClickListener(this); } @Override public void onClick(View view) { final int id = view.getId(); //handle click based on id... } }
المبدأ الأساسي هنا هو أن Button
و ImageButton
و ImageView
يرثون من فئة View
. والنتيجة هي أن هذه الوظيفة onClick
يمكنها تلقي أحداث نقر من عناصر واجهة مستخدم متباينة (على الرغم من أنها مرتبطة بالتسلسل الهرمي) من خلال الرجوع إليها على أنها فئة رئيسية أقل تفصيلاً. يعد هذا أكثر ملاءمة من الاضطرار إلى كتابة طريقة مميزة للتعامل مع كل نوع من عناصر واجهة المستخدم على نظام Android الأساسي (ناهيك عن الأدوات المخصصة).
الواجهات والتجريد
ربما تكون قد وجدت مثال الكود السابق غير ملهم بعض الشيء ، حتى لو فهمت سبب اختياره. أن تكون قادرًا على مشاركة التنفيذ عبر تسلسل هرمي للفئات أمر مفيد للغاية ، وأنا أزعم أن هذا هو المنفعة الأساسية للوراثة . فيما يتعلق بالسماح لنا بمعاملة مجموعة من الفئات التي لها فئة رئيسية مشتركة على أنها متساوية في النوع (مثل الفئة الأصلية ) ، فإن ميزة الوراثة هذه لها استخدام محدود.
بشكل محدود ، أنا أتحدث عن متطلبات أن تكون الفئات الفرعية ضمن نفس التسلسل الهرمي للفئة من أجل الإشارة إليها عبر ، أو المعروفة باسم الفئة الأصلية. بمعنى آخر ، الوراثة آلية مقيدة للغاية للتجريد . في الواقع ، إذا افترضت أن التجريد عبارة عن طيف يتحرك بين مستويات مختلفة من التفاصيل (أو المعلومات) ، فقد أقول إن الوراثة هي أقل آلية تجريدية للتجريد في Java.
قبل الشروع في مناقشة الواجهات ، أود أن أذكر أنه اعتبارًا من Java 8 ، تمت إضافة ميزتين تسمى الأساليب الافتراضية والطرق الثابتة إلى الواجهات . سأناقشهم في النهاية ، لكن في الوقت الحالي أود أن نتظاهر بأنهم غير موجودين. هذا لكي أجعل من السهل شرح الغرض الأساسي من استخدام الواجهة ، والتي كانت في البداية ، ويمكن القول أنها لا تزال ، الآلية الأكثر تجريدًا للتجريد في Java .
تفاصيل أقل تعني المزيد من الحرية
في القسم الخاص بالميراث ، قدمت تعريفًا لكلمة تنفيذ ، والتي كان من المفترض أن تتناقض مع مصطلح آخر سنناقشه الآن. لأكون واضحًا ، لا أهتم بالكلمات نفسها ، أو ما إذا كنت توافق على استخدامها ؛ فقط أنك تفهم ما يشيرون إليه من الناحية المفاهيمية.
في حين أن الوراثة هي في الأساس أداة لمشاركة التنفيذ عبر مجموعة من الفئات ، فقد نقول أن الواجهات هي في الأساس آلية لمشاركة السلوك عبر مجموعة من الفئات. السلوك المستخدم بهذا المعنى هو في الحقيقة مجرد كلمة غير تقنية للطرق المجردة . الطريقة المجردة هي عملية لا تحتوي في الواقع على جسم طريقة :
public interface OnClickListener { void onClick(View v); }
كان رد الفعل الطبيعي بالنسبة لي ، ولعدد من الأفراد الذين قمت بتعليمهم ، بعد النظر لأول مرة في الواجهة ، هو التساؤل عن فائدة مشاركة نوع الإرجاع واسم الطريقة وقائمة المعلمات فقط. ظاهريًا ، يبدو أنه طريقة رائعة لإنشاء عمل إضافي لنفسك ، أو لأي شخص آخر يكتب الفصل الذي implements
الواجهة . الإجابة ، هي أن الواجهات مثالية للمواقف التي تريد فيها مجموعة من الفئات أن تتصرف بنفس الطريقة (أي أنها تمتلك نفس الأساليب المجردة العامة) ، لكنك تتوقع منهم تنفيذ هذا السلوك بطرق مختلفة.
لنأخذ مثالًا بسيطًا ولكنه ملائم ، تمتلك منصة Android فئتين تعملان بشكل أساسي في إنشاء وإدارة Fragment
من واجهة المستخدم: Activity
والجزء. ويترتب على ذلك أن هذه الفئات غالبًا ما تتطلب الاستماع إلى الأحداث التي تنبثق عند النقر فوق عنصر واجهة المستخدم (أو التفاعل مع المستخدم بطريقة أخرى). من أجل الجدل ، دعونا نتوقف لحظة لنفهم لماذا لن تحل الوراثة مثل هذه المشكلة:
public class OnClickManager { public void onClick(View view){ //Wait a minute... Activities and Fragments almost never //handle click events exactly the same way... } }
لن يقتصر الأمر على جعل الأنشطة OnClickManager
مما يجعل من المستحيل التعامل مع الأحداث بطريقة مختلفة ، ولكن المهم هو أننا لا نستطيع حتى القيام بذلك إذا أردنا ذلك. يعمل كل من النشاط والجزء بالفعل على توسيع فئة أصل ، ولا تسمح Java بفئات رئيسية متعددة. لذا فإن مشكلتنا هي أننا نريد مجموعة من الفصول أن تتصرف بنفس الطريقة ، ولكن يجب أن تكون لدينا المرونة في كيفية تنفيذ الفصل لهذا السلوك . هذا يعيدنا إلى المثال السابق View.OnClickListener
.
public interface OnClickListener { void onClick(View v); }
هذا هو رمز المصدر الفعلي (المتداخل في فئة View
) ، وتسمح لنا هذه الأسطر القليلة بضمان سلوك متسق عبر عناصر واجهة مستخدم مختلفة ( طرق عرض) ووحدات تحكم واجهة المستخدم ( الأنشطة ، الأجزاء ، إلخ ).
يعزز التجريد الاقتران المفكوك
أتمنى أن أجب على السؤال العام حول سبب وجود الواجهات في Java ؛ من بين العديد من اللغات الأخرى. من منظور واحد ، فهي مجرد وسيلة لمشاركة التعليمات البرمجية بين الفئات ، لكنها عمداً أقل تفصيلاً من أجل السماح بالتطبيقات المختلفة. ولكن مثلما يمكن استخدام الوراثة كآلية لمشاركة الكود والتجريد (وإن كان ذلك مع وجود قيود على التسلسل الهرمي للفئة) ، فإن ذلك يتبع أن الواجهات توفر آلية أكثر مرونة للتجريد .
في قسم سابق من هذه المقالة ، قدمت موضوع الاقتران السائب / الضيق عن طريق قياس الفرق بين استخدام المسامير والبراغي لبناء نوع من الهياكل. للتلخيص ، الفكرة الأساسية هي أنك ستحتاج إلى استخدام البراغي في المواقف التي من المحتمل أن يحدث فيها تغيير الهيكل الحالي (والذي يمكن أن يكون نتيجة لإصلاح الأخطاء وتغييرات التصميم وما إلى ذلك). من الجيد استخدام المسامير عندما تحتاج فقط إلى ربط أجزاء من الهيكل معًا ولا تشعر بالقلق بشكل خاص بشأن تفكيكها في المستقبل القريب.
من المفترض أن تكون المسامير والبراغي مماثلة للمراجع الملموسة والمجردة (ينطبق مصطلح التبعيات أيضًا) بين الفئات. فقط حتى لا يكون هناك أي لبس ، سيوضح النموذج التالي ما أعنيه:
class Client { private Validator validator; private INetworkAdapter networkAdapter; void sendNetworkRequest(String input){ if (validator.validateInput(input)) { try { networkAdapter.sendRequest(input); } catch (IOException e){ //handle exception } } } } class Validator { //...validation logic boolean validateInput(String input){ boolean isValid = true; //...change isValid to false based on validation logic return isValid; } } interface INetworkAdapter { //... void sendRequest(String input) throws IOException; }
هنا ، لدينا فئة تسمى Client
والتي تمتلك نوعين من المراجع . لاحظ أنه ، بافتراض أن Client
لا علاقة له بإنشاء مراجعه (لا ينبغي فعل ذلك) ، فإنه منفصل عن تفاصيل التنفيذ الخاصة بأي محول شبكة معين.
هناك عدد قليل من الآثار الهامة لهذا الاقتران السائب . بالنسبة للمبتدئين ، يمكنني إنشاء Client
بمعزل عن أي تطبيق لـ INetworkAdapter
. تخيل للحظة أنك تعمل في فريق من مطورين ؛ واحد لبناء الواجهة الأمامية وواحد لبناء النهاية الخلفية. وطالما ظل كلا المطورين على دراية بالواجهات التي تجمع بين فصولهما معًا ، فيمكنهما الاستمرار في العمل بشكل مستقل عن بعضهما البعض.
ثانيًا ، ماذا لو أخبرتك أن كلا المطورين يمكنهما التحقق من أن تطبيقاتهما تعمل بشكل صحيح ، وأيضًا بشكل مستقل عن تقدم بعضهما البعض؟ هذا سهل للغاية مع الواجهات ؛ ما عليك سوى إنشاء اختبار مزدوج يقوم implements
الواجهة المناسبة:
class FakeNetworkAdapter implements INetworkAdapter { public boolean throwError = false; @Override public void sendRequest(String input) throws IOException { if (throwError) throw new IOException("Test Exception"); } }
من حيث المبدأ ، ما يمكن ملاحظته هو أن العمل مع المراجع المجردة يفتح الباب أمام زيادة نمطية وقابلية الاختبار وبعض أنماط التصميم القوية جدًا مثل نمط الواجهة ونمط المراقب وغير ذلك. يمكنهم أيضًا السماح للمطورين بالعثور على توازن سعيد في تصميم أجزاء مختلفة من النظام بناءً على السلوك ( Program To An Interface ) ، دون التورط في تفاصيل التنفيذ .
نقطة أخيرة في التجريد
لا توجد التجريدات بنفس الطريقة التي توجد بها الأشياء الملموسة . ينعكس هذا في لغة برمجة Java من خلال حقيقة أن الفئات والواجهات المجردة قد لا يتم إنشاء مثيل لها.
على سبيل المثال ، لن يؤدي هذا بالتأكيد إلى تجميع:
public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { //ERROR x2: Foo f = new Foo(); Bar b = new Bar() } private abstract class Foo{} private interface Bar{} }
في الواقع ، فإن فكرة توقع واجهة غير مطبقة أو فئة مجردة للعمل في وقت التشغيل تبدو منطقية تمامًا مثل توقع أن يطفو زي UPS حول تسليم الحزم. يجب أن يكون هناك شيء ملموس وراء التجريد حتى يكون ذا فائدة ؛ حتى لو لم يكن الفصل الداعي بحاجة إلى معرفة ما هو في الواقع وراء المراجع المجردة .
فئات مجردة: ضعها جميعًا معًا
إذا كنت قد وصلت إلى هذا الحد ، فأنا سعيد بإخبارك أنه ليس لدي المزيد من الظلال الفلسفية أو العبارات الاصطلاحية التي يجب ترجمتها. ببساطة ، الفصول المجردة هي آلية لمشاركة التنفيذ والسلوك عبر مجموعة من الفئات. الآن ، سأعترف على الفور أنني لا أجد نفسي أستخدم الفصول المجردة كثيرًا. ومع ذلك ، آمل أنه بنهاية هذا القسم ستعرف بالضبط متى يتم استدعاؤهم.
دراسة حالة سجل التمرين
بعد مرور عام تقريبًا على إنشاء تطبيقات Android في Java ، كنت أعيد إنشاء تطبيق Android الأول الخاص بي من البداية. الإصدار الأول كان نوعًا من الكتلة الرهيبة من التعليمات البرمجية التي تتوقعها من مطور يدرس نفسه بنفسه مع القليل من التوجيه. بحلول الوقت الذي أردت فيه إضافة وظائف جديدة ، أصبح من الواضح أن الهيكل المترابط بإحكام الذي قمت ببنائه حصريًا بالمسامير ، كان من المستحيل الحفاظ عليه لدرجة أنني يجب أن أعيد بنائه بالكامل.
كان التطبيق عبارة عن سجل تمرين تم تصميمه للسماح بتسجيل التدريبات الخاصة بك بسهولة ، والقدرة على إخراج بيانات تمرين سابق كملف نصي أو صورة. بدون الخوض في الكثير من التفاصيل ، قمت ببناء نماذج البيانات الخاصة بالتطبيق بحيث كان هناك كائن Workout
، والذي يتكون من مجموعة من كائنات Exercise
(من بين الحقول الأخرى التي لا صلة لها بهذه المناقشة).
عندما كنت أقوم بتطبيق ميزة إخراج بيانات التمرين إلى نوع من الوسائط المرئية ، أدركت أنه يجب علي التعامل مع مشكلة: تتطلب أنواع مختلفة من التمارين أنواعًا مختلفة من مخرجات النص.
لإعطائك فكرة تقريبية ، أردت تغيير المخرجات اعتمادًا على نوع التمرين مثل ذلك:
- باربل: 10 REPS @ 100 LBS
- الدمبل: 10 REPS @ 50 LBS x2
- وزن الجسم: 10 REPS @ Bodyweight
- وزن الجسم +: 10 REPS @ Bodyweight + 45 LBS
- التوقيت: 60 ثانية لكل 100 رطل
قبل المتابعة ، لاحظ أن هناك أنواعًا أخرى (يمكن أن يصبح التمرين معقدًا) وأن الكود الذي سأعرضه قد تم تقليصه وتغييره ليلائم مقالة بشكل جيد.
تمشيا مع تعريفي من قبل ، فإن الهدف من كتابة فئة مجردة ، هو تنفيذ كل شيء (حتى الحالة مثل المتغيرات والثوابت ) التي يتم مشاركتها عبر جميع الفئات الفرعية في فئة الملخص . ثم ، لأي شيء يتغير عبر الفئات الفرعية المذكورة ، قم بإنشاء عملية مجردة :
abstract class Exercise { private final String type; protected final String name; protected final int[] repetitionsOrTime; protected final double[] weight; protected static final String POUNDS = "LBS"; protected static final String SECONDS = "SEC "; protected static final String REPETITIONS = "REPS "; public Exercise(String type, String name, int[] repetitionsOrTime, double[] weight) { this.type = type; this.name = name; this.repetitionsOrTime = repetitionsOrTime; this.weight = weight; } public String getFormattedOutput(){ StringBuilder sb = new StringBuilder(); sb.append(name); sb.append("\n"); getSetData(sb); sb.append("\n"); return sb.toString(); } /** * Append data appropriately based on Exercise type * @param sb - StringBuilder to Append data to */ protected abstract void getSetData(StringBuilder sb); //...Getters }
قد أذكر ما هو واضح ، ولكن إذا كان لديك أي أسئلة حول ما يجب أو لا ينبغي تنفيذه في الفصل التجريدي ، فإن المفتاح هو النظر إلى أي جزء من التنفيذ تم تكراره في جميع الفصول الدراسية.
الآن بعد أن أنشأنا ما هو مشترك بين جميع التمارين ، يمكننا البدء في إنشاء فصول فرعية مع تخصصات لكل نوع من مخرجات السلسلة:
تمرين الحديد:
class BarbellExercise extends Exercise { public BarbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append("\n"); } } }
تمرين الدمبل:
class DumbbellExercise extends Exercise { private static final String TIMES_TWO = "x2"; public DumbbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append(TIMES_TWO); sb.append("\n"); } } }
تمرين وزن الجسم:
class BodyweightExercise extends Exercise { private static final String BODYWEIGHT = "Bodyweight"; public BodyweightExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(BODYWEIGHT); sb.append("\n"); } } }
أنا متأكد من أن بعض القراء المخضرمين سيجدون أشياء كان من الممكن استخلاصها بطريقة أكثر كفاءة ، لكن الغرض من هذا المثال (الذي تم تبسيطه من المصدر الأصلي) هو إظهار النهج العام. بالطبع ، لن تكتمل أي مقالة برمجية بدون شيء يمكن تنفيذه. هناك العديد من برامج التحويل البرمجي لـ Java عبر الإنترنت والتي قد تستخدمها لتشغيل هذا الرمز إذا كنت ترغب في اختباره (إلا إذا كان لديك IDE بالفعل):
public class Main { public static void main(String[] args) { //Note: I actually used another nested class called a "Set" instead of an Array //to represent each Set of an Exercise. int[] reps = {10, 10, 8}; double[] weight = {70.0, 70.0, 70.0}; Exercise e1 = new BarbellExercise( "Barbell", "Barbell Bench Press", reps, weight ); Exercise e2 = new DumbbellExercise( "Dumbbell", "Dumbbell Bench Press", reps, weight ); Exercise e3 = new BodyweightExercise( "Bodyweight", "Push Up", reps, weight ); System.out.println( e1.getFormattedOutput() + e2.getFormattedOutput() + e3.getFormattedOutput() ); } }
Executing this toy application yields the following output: Barbell Bench Press
10 REPS @ 70.0LBS 10 REPS @ 70.0LBS 8 REPS @ 70.0LBS Dumbbell Bench Press 10 REPS @ 70.0LBSx2 10 REPS @ 70.0LBSx2 8 REPS @ 70.0LBSx2 Push Up 10 REPS @ Bodyweight 10 REPS @ Bodyweight 8 REPS @ Bodyweight
Further Considerations
Earlier, I mentioned that there are two features of Java interfaces (as of Java 8) which are decidedly geared towards sharing implementation , as opposed to behavior . These features are known as Default Methods and Static Methods .
I have decided not to go into detail on these features for the reason that they are most typically used in mature and/or large code bases where a given interface has many inheritors. Despite the fact that this is meant to be an introductory article, and I still encourage you to take a look at these features eventually, even though I am confident that you will not need to worry about them just yet.
I would also like to mention that there are other ways to share implementation across a set of classes (or even static methods ) in a Java application that does not require inheritance or abstraction at all. For example, suppose you have some implementation which you expect to use in a variety of different classes, but does not necessarily make sense to share via inheritance . A common pattern in Java is to write what is known as a Utility class, which is a simple class
containing the requisite implementation in a static method :
public class TimeConverterUtil { /** * Accepts an hour (0-23) and minute (0-59), then attempts to format them into an appropriate * format such as 12, 30 -> 12:30 pm */ public static String convertTime (int hour, int minute){ String unformattedTime = Integer.toString(hour) + ":" + Integer.toString(minute); DateFormat f1 = new SimpleDateFormat("HH:mm"); Date d = null; try { d = f1.parse(unformattedTime); } catch (ParseException e) { e.printStackTrace(); } DateFormat f2 = new SimpleDateFormat("h:mm a"); return f2.format(d).toLowerCase(); } }
Using this static method in an external class (or another static method ) looks like this:
public class Main { public static void main(String[] args){ //... String time = TimeConverterUtil.convertTime(12, 30); //... } }
Cheat Sheet
We have covered a lot of ground in this article, so I would like to spend a moment summarizing the three main mechanisms based on what problems they solve. Since you should possess a sufficient understanding of the terms and ideas I have either introduced or redefined for the purposes of this article, I will keep the summaries brief.
I Want A Set Of Child Classes To Share Implementation
Classic inheritance , which requires a child class to inherit from a parent class , is a very simple mechanism for sharing implementation across a set of classes. An easy way to decide if some implementation should be pulled into a parent class , is to see whether it is repeated in a number of different classes line for line. The acronym DRY ( Don't Repeat Yourself ) is a good mnemonic device to watch out for this situation.
While coupling child classes together with a common parent class can present some limitations, a side benefit is that they can all be referenced as the parent class , which provides a limited degree of abstraction .
I Want A Set Of Classes To Share Behavior
Sometimes, you want a set of classes to be capable of possessing certain abstract methods (referred to as behavior ), but you do not expect the implementation of that behavior to be repeated across inheritors.
By definition, Java interfaces may not contain any implementation (except for Default and Static Methods ), but any class which implements an interface , must supply an implementation for all abstract methods, otherwise, the code will not compile. This provides a healthy measure of flexibility and restriction on what is actually shared and does not require the inheritors to be of the same class hierarchy .
I Want A Set Of Child Classes To Share Behavior And Implementation
Although I do not find myself using abstract classes all over the place, they are perfect for situations when you require a mechanism for sharing both behavior and implementation across a set of classes. Anything which will be repeated across inheritors may be implemented directly in the abstract class
, and anything which requires flexibility may be specified as an abstract method .