السابقالفهرسالتالي

الفصل 15

البرمجة كائنية التوجّه

 

15.1 لغات البرمجة وأساليبها

يوجد العديد من لغات البرمجة وهي تقريباً بعدد أساليب البرمجة (أحياناً تدعى paradigms). كانت البرامج التي كتبناها حتى الآن إجرائية (procedural)، لأن تركيزنا كان منصباً على تحديد مجريات الحسابات.

معظم البرامج المكتوبة بلغة Java تكون كائنية التوجه (Object-oriented)، ما يعني أن التركيز يكون على الكائنات وتفاعلاتها (interactions). ها هي بعض خصائص البرمجة كائنية التوجه:

في هذا الفصل سأترجم برنامج Card من الفصل السابق من الأسلوب الإجرائي إلى الأسلوب كائني التوجه. يمكنك تنزيل شفرة هذا الفصل من http://thinklikecs.webs.com/resources/code/CardSoln3.java.

15.2 عمليات الكائنات وعمليات الأصناف

يوجد نوعين من العمليات في Java، يدعيان عمليات الأصناف (class methods) وعمليات الكائنات أو العمليات الكائنية (object methods). تميَّز عمليات الأصناف بالكلمة المفتاحية static في السطر الأول. أي عملية لا تملك الكلمة المفتاحية static تكون عملية كائنية.

بالرغم من أننا لم نكتب أية عمليات كائنية حتى الآن، إلا أننا قد استدعينا بعضاً منها. كلما تستدعي عملية "على" كائن، تكون هذه العملية عملية كائنية. مثلاً، charAt وغيرها من العمليات التي استدعيناها على كائنات String كانت كلها عمليات كائنية.

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

مثلاً، هذه هي العملية printCard مكتوبة كعملية صنف:

public static void printCard(Card c) {
   System.out.println(ranks[c.rank] + " of " + suits[c.suit]);
}
وها هي كعملية كائنية:
public void print() {
   System.out.println(ranks[rank] + " of " + suits[suit]);
}
ها هي الاختلافات بين النسختين:
  1. أزلت الكلمة static.
  2. غيرت اسم العملية ليكون معبراً أكثر.
  3. أزلت المعامل.
  4. يمكنك الإشارة إلى متغيرات الحالة كما لو كانت متغيرات محلية داخل عمليات الكائنات، لذا غيرت c.rank إلى rank، كما قمت بنفس التغيير مع suit.
سيتم استدعاء هذه العملية بالشكل التالي:
Card card = new Card(1, 1);
card.print();
عندما تستدعي عملية على كائن، يصبح ذلك الكائن الكائن الحالي (current object)، ويعرف أيضاً بthis. داخل print، تشير this إلى كائن Card الذي تم استدعاء العملية عليه.

15.3 عملية toString

لكل نوع كائني عملية تدعى toString تعيد الكائن ممثلاً بسلسلة من المحارف (String). عندما تطبع كائناً باستخدام العملية print أو println، تستدعي Java العملية الكائنية toString.

النسخة الافتراضية من toString تعيد سلسلة محرفية تحتوي على نوع الكائن ومعرّف فريد (انظر القسم 11.6). عندما تعرف نوعاً كائنياً جديداً، يمكنك تجاهل (override) السلوك الافتراضي بكتابة عملية جديدة تنفذ السلوك الذي ترغب.

مثلاً، ها هي عملية toString للكائن Card:

public String toString() {
   return ranks[rank] + " of " + suits[suit];
}
من الطبيعي أن يكون نوع الإرجاع String، ولا تأخذ هذه العملية أية معاملات. يمكنك استدعاء toString بالطريقة المعتادة:
Card card = new Card(1, 1);
String s = card.toString();
أو يمكنك استدعاءها مباشرة من خلال println:
System.out.println(card);

15.4 عملية equals

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

يختبر عامل == التطابق، ولا يوجد عامل لاختبار المساواة، لأن تعريف "المساواة" يعتمد على نوع الكائن. بدلاً من ذلك، تملك الكائنات عملية تدعى equals تعرّف مساواة الكائنات.

توفر أصناف Java عمليات equals التي تنفذ عمليات المقارنة بالشكل الصحيح، لكن بالنسبة للأنواع التي يعرفها المستخدم يكون تعريف العملية الافتراضي نفس تعريف التطابق، وهو ما لا تريده عادة.

بالنسبة لكائنات Card فقد كتبنا مسبقاً عملية تتحقق من المساواة:

public static boolean sameCard(Card c1, Card c2) {
   return (c1.suit == c2.suit && c1.rank == c2.rank);
}
لذا كل ما علينا فعله هو إعادة كتابتها بشكل عملية كائنية:
public boolean equals(Card c2) {
   return (suit == c2.suit && rank == c2.rank);
}
لقد أزلت الكلمة static والمعامل الأول c1. هذه هي كيفية استدعاء هذه العملية:
Card card = new Card(1, 1);
Card card2 = new Card(1, 1);
System.out.println(card.equals(card2));
داخل equals، يكون card هو الكائن الحالي وcard2 يكون المعامل c2. بالنسبة للعمليات التي تعمل على كائنين من نفس النوع، أحياناً أستخدم الكلمة this صراحة وأدعو المعامل الثاني that:
public boolean equals(Card that) {
   return (this.suit == that.suit && this.rank == that.rank);
}
أعتقد أن هذا يجعل فهم العملية أسهل.

تمرين 15.1

نزل http://thinklikecs.webs.com/resources/code/CardSoln2.java و http://thinklikecs.webs.com/resources/code/CardSoln3.java.

يحتوي CardSoln2 على حلول تمارين الفصل السابق. وهو يستخدم عمليات الأصناف فقط (ما عدا العمليات البانية).

يحتوي CardSoln3 على نفس البرامج، لكن معظم العمليات أصبحت كائنية. لقد تركت merge كما هي لأنني أعتقد أنها أسهل للفهم كعملية صنف.

حول merge إلى عملية كائنية، وغير mergeSort وفقاً لذلك. أي نسخة من merge أفضل؟

15.5 الشذوذ والأخطاء

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

يمكن أن توجد عملية كائنية وعملية صنف بنفس الاسم، طالما أنهما تختلفان في عدد المعاملات أو نوعها. وكما يحصل مع الأنواع الأخرى من التحميل الزائد (overloading)، تقرر Java أي نسخة من العملية ستستدعيها بالنظر إلى المتحولات التي تعطيها.

الآن وقد بتنا نعرف معنى الكلمة المفتاحية static، ستكون قد استنتجت على الأغلب أن main هي عملية صنف، ما يعني عدم وجود "كائن حالي" عند استدعائها.

نظراً لعدم وجود كائن حالي في عمليات الأصناف، من الخطأ استعمال الكلمة المفتاحية this. إذا حاولت عمل ذلك، ستحصل على رسالة خطأ مثل: "Undefined variable: this" – "متغير غير معرّف: this".

كما لا يمكنك الوصول إلى متغيرات الحالة بدون استخدام النقطة وتوفير اسم كائن. إذا حاولت عمل ذلك، ستحصل على رسالة مثل "non-static variable… cannot be referenced from a static context". يقصد المجمع بكلمة "non-static variable" أن يقول: "متغير حالة" — "instance variable".

15.6 الوراثة

إن الوراثة (inheritance) هي أكثر مقومات اللغة المرتبطة بمفهوم البرمجة الكائنية على الأغلب. الوراثة هي القدرة على تعريف صنف جديد يكون نسخة معدلة من صنف موجود.

لتوسيع المصطلح، الصنف الموجود يدعى أحياناً الصنف الأب (parent) والصنف الجديد يكون الابن (child).

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

إذا حللت تمارين GridWorld (الفصلين 5 و10) فقد شاهدت أمثلة عن الوراثة:

public class BoxBug extends Bug {
   private int steps;
   private int sideLength;

   public BoxBug(int length) {
      steps = 0;
      sideLength = length;
   }
}
BoxBug extends Bug تعني أن BoxBug هو نوع جديد من Bug يرث العمليات ومتغيرات الحالة الخاصة بBug. يضاف إلى ذلك: وإذا حللت تمارين الرسوميات في الملحق A، فقد رأيت مثالاً آخر:
public class MyCanvas extends Canvas {
   public void paint(Graphics g) {
      g.fillOval(100, 100, 200, 200);
   }
}
MyCanvas هو نوع جديد من Canvas ليس له أي متغيرات حالة أو عمليات جديدة، لكنه يتجاهل العملية paint.

إذا لم تحل أياً من هذه التمارين، سيكون الآن وقتاً مناسباً!

15.7 سلسلة الأصناف الهرمية

في Java، كل الأصناف توسع أصنافاً أخرى. الصنف الأساسي الأول يدعى Object. لا يحتوي ذاك الصنف على أية متغيرات حالة، لكنه يوفر العمليتين toString وequals، من بين عمليات أخرى يوفرها أيضاً.

العديد من الأصناف توسع Object، بما فيها جميع الأصناف التي كتبناها تقريباً والعديد من أصناف Java، مثل java.awt.Rectangle. أي صنف لا يصرح عن اسم والده صراحة سيرث من Object افتراضياً.

بعض السلاسل الوراثية تكون أطول. مثلاً، java.swing.JFrame يوسِّع java.awt.Frame، الذي يوسّع Window، الذي يوسع Container، الذي يوسّع Object. مهما كان طول السلسلة، سيكون Object الجد المشترك لجميع الأصناف.

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

15.8 التصميم كائني التوجه

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

من جهة أخرى، قد تجعل الوراثة البرامج صعبة القراءة. عندما ترى استدعاءً لعملية، من الصعب معرفة أي عملية ستستدعى.

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

تصميم الكائنات والعلاقات فيما بينها هو محور التصميم كائني التوجّه (object-oriented design)، وهو يتخطى مدى هذا الكتاب. لكن إذا كنت مهتماً، أنصحك بكتاب Head First Design Patterns، الذي نشرته O'Reilly Media.

15.9 المصطلحات

عملية كائنية: عملية يتم استدعاؤها على كائن، وتشتغل على ذلك الكائن، الذي يشار له بالكلمة المفتاحية this في Java أو "الكائن الحالي" في اللغة العربية. العمليات الكائنية لا تملك الكلمة المفتاحية static.

عملية صنف: عملية لها الكلمة المفتاحية static. لا تستدعى عمليات الأصناف على الكائنات وهي لا تملك كائناً حالياً.

الكائن الحالي: الكائن الذي تم استدعاء العملية الكائنية عليه. داخل العملية، يشار للكائن الحالي بالكلمة this.

this: الكلمة المفتاحية التي تشير إلى الكائن الحالي.

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

صريح: أي شيء يقال بشكل كامل. داخل عملية صنف، يجب أن تكون كافة المرجعيات إلى متغيرات الحالة صريحة.

object method: A method that is invoked on an object, and that operates on that object, which is referred to by the keyword this in Java or "the current object" in English. Object methods do not have the keyword static.

class method: A method with the keyword static. Class methods are not invoked on objects and they do not have a current object.

current object: The object on which an object method is invoked. Inside the method, the current object is referred to by this.

this: The keyword that refers to the current object.

implicit: Anything that is left unsaid or implied. Within an object method, you can refer to the instance variables implicitly (without naming the object).

explicit: Anything that is spelled out completely. Within a class method, all references to the instance variables have to be explicit.

15.10 تمرينات

تمرين 15.2

حول عملية الصنف التالية إلى عملية كائنية.

public static double abs(Complex c) {
   return Math.sqrt(c.real * c.real + c.imag * c.imag);
}

تمرين 15.3

حول العملية الكائنية التالية إلى عملية صنف.

public boolean equals(Complex b) {
   return(real == b.real && imag == b.imag);
}

تمرين 15.4

هذا التمرين تابع للتمرين 11.3. الغرض هو التدرب على بنية عمليات الكائنات والتعود على رسائل الخطأ المتعلقة بها.

  1. حول العمليات في صنف Relational من عمليات صنف إلى عمليات كائنات، وقم بالتغييرات اللازمة في main.
  2. اصنع بعض الأخطاء. جرب استدعاء عمليات صنف على أنها عمليات كائنية وبالعكس. حاول أن تعرف ما هو المشروع وما هو الممنوع، وحاول أن تفهم رسائل الخطأ التي تحصل عليها عندما تخلط الأمور.
  3. فكر بمحاسن ومساوئ عمليات الأصناف وعمليات الكائنات. أيهما أكثر اختصاراً (عادة)؟ أيهما تبدو طبيعية أكثر للتعبير عن الحسابات (computations)؟ (أو ربما، أي نوع من الحسابات يمكن التعبير عنه بصورة طبيعية أكثر باستخدام كل من النمطين؟)

تمرين 15.5

The goal of this exercise is to write a program that generates random poker hands and classifies them, so that we can estimate the probability of the various poker hands. If you don't play poker, you can read about it here http://en.wikipedia.org/wiki/List_of_poker_hands.

  1. Start with http://thinklikecs.webs.com/resources/code/CardSoln3.java. and make sure you can compile and run it.
  2. Write a definition for a class named PokerHand that extends Deck.
  3. Write a Deck method named deal that creates a PokerHand, transfers cards from the deck to the hand, and returns the hand.
  4. In main use shuffle and deal to generate and print four PokerHands with five cards each. Did you get anything good?
  5. Write a PokerHand method called hasFlush returns a boolean indicating whether the hand contains a flush.
  6. Write a method called hasThreeKind that indicates whether the hand contains Three of a Kind.
  7. Write a loop that generates a few thousand hands and checks whether they contain a flush or three of a kind. Estimate the probability of getting one of those hands. Compare your results to the probabilities at http://en.wikipedia.org/wiki/List_of_poker_hands.
  8. Write methods that test for the other poker hands. Some are easier than others. You might find it useful to write some general-purpose helper methods that can be used for more than one test.
  9. In some poker games, players get seven cards each, and they form a hand with the best five of the seven. Modify your program to generate seven-card hands and recompute the probabilities.
السابقالفهرسالتالي