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

الفصل 11

اصنع كائناتك الخاصة

 

11.1 تعاريف الأصناف وأنواع الكائنات

في كل مرة تكتب فيها تعريف صنف جديد، يتم إنشاء نوع كائني جديد له نفس اسم الصنف. بعيداً جداً في القسم 1.5، عندما عرفنا الصنف Hello، تم إنشاء نوع كائني جديد اسمه Hello. لم نقم بإنشاء أية متغيرات من نوع Hello، ولم نستعمل الأمر new لإنشاء أية متغيرات من صنف Hello، لكن ذلك كان ممكناً!

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

إليك أهم أفكار هذا الفصل:

إليك بعض مشاكل البنية اللغوية المتعلقة بتعاريف الأصناف:

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

11.2 الوقت

من الدوافع الشائعة لإنشاء نوع كائني جديد هو تغليف البيانات ذات الصلة معاً في كائن يمكن التعامل معه كعنصر واحد. لقد رأينا نوعين مشابهين لذلك، Point وRectangle.

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

الخطوة الأولى هي تحديد نوع كل متغير. من الواضح أن hour وminute يجب أن يكونا integers. ولإضافة بعض التشويق إلى العمل، دعنا نجعل متغير الثواني second من النوع double.

يتم التصريح عن متغيرات الحالة في بداية تعريف الصنف، خارج أي تعريف لأية عملية، هكذا:

class Time {
   int hour, minute;
   double second;
}
إن قطعة الشفرة هذه لوحدها، تشكل تعريفاً مشروعاً للصنف. مخطط الحالة (state diagram) لكائن Time يبدو كهذا:

بعد التصريح عن متغيرات الحالة، الخطوة التالية هي تعريف بانِ للصنف الجديد.

11.3 العمليات البانية

تهيئ العمليات البانية متغيراتَ الحالة. بنية الباني مشابهة لبنية أي عملية أخرى، ما عدا ثلاثة اختلافات:

إليك مثالاً عن عملية بانية للصنف Time:

public Time() {
   this.hour = 0;
   this.minute = 0;
   this.second = 0.0;
}
في الموقع الذي تتوقع فيه رؤية نوع الإرجاع، بين Public وTime، لا يوجد شيء. وهذا هو السبب الذي يمكننا (ويمكن المجمع) من معرفة أن هذه العملية هي عملية بانية.

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

لكن لا يمكنك التصريح عن this ولا يمكنك إسناد أي شيء له. يتم إنشاء this بوساطة النظام؛ كل ما عليك فعله هو تهيئة متغيرات الحالة الخاصة به.

من الأخطاء الشائعة عند كتابة العمليات البانية وضع تعليمة return عند النهاية. قاوم العادة.

11.4 المزيد من البناء

يمكن عمل تحميل زائد للعمليات البانية، مثل العمليات الأخرى تماماً، ما يعني أنك تستطيع كتابة عدة عمليات بانية لكل منها معاملات مختلفة. ستعرف Java أي عملية يجب استدعاؤها بمطابقة متحولات new مع معاملات العمليات البانية.

من الشائع وجود بانٍ لا يأخذ أية متحولات (المبين أعلاه)، وآخر يأخذ عدد من معاملات تطابق (بالعدد والنوع) متغيرات الحالة. مثلاً:

public Time(int hour, int minute, double second) {
   this.hour = hour;
   this.minute = minute;
   this.second = second;
}
تطابق أسماء وأنواع المعاملات أسماء وأنواع متغيرات الحالة. كل ما يفعله الباني هو نسخ المعلومات من المعاملات إلى متغيرات الحالة.

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

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

11.5 صناعة كائن جديد

بالرغم من أن العمليات البانية تبدو مثل العمليات العادية، إلا أنك لا تستدعيها بشكل مباشر أبداً. بدلاً من ذلك، تستدعي new، ويحجز النظام مساحة للكائن الجديد وبعدها يستدعي الباني.

يوضح البرنامج التالي طريقتين لصنع وتهيئة كائن Time:

class Time {
   int hour, minute;
   double second;

   public Time() {
      this.hour = 0;
      this.minute = 0;
      this.second = 0.0;
   }

   public Time(int hour, int minute, double second) {
      this.hour = hour;
      this.minute = minute;
      this.second = second;
   }

   public static void main(String[] args) {
      // one way to create and initialize a Time object
      Time t1 = new Time();
      t1.hour = 11;
      t1.minute = 8;
      t1.second = 3.14159;
      System.out.println(t1);

      // another way to do the same thing
      Time t2 = new Time(11, 8, 3.14159);
      System.out.println(t2);
   }
}
في main، أول مرة نستدعي new، لا نعطيه أية متحولات، لذا تستدعي Java الباني الأول. تسند الأسطر القليلة التالية قيماً لمتغيرات الحالة.

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

11.6 طباعة الكائنات

إن خرج البرنامج السابق هو:

[email protected]
[email protected]
عندما تطبع Java قيمة نوع كائني معرف بوساطة المستخدم، تطبع اسم النوع ورمز ست عشري خاص (عدد أساسه 16) فريد ولا يمكن أن يكون مكرراً لكائنين. لا معنى لهذا الرمز في حد ذاته؛ في الحقيقة، يمكن أن يتغير هذا الرمز بين جهاز وآخر بل حتى بين تشغيل وآخر. لكن يمكن أن يكون مفيداً عند تصحيح الأخطاء، في حال كنت ترغب بمتابعة مسار الكائنات المنفردة.

لطباعة الكائنات بطريقة ذات معنى للمستخدمين (ما يقابل المبرمجين)، يمكنك كتابة عملية تدعى شيئاً ما مثل printTime:

public static void printTime(Time t) {
   System.out.println(t.hour + ":" + t.minute + ":" + t.second);
}
قارن هذه العملية بنسخة printTime الموجودة في القسم 3.10.

إن خرج هذه العملية هو 11:8:3.14159، سواء مررنا t1 أو t2 كمتحول لها. ومع أننا نستطيع معرفة أن هذا الشكل يمثل الوقت، إلا أنه ليس مكتوباً بصيغة صحيحة. مثلاً، إذا كان عدد الدقائق أو الثواني أقل من 10، فيفترض وجود 0 على يسار الرقم لتملأ الفراغ. قد نرغب أيضاً بإهمال الجزء العشري من الثواني. أي أننا نريد شيئاً مثل 11:08:03.

في معظم لغات البرمجة، توجد أساليب بسيطة للتحكم بتنسيق طباعة الأرقام. أما في Java فلا توجد طرق بسيطة.

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

11.7 العمليات على الكائنات

في الأقسام القليلة المقبلة سأشرح ثلاثة أنواع من العمليات التي تشتغل على الكائنات:

التابع المجرد (pure function): يأخذ كائنات كمتحولات لكنه لا يعدلها. قيمته المعادة إما أن تكون قيمة بسيطة أو كائن جديد تم إنشاؤه داخل العملية.

عملية التعديل (modifier): يأخذ كائنات كمتحولات ويعدل على بعض منها أو كلها. غالباً ما يكون نوع إرجاعه void.

عملية التعبئة (fill-in method): يكون أحد متحولاتها كائن "فارغ" تعبئه العملية. عملياً، هذه العمليات هي نوع من عمليات التعديل.

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

11.8 التوابع المجردة

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

من الأمثلة على التوابع المجردة isAfter، التي تقارن بين زمنين (كائنين من الصنف Time) وتعيد قيمة بوليانية تبين فيما إذا كان المعامل الأول يتأخر عن الثاني أو لا:

public static boolean isAfter(Time time1, Time time2) {
   if (time1.hour > time2.hour) return true;
   if (time1.hour < time2.hour) return false;
   if (time1.minute > time2.minute) return true;
   if (time1.minute < time2.minute) return false;
   if (time1.second > time2.second) return true;
   return false;
}
ما هي نتيجة هذه العملية لو كان الوقتان متساويين؟ هل تبدو تلك النتيجة ملائمة لهذه العملية؟ لو كنت تكتب وثائق هذه العملية، فهل كنت لتذكر تلك الحالة بشكل محدد؟

ومن الأمثلة الأخرى addTime، التي تحسب مجموع زمنين. مثلاً، إذا كانت الساعة 9:14:30، وكانت آلة تحميص الخبز عندك تستغرق 3 ساعات و35 دقيقة، يمكنك استخدام addTime لمعرفة الوقت الذي سيجهز فيه الخبز.

إليك مسودة أولية لهذه العملية وهي ليست مضبوطة تماماً:

public static Time addTime(Time t1, Time t2) {
   Time sum = new Time();
   sum.hour = t1.hour + t2.hour;
   sum.minute = t1.minute + t2.minute;
   sum.second = t1.second + t2.second;
   return sum;
}
بما أن هذه العملية تعيد كائن Time، فقد تظن أنها عملية بناء، إلا أنها ليست كذلك. عليك العودة والتحقق من بنية العمليات العادية (مثل هذه) والعمليات البانية، لأنك قد تخلط بينهما بسهولة.

إليك مثالاً عن كيفية استعمال هذه العملية. إذا احتوى currentTime على الوقت الحالي وbreadTime على الزمن اللازم لتحميص الخبز، فعندئذ يمكنك استعمال addTime لمعرفة الوقت الذي سيكون الخبز فيه جاهزاً.

Time currentTime = new Time(9, 14, 30.0);
Time breadTime = new Time(3, 35, 0.0);
Time doneTime = addTime(currentTime, breadTime);
printTime(doneTime);
إن خرج هذا البرنامج هو 12:49:30ز0، وهو صحيح. من ناحية أخرى، توجد حالات تكون فيها الإجابة خاطئة. هل يمكنك التفكير في واحدة منها؟

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

ها هي النسخة الصحيحة من العملية.

public static Time addTime(Time t1, Time t2) {
  Time sum = new Time();
  sum.hour = t1.hour + t2.hour;
  sum.minute = t1.minute + t2.minute;
  sum.second = t1.second + t2.second;
  if (sum.second >= 60.0) {
    sum.second -= 60.0;
    sum.minute += 1;
  }
  if (sum.minute >= 60) {
    sum.minute -= 60;
    sum.hour += 1;
  }
  return sum;
}
ومع أنها صحيحة، إلا أنها بدأت تكبر. سأقترح بدائل أقصر لها لاحقاً.

تشرح هذه الشفرة عمليتين لم نرهما من قبل، وهما =+ و=-. هذان العاملان يوفران طريقة مختصرة لزيادة أو إنقاص المتغيرات. وهما يشبهان ++ و-- في عملهما، عدا (1) يمكن أن يعملا على الأعداد العشرية بالإضافة إلى الأعداد الصحيحة، و(2) ليس بالضرورة أن تكون الزيادة أو النقصان بمقدار 1. التعليمة sum.second -= 60.0; مكافئة للتعليمة sum.second = sum.second – 60.0;.

11.9 عمليات التعديل

لنأخذ increment، كمثال عن عملية تعديل، التي تضيف عدد معطى من الثواني إلى كائن Time. المسودة الأولية لهذه العملية ستبدو كهذه:

public static void increment(Time time, double secs) {
  time.second += secs;
  if (time.second >= 60.0) {
    time.second -= 60.0;
    time.minute += 1;
  }
  if (time.minute >= 60) {
    time.minute -= 60;
    time.hour += 1;
  }
}
يجري السطر الأول العملية الأساسية؛ وتتم معالجة الباقي كما فعلنا في الحالات التي مرت معنا سابقاً.

هل هذه العملية صحيحة؟ ماذا يحدث لو أن المتحول كان أكبر بكثير من 60؟ في تلك الحالة لن يكون طرح 60 مرة واحدة كافياً؛ علينا الاستمرار في الطرح حتى تصبح قيمة second أقل من 60. يمكننا عمل ذلك باستبدال تعليمة if بتعليمة while:

public static void increment(Time time, double secs) {
   time.second += secs;
   while (time.second >= 60.0) {
     time.second -= 60.0;
     time.minute += 1;
   }
   while (time.minute >= 60) {
     time.minute -= 60;
     time.hour += 1;
   }
}
هذا الحل صحيح، لكنه غير فعال جداً. هل يمكنك التفكير بحل لا يتطلب التكرار؟

11.10 عمليات التعبئة

بدلاً من إنشاء كائن جديد في كل مرة يستدعى فيها addTime، يمكننا أن نطلب من المستدعي أن يوفر كائن تخزن addTime النتيجة فيه. قارن هذه النسخة مع النسخة السابقة:

public static void addTimeFill(Time t1, Time t2, Time sum) {
   sum.hour = t1.hour + t2.hour;
   sum.minute = t1.minute + t2.minute;
   sum.second = t1.second + t2.second;
   if (sum.second >= 60.0) {
     sum.second -= 60.0;
     sum.minute += 1;
   }
   if (sum.minute >= 60) {
     sum.minute -= 60;
     sum.hour += 1;
   }
}
يتم تخزين النتيجة في sum، لذلك يكون نوع الإرجاع void.

عمليات التعديل وعمليات التعبئة فعالة بسبب عدم الحاجة لصنع كائنات جديدة. لكنها تصعب فصل أجزاء البرنامج؛ في المشاريع الكبيرة يمكن أن تتسبب هذه العمليات بأخطاء يصعب العثور عليها.

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

أنا أنصحك بأن تكتب توابع مجردة طالما أن ذلك ممكن، وأن تلجأ إلى عمليات التعديل في حال وجود مزايا ضرورية فقط.

11.11 التطوير والتخطيط التصاعدي

لقد اعتمدت في هذا الفصل على أسلوب في تطوير البرامج يدعى النمذجة الأولية السريعة (rapid prototyping)(1). في كل مرة، كتبت مسودة أولية للعملية تجري الحسابات الأساسية، بعد ذلك اختبرتها على عدة حالات، وصححت الأخطاء التي وجدتها.

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

من الأساليب البديلة هو البحث عن فكرة في تلك المشكلة تجعل البرمجة أسهل. في هذه الحالة الفكرة هي أن الوقت في الحقيقة هو عدد مؤلف من ثلاث خانات في الأساس الستيني! الثواني هي خانة "الآحاد"، والدقائق هي "الستينات"، والساعات هي خانة "3600".

عندما كتبنا addTime وincrement، كنا نجري عملية الجمع في النظام الستيني في الحقيقة، ولذلك اضطررنا إلى "الحمل" من عمود إلى تاليه.

من الأساليب الأخرى للتعامل مع المشكلة ككل هو تحويل الأوقات إلى أعداد عشرية والاستفادة من قدرة الحواسيب على تنفيذ العمليات الرياضية على الأعداد العشرية. ها هي العملية التي تحول كائن Time إلى عدد من نوع double:

public static double convertToSeconds(Time t) {
   int minutes = t.hour * 60 + t.minute;
   double seconds = minutes * 60 + t.second;
   return seconds;
}
كل ما نحتاجه الآن هو التحويل من double إلى كائن Time. يمكننا كتابة عملية تجري بذلك، لكن كتابتها كعملية بانية ثالثة تبدو منطقية أكثر:
public Time(double secs) {
   this.hour =(int)(secs / 3600.0);
   secs -= this.hour * 3600.0;
   this.minute =(int)(secs / 60.0);
   secs -= this.minute * 60;
   this.second = secs;
}
هذا الباني يختلف قليلاً عن سابقيه؛ فهو يشتمل على حسابات بالإضافة إلى تعليمات الإسناد إلى متغيرات الحالة.

قد تحتاج للتفكير قبل أن تقنع نفسك بأن التقنية التي أستخدمها للتحويل من أساس إلى آخر صحيحة. وبمجرد أن تقتنع، سنكون جاهزين لاستعمال هذه العمليات لكتابة addTime من جديد:

public static Time addTime(Time t1, Time t2) {
   double seconds = convertToSeconds(t1) + convertToSeconds(t2);
   return new Time(seconds);
}
هذه النسخة أقصر من الأصلية، ومن الأسهل بكثير معرفة أنها ستعمل بشكل صحيح (بفرض – كالعادة – أن العمليات التي تستدعيها صحيحة).

كتمرين، أعد كتابة increment بنفس الطريقة.

11.12 التعميم

في بعض الأحيان يكون التحويل من الأساس 60 إلى الأساس 10 وبالعكس أصعب من التعامل مع الوقت وحسب. التحويل بين الأسس أكثر تجريداً؛ عند التعامل مع الوقت فإن إدراكنا البديهي يكون أفضل.

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

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

ومن السخرية أن جعل المشكلة أصعب (جعلها مشكلة عامة) يجعلها أسهل للحل أحياناً (عدد أقل من الحالات الخاصة، احتمالات أقل للخطأ).

11.13 الخوارزميات

عندما تكتب حلاً عاماً لفئة من المشاكل، بدلاً من حل خاص لمشكلة واحدة، فأنت تكتب خوارزمية (algorithm). إن تعريف هذه الكلمة ليس سهلاً، لذا سأجرب أسلوبين للتعريف.

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

لكن لو كنت "كسولاً"، فعلى الأغلب أنك تعلمت بعض الحيل. مثلاً، لإيجاد ناتج ضرب n ب9، يمكنك كتابة n-1 في الخانة الأولى و10-n في الخانة الثانية. هذه الحيلة هي حل عام لضرب أي عدد مؤلف من خانة واحدة بالعدد 9. هذه الحيلة هي خوارزمية!

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

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

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

قريباً ستملك القدرة على تصميم خوارزميات بسيطة لمجموعة متنوعة من المشاكل.

11.14 المصطلحات

صنف: سابقاً، عرّفتُ الصنف على أنه مجموعة من العمليات المترابطة. في هذا الفصل تعلمنا أن تعريف الصنف هو قالب لنوع كائنات جديد أيضاً.

حالة: عضو ينتمي لصنف ما. كل كائن هو حالة من صنف ما.

الباني: عملية خاصة تهيئ متغيرات الحالة للكائنات المنشأة حديثاً.

الصنف البادئ: الصنف الذي يحتوي على عملية main التي يبدأ تنفيذ البرنامج عندها.

تابع مجرد: عملية تعتمد نتيجتها على معاملاتها فقط، وليس لها أي تأثيرات سوى إرجاع قيمة.

عملية تعديل: عملية تغير كائناً واحداً أو أكثر من الكائنات التي تأخذها كمعاملات، وعادة تعيد void.

عمليات التعبئة: نوع من العمليات التي تأخذ كائناً "فارغاً" كمعامل وتملأ متغيرات الحالة الخاصة به بدلاً من توليد قيمة معادة.

خوارزمية: مجموعة من التعليمات المستخدمة لحل فئة من المشاكل بعملية ميكانيكية.

class: Previously, I defined a class as a collection of related methods. In this chapter we learned that a class definition is also a template for a new type of object.

instance: A member of a class. Every object is an instance of some class. constructor: A special method that initializes the instance variables of a newly-constructed object.

startup class: The class that contains the main method where execution of the program begins.

pure function: A method whose result depends only on its parameters, and that has no side-effects other than returning a value.

modifier: A method that changes one or more of the objects it receives as parameters, and usually returns void.

fill-in method: A type of method that takes an “empty” object as a parameter and fills in its instance variables instead of generating a return value.

algorithm: A set of instructions for solving a class of problems by a mechanical process.

11.15 تمرينات

تمرين 11.1

في لعبة الكلمات "سكرابل" (2)(Scrabble)، كل قطعة تحتوي على حرف، يستعمل لتركيب الكلمات، ورقماً، يستخدم عند حساب النقاط الموافقة لقيمة الكلمة.

  1. اكتب تعريفاً لصنف اسمه Tile يعبر عن قطع سكرابل. متغيرات الحالة يجب أن يكونا محرفاً يدعى letter وعدداً صحيحاً يدعى value.
  2. اكتب عملية بناء تأخذ معاملات اسمها letter وvalue وتهيئ متغيرات الحالة.
  3. اكتب عملية اسمها printTile تأخذ كائن Tile كمعامل وتطبع متغيرات الحالة بصيغة سهلة القراءة.
  4. اكتب عملية اسمها testTile تنشئ كائن Tile له الحرف Z والقيمة 10، ثم تستخدم printTile لطباعة حالة الكائن.

إن الغرض من هذا التمرين هو التدرب على الجزء الميكانيكي من إنشاء تعريف لصنف جديد وشفرة تختبره.

تمرين 11.2

اكتب تعريفاً للصنف Date، وهو نوع كائني يحوي ثلاثة أعداد صحيحة، year، month، وday. يجب أن يحوي هذا الصنف عمليتي بناء. الأولى لا تأخذ أية معاملات. أما الثانية فيجب أن تأخذ ثلاثة معاملات year، month، وday وتستخدمها لتهيئة متغيرات الحالة.

اكتب عملية main تنشئ كائن Date جديد باسم birthday. يجب أن يحتوي الكائن الجديد على تاريخ ميلادك. يمكنك استخدام أي من عمليتي البناء.

تمرين 11.3

العدد الكسري هو عدد يمكن تمثيله بكسر يكون حديه عددين صحيحين. مثلاً، 2/3 عدد كسري، ويمكنك اعتبار 7 عدد كسري بما أن مقامه 1. في هذه الوظيفة، سوف تكتب تعريف صنف للأعداد الكسرية.

  1. أنشئ برنامجاً جديداً يدعى Rational.java يعرف صنفاً باسم Rational. يجب أن يحتوي كائن Rational على متغيري حالة من النوع int لتخزين البسط والمقام.
  2. اكتب عملية بانية لا تأخذ أية متحولات وتعطي المتغيرين قيمة الصفر.
  3. اكتب عملية تدعى printRational تأخذ كائن Rational كمتحول وتطبعه في صيغة مناسبة.
  4. اكتب عملية main تنشئ كائناً جديداً من النوع Rational، وتعطي قيماً ما لمتغيري الحالة الخاصين به، وتطبع ذلك الكائن.
  5. في هذه المرحلة، ستملك برنامجاً مصغراً قابلاً للاختبار. اختبره و –في حال دعت الحاجة- صحح الأخطاء.
  6. اكتب عملية بانية أخرى للصنف تأخذ متحولين وتستخدمهم لتهيئة متغيرات الحالة.
  7. اكتب عملية باسم negate تعكس إشارة العدد الكسري. يجب أن تكون هذه العملية معدّلة، لذلك يجب أن يكون نوع إرجاعها void. أضف بعض السطور إلى main لاختبار العملية الجديدة.
  8. اكتب عملية باسم invert تقلب العدد بالتبديل بين بسطه ومقامه. أضف سطوراً إلى main لاختبار العملية الجديدة.
  9. اكتب عملية تدعى toDouble تحول العدد الكسري إلى عدد عشري (عدد ذو فاصلة) وتعيد النتيجة. هذه العملية هي تابع مجرد؛ فهي لا تعدل على الكائن. كما هو الحال دائماً، اختبر العملية الجديدة.
  10. اكتب عملية معدّلة باسم reduce تختزل العدد الكسري إلى أبسط شكل له وذلك بحساب القاسم المشترك الأكبر (GCD) للبسط والمقام وتقسيمهما عليه. يجب أن تكون هذه العملية تابعاً مجرداً؛ يجب ألا تعدل متغيرات الحالة للكائن الذي استدعيت العملية عليه. لحساب GCD، انظر التمرين 6.10.
  11. اكتب عملية باسم add تأخذ عدد كسريين كمتحولات وتعيد كائناً جديداً من نوع Relational. يجب أن يحتوي الكائن المعاد على مجموع المتحولين.

    توجد عدة طرق لجمع الكسور. يمكنك استخدام أي منها، لكن عليك التأكد من اختزال نتيجة العملية بحيث لا يوجد للبسط والمقام قواسم مشتركة (ما عدا 1).

إن الغرض من هذا التمرين هو كتابة تعريف صنف يتضمن مجموعة من العمليات المتنوعة. بما في ذلك عمليات بناء ومعدلات وتوابع مجردة.

(1)ما أدعوه بالنمذجة الأولية السريعة rapid prototyping مشابه للتطوير الموجه بالاختبار test-driven development (TDD)؛ الفرق بينهما هو أن TDD يقوم عادة على أساس الاختبار المؤتمت. انظر http://en.wikipedia.org/wiki/Test-driven_development.

(2) Scrabble هي علامة تجارية مسجلة تملكها Hasbro, Inc. في الولايات المتحدة الأمريكية وكندا، وفي بقية العالم J.W. Spear & Sons Limited of Maidenhead, Berkshire, England، فرع من شركة Mattel, Inc..

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