أستمع الى المقال

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

يوفر نمط OOP الوصفات (المفاهيم) فقط، وفي نهاية المطاف، الأمر متروك لنا كمبرمجين في اختيار النسب من هذه الوصفات، لـ (طهي) برامج جيدة قابلة للصيانة.

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

مبدأ إعادة الاستخدام في البرمجة الكائنية:

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

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

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

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

التركيب Composition:

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

في الأغلب، يتم الإشارة إلى التركيب، على أنه علاقة has-a. والكلمة has-a، يمكننا فهمها على أنها كلمة “لديه” في اللغة العربية. كمثال، إذا كان لدينا العبارة “المنزل لديه مطبخ”، أو A house has a kitchen. يمكننا التعبير عنها باستخدام شفرة كوتلن، على النحو التالي:

class Kitchen

class House {
    val kitchen: Kitchen = Kitchen()
}

عند النظر إلى العلاقة بين الصنفين: Kitchen و House، نجد أن المنزل House قد يملك مطبخ Kitchen والعكس غير صحيح. إذًا العلاقة بين الصنفين، هي علاقة has-a. وبما أن المنزل هو من يملك المطبخ، إذًا يمكننا إنشاء كائن المطبخ داخل الصنف House. ولأن الكائن Kitchen تم إسناده لمتغير داخل صنف House، يمكننا الوصول لكل أعضاء الصنف Kitchen العامّة (public)، باستخدام المتغير المسمى kitchen.

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

وضع أكثر من كائن في الصنف:

إذا كان المنزل يملك مطبخين مثلًا، يمكننا تمثيل هذه العلاقة على النحو التالي:

class Kitchen

class House {
    val kitchen1: Kitchen = Kitchen()
    val kitchen2: Kitchen = Kitchen()
}

نحن هنا بكل بساطة، نضع عدة كائنات من المطبخ، داخل صنف المنزل. كل كائن منهما، لديه متغير يُمثله. أمّا إذا كنا نريد متغير واحد، يُمثل عدد لا نهائي من كائنات المطبخ، حينها يمكننا استخدام القوائم Lists:

class Kitchen

class House {
    val kitchens: List<Kitchen> = listOf()
}

الفرق بين الوراثة والتركيب:

عند بداية تعلمنا للبرمجة، نقضي الكثير من الوقت والجهد في فهم الوراثة وتعقيداتها. ومن ثم يتولّد لدينا انطباعًا، بأن الوراثة هي الأكثر أهمية من التركيب. ولكن هناك قاعدة تقول:

من الأفضل اختيار التركيب بدلًا عن الوراثة.

ويكيبيديا

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

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

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

بالعودة إلى مثالنا السابق عن المنزل والمطبخ، يمكننا النظر إلى علاقة أخرى بين المنزل وكائن آخر، وهو المبنى Building. حينما نقول العبارة التالية: “المنزل هو مبنى” أو A house is a building. تبدو العبارة صحيحة أليس كذلك؟ فالمبنى من الممكن أن يكون برج أو منزل. بالتالي، البرج “هو” مبنى، والمنزل “هو” مبنى. إذًا، العلاقة بين المنزل والمبنى، هي علاقة is-a. والكلمة التي تقابلها في العربية، هي كلمة “هو”.

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

open class Building

class Kitchen

class House: Building() {
    val kitchens: List<Kitchen> = listOf()
}

يمكن لكل الأصناف التي تربطها علاقة is-a بالصنف Building، الوراثة منه لأنه مفتوح open للوراثة. كمثال آخر، الفندق قد يكون به طوابق وغرف ومطبخ رئيسي:

open class Building

class Floor {
    val rooms: Room = Room()
}
class Room
class Kitchen

class Hotel: Building() {
    val mainKitchen: Kitchen = Kitchen()
    val floors: List<Floor> = listOf()
    val rooms: List<Room> = listOf()
}

class House: Building() {
    val kitchens: List<Kitchen> = listOf()
    val rooms: List<Room> = listOf()
}

لأن الفندق “هو” مبنى، جعلناه يرث من الصنف Building. ولأن الفندق “لديه” مطبخ رئيسي Main Kitchen، وطوابق Floors، وغرف Rooms، استخدمنا مبدأ التركيب ووضعنا كل هذه الكائنات بداخله. علاقة أخرى نلاحظها في الشفرة أعلاه، وهي أن الطابق Floor “لديه” غرف. في هذه الحالة أيضًا، استخدمنا التركيب ووضعنا كائن Room مباشرةً داخل الصنف Floor.

وهكذا نمضي بعملية التفكير في ما هو المبدأ الأفضل استخدامه في تصميم شفرتنا حاليًا، حسب القاعدتين: للوراثة is-a، وللتركيب has-a

مثال عملي آخر:

وكمثال آخر، لنفترض أنه لدينا كائن سيارة Car. نحن نعرف أن كائن السيارة يتكون من كائنات أصغر. وعند وجود هذه الكائنات معه في الشفرة، هنا تتحقق العلاقة “لديه” أو has-a. وبناء على ذلك، سنستخدم مبدأ التركيب، ونضع كل هذه الكائنات مباشرةً في كائن السيارة:

class Engine {
    fun start() = println("Engine started")
    fun stop() = println("Engine stopped")
}

class Wheel {
    fun inflate(psi: Int) = println("Wheel inflate($psi)")
}

class Window(private val side: String) {
    fun rollUp() = println("$side Window rolled up")
    fun rollDown() = println("$side Window rolled down")
}

class Door(private val side: String) {
    val window = Window(side)
    fun open() = println("$side Door opened")
    fun close() = println("$side Door closed")
}

class Car {
    val engine = Engine()
    val wheel = List(4) { Wheel() }
    // Two door:
    val leftDoor = Door("Left")
    val rightDoor = Door("Right")
}

لدينا في الشفرة، 5 أصناف: المحرك Engine، والعَجَل Wheel، والنافذة Window، والباب Door، والسيارة Car. الأصناف الـ 4 الأولى، تملك دوال تقوم بالوظائف الخاصة بها. بعضها لديه خاصيّات، يجب إرسال قيمة لها عند إنشاء كائن من الصنف. كما في صنفي النافذة والباب واللذين يحتاجان إلى معرفة الجانب side المُراد. ثم لدينا صنف الباب، الذي “لديه” نافذة. ولكون العلاقة بين الصنفين كذلك، وضعنا كائن من الصنف window داخل Door.

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

والآن، يمكننا إنشاء كائن من السيارة Car، ومن ثم الوصول إلى كل الكائنات الأخرى والوظائف العامّة التي تقدمها، عبر هذا الكائن:

fun main() {
    val car = Car()
    car.leftDoor.open()
    car.leftDoor.close()
    car.rightDoor.window.rollUp()
    car.leftDoor.window.rollDown()
    car.wheel[0].inflate(72)
    car.engine.start()
}

أنشأنا كائن سيارة واحد فقط. وعبر هذا الكائن، وصلنا لكل الكائنات الأخرى. فمثلًا، لدينا في صنف Car الكائن leftDoor. هذا كائن يُمثل الباب اليسار تحديدًا، لأننا نُرسل له القيمة “Left” لخاصيّته side. و leftDoor هو أيضًا خاصيّة بالنسبة للصنف Car. لذا عند إنشاء كائن من Car، يمكننا الوصول إلى خاصيّاته عبر استخدام النقطة (.):

car.leftDoor

وبما أن الخاصيّة leftDoor داخل الصنف Car تمثل كائن الباب، يمكننا عبر استخدام النقطة الوصول إلى دواله أيضًا:

car.leftDoor.open()

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

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

Left Door opened
Left Door closed
Right Window rolled up
Left Window rolled down
Wheel inflate(72)
Engine started

الخلاصة:

باستخدام مبدأ الوراثة، يمكننا إنشاء نسخة مخصصة من صنف موجود ذي غرض عام. يأخذ الصنف الجديد خاصيّات ودوال الصنف القديم، ويخصصها حسب احتياجه. ولكن من الأفضل اتباع قاعدة is-a قبل استخدام هذا المبدأ. ففي مثالنا الأخير للسيارة مثلًا، إذا كان هناك صنف اسمه مركبة Vehicle، نجد العبارة “السيارة لديها مركبة” غير منطقية وغير صحيحة. أمّا عبارة “السيارة هي مركبة”، أو Car is a Vehicle، نجدها منطقية أكثر وأصح من العبارة السابقة. لذا، يكون من المنطقي استخدام الوراثة بين هذين الصنفين في هذه الحالة.

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

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

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