تعلّم البرمجة بلغة كوتلن (63): الوراثة والدوال المُلحقة Inheritance & Extensions

تعلّم البرمجة بلغة كوتلن (63): الوراثة والدوال المُلحقة Inheritance & Extensions
أستمع الى المقال

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

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

مفهوم الكائنات، هو مفهوم واحد من بين العديد من المفاهيم، لكنه ليس لكل شيء. أحيانًا يكون استخدام مفهوم الكائنات، مفيدًا للغاية. ولكن في كثير من الأحيان، الدوال هي كل ما نحتاجه.

من كتاب Atomic Kotlin بتصرف

إضافة وظيفة عبر الوراثة:

لنفترض أن مبرمجًا آخر كتب صنفًا class لتمثيل المدفأة (سخان) وأسماه Heater ووضع به الدوال الخاصة به. ولغرض التبسيط دعونا نفترض أن الصنف لديه دالة واحدة فقط، كالتالي:

open class Heater {
    fun heat(temperature: Int) = "Heating to: $temperature"
}

fun warm(heater: Heater) {
    println(heater.heat(25))
}

كما نرى، الصنف مفتوح open للوراثة، وبه دالة واحدة وهي ()heat. وأن هناك دالة خارج الصنف اسمها warm. لدى الدالة ()warm معامل نوع بياناته هو Heater، لذا يمكنه استقبال أي كائن من النوع Heater بما فيها الكائنات من الأصناف التي ترث من Heater. وهو مبدأ شرحناه تفصيليًا في درسي: تحويل النوع لأعلى upcasting و تعدد الأشكال Polymorphism.

نحن الآن، نريد أن نستخدم هذا الصنف، ولكننا نحتاج أيضًا إلى وظيفة أخرى، وهي التبريد cooling. لأن ما نريده حقًا، هو نظام تدفئة وتبريد وتكييف الهواء، بالإنجليزية (Heating, Ventilation and Air Conditioning (HVAC. بالطبع ليس علينا تجاهل الصنف Heater المتواجد مسبقًا والذي بكل تأكيد تم فحصه والتأكد منه، وإنشاء صنف جديد كليًا يكلف المزيد من الجهد ويخرِق مبدأ إعادة استخدام الشفرة.

اختيار الوراثة بدلًا عن التركيب:

لذلك، سننشئ صنفًا جديدًا ونجعله يرث من الصنف Heater لإعادة استخدام الخاصيّات والدوال التي يوفرها. وفي حالتنا هي دالة واحدة heat، ثم نضيف الدوال الخاصة بنا:

class HVAC: Heater() {
    fun cool(temperature: Int) = "Cooling to: $temperature"
}

وعندما ننشئ كائن من HVAC، يمكننا استخدام دالة ()heat، بالإضافة إلى الدالة التي يملكها الصنف، وهي ()cool. ويمكننا أيضًا استخدام دالة ()warm، وهو ما لا يمكن حدوثه إذا استخدمنا التركيب Composition بدلًا عن الوراثة:

fun main() {
    val hvac = HVAC()
    warm(hvac)
}

عند استدعاء دالة ()warm، أرسلنا لها كائن من النوع المُشتق HVAC، على الرغم من أن معاملها هو من النوع الأعلى Heater. وعملًا بمبدأ التحويل لأعلى، تستدعي الدالة، دالة ()heat، المتواجدة في الصنف الأعلى. لتكون نتيجة الطباعة، هي:

Heating to: 25

نلاحظ أن نص الطباعة قادم من الدالة ()heat المتواجدة في الصنف الأعلى (الأب) Heater.

إنشاء دالة تعدد أشكال جديدة:

بالطبع ما نريده هو أن يتم استدعاء دالة ()heat ودالة ()cool عبر نفس الكائن. لا يمكن استدعاء دالة ()cool المتواجدة في الصنف HVAC لأنه يتم تحويل نوعه للأعلى، بالتالي ستتوفر الدوال الخاصة بالصنف الأعلى Heater فقط (وهو يعرف بمبدأ قابلية الاستبدال Substitutability والذي تحدثنا عنه في درس الـ upcasting). وإذا افترضنا أننا لا يمكننا التعديل على دالة ()warm، لذا يجب أن نقوم بإنشاء دالة جديدة تحقق لنا هذا الأمر:

class HVAC: Heater() {
    fun cool(temperature: Int) = "Cooling to: $temperature"
}

fun warmAndCool(hvac: HVAC) {
    println(hvac.heat(25))
    println(hvac.cool(18))
}

وعند استدعاء الدالة الجديدة ()warmAndCool، سيتم عبر استخدام الكائن من صنف HVAC، استدعاء الدالة الخاصة به ()cool، و أيضًا دالة ()heat المتواجدة في الصنف Heater:

fun main() {
    val hvac = HVAC()
    warmAndCool(hvac)
}

وعند تنفيذ الشفرة، ستكون النتيجة هي:

Heating to: 25
Cooling to: 18

هكذا نكون قد حللنا مشكلتنا الحالية، وهي إضافة وظيفة تبريد عبر دالة ()cool. وحققنا إعادة مبدأ إعادة استخدام الشفرة، باستخدامنا الدالة ()heat في الصنف Heater.

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

رائحة الخطأ في الشفرة Code Smell والدّين التقني Technical Debt:

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

وقد يسبب إضافة وظائف عبر الوراثة، مشكلة للشخص الذي سيعمل صيانة وتعديل الشفرة لاحقًا (والذي قد نكون نحن بعد عدة أشهر). الإهمال في تجنب إشارات وعلامات وجود خطأ في تصميم الشفرة، لتحقيق هدف قصير المدة، سيجلب تكاليف إضافية – جهد لا داعي له – لاحقًا. وهي نفس فكرة الدّين المالي في الحياة الواقعية. ويعرف في البرمجة بـ الدّين التقني Technical Debt.

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

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

استبدال الوراثة بالدوال المُلحقة:

ما أردناه في البداية عند إنشاء صنف HVAC، هو إضافة وظيفة تبريد لأنها غير متواجدة في الصنف Heater، لنتمكن من استخدامها في دالة ()warmAndCool. يمكننا القيام بنفس الأمر دون الاضطرار إلى الوراثة، عبر استخدام الدوال المُلحقة:


fun Heater.cool(temperature: Int) = "cooling to: $temperature"

fun warmAndCool(heater: Heater) {
    println(heater.heat(25))
    println(heater.cool(18))
}

في الشفرة أعلاه، نستفيد من ميزة في لغة كوتلن، والتي تتمثل في إضافة وظائف للأصناف التي لا نملك إمكانية تعديلها – وهو هنا صنف Heater -، ونضيف دالة ()cool كدالة مُلحقة بالصنف Heater. ثم بالطبع يجب تعديل نوع بيانات معامل دالة ()warmAndCool من الصنف السابق HVAC الذي حذفناه لعدم احتياجنا له الآن، إلى النوع Heater.

وعند تنفيذ الشفرة، ستكون النتيجة هي نفسها كالسابق:

Heating to: 25
cooling to: 18

بدلاً عن إنشاء صنف جديد ومن ثم الوراثة من الصنف Heater، باستخدام الدوال المُلحقة، يمكننا تمديد – توسيع – وظائف الصنف Heater مباشرةً دون وراثة.

الخلاصة:

تسمح لغات البرمجة مثل ++C و جافا بالوراثة تلقائيًا. وعليه مهمة منع الوراثة من الصنف، تقع على عاتق المبرمج باستخدام الكلمة المفتاحية final. على العكس من ذلك، تفترض لغة البرمجة كوتلن، عدم الحاجة لاستخدام الوراثة وتعدد الأشكال. لذلك، مهمة جعل الصنف قابلًا للوراثة، تقع على المبرمج باستخدام الكلمة المفتاحية open.

وهذا الأمر، يعطينا نظرة إلى توجه لغة كوتلن. والتي تعمل بنظرية:

مفهوم الكائنات، هو مفهوم واحد من بين العديد من المفاهيم، لكنه ليس لكل شيء. أحيانًا يكون استخدام مفهوم الكائنات، مفيدًا للغاية. ولكن في كثير من الأحيان، الدوال هي كل ما نحتاجه.

إذا كنا محتارين في موقف ما، بين استخدام الوراثة أم لا، يجب أن نفكر، هل نحتاج الوراثة للقيام بهذه المهمة أساسًا. لأنه في نهاية المطاف، يمكننا أيضًا استبدالها بمفهومي التركيب والدوال المُلحقة.

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

هل أعجبك المحتوى وتريد المزيد منه يصل إلى صندوق بريدك الإلكتروني بشكلٍ دوري؟
انضم إلى قائمة من يقدّرون محتوى إكسڤار واشترك بنشرتنا البريدية.