اذهب إلى المحتوى

البرمجة في جافا باستخدام الكائنات (Objects)


رضوى العربي

يُمكِن تطبيق المفاهيم كائنية التوجه (object-oriented) على عملية تصميم البرامج وكتابتها في العموم بأكثر من طريقة، منها التحليل والتصميم كائني التوجه (object-oriented analysis and design). تُطبِق تلك الطريقة الأساليب كائنية التوجه (object-oriented) على أُولَى مراحل تطوير البرمجيات، والمسئولة عن تصميم البرنامج ككل. تَتَلخَّص تلك الطريقة بتَحْديد مجموعة الكيانات المُرتَبِطة بموضوع المشكلة (problem domain)، والتي يُمكِن تمثيلها ككائنات (objects). على مستوى آخر، تُشجِع البرمجة كائنية التوجه (object-oriented programming) المبرمجين على إنشاء "أدوات برمجية مُعمَّمة" قابلة للاِستخدَام بالعديد من المشروعات البرمجية المختلفة.

سنَستخدِم "الأدوات البرمجية المُعمَّمة" بصورة أكبر حين نبدأ باِستخدَام الأصناف القياسية (standard classes) بالجافا. سنبدأ هذا القسم بفَحْص بعضًا من تلك الأصناف المَبنية مُسْبَقًا (built-in)، والمُستخدَمة لإنشاء أنواع معينة من الكائنات (objects)، وسنعود مُجدّدًا إلى الصورة العامة بنهاية القسم.

بعض الأصناف المبنية مسبقًا

بالبرمجة كائنية التوجه (object-oriented programming)، يَكُون التركيز عادةً على تصميم أصناف جديدة، وكتابة تَّنْفيذها (implementation)، ولكن يَنبغي أيضًا ألا نُهمِل ذلك العدد الكبير من الأصناف (classes) التي وفرها لنا مُصمّمي الجافا. يُمكِننا أن نَستخدِم بعضًا من تلك الأصناف لإنتاج أصناف جديدة، بينما قد نَستخدِم البعض الآخر مُباشرة لإِنشاء كائنات. لكي تَتَمكَّن فعليًا من لغة الجافا، ينبغي أن تَكُون على دراية بعدد كبير من تلك الأصناف المَبنية مُسْبَقًا (built-in classes)، وهو ما يَحتاج إلى الكثير من الوقت والخبرة لتطويره. سنَفْحَص خلال هذا القسم بعضًا من تلك الأصناف.

يُمكِننا أن نُنشِئ سِلسِلة نصية (string) من نصوص أصغر باِستخدَام العَامِل +، ولكن لا يَكُون ذلك دائمًا هو الحل الأفضل. فمثلًا، إذا كان str مُتْغيِّرًا من النوع String و ch محرفًا (character)، فإن تّنْفيذ الأمر str = str + ch;‎ يَتَضمَّن إنشاء كائن جديد كليًا من النوع String، مُكوَّن من نُسخة من str مع قيمة ch مُلحقَّة بآخره. يَستغرِق نَسخ السِلسِلة النصية بعض الوقت كما يَتَطلَّب قدرًا كبيرًا من المعالجة. في المقابل، يَسمَح الصنف StringBuilder بإنشاء سِلسِلة نصية طويلة من نصوص أصغر بكفاءة. يُمكِننا أن نُنشِئ كائنًا ينتمي إلى الصنف StringBuilder كالتالي:

StringBuilder builder = new StringBuilder();

تُصرِّح تلك التَعْليمَة عن المُتْغيِّر builder، وتُهيئه مبدئيًا، بحيث يُشير إلى كائن من الصَنْف StringBuilder. بالقسم الفرعي ٤.٨.١، ناقشنا دمج عمليتي التّصريح (declaration) والتهيئة المبدئية (initialization) ضِمْن تَعْليمَة واحدة فيما يَتعَلَّق بالأنواع الأساسية (primitive types)، ولكن الأمر هو نفسه بالنسبة للكائنات.

تمامًا كأي كائن من الصَنْف String، تَتَكوَّن كائنات الصَنْف StringBuilder من مُتتالية من المحارف. ولكن بإمكانك إلحاق محارف جديدة بدون إنشاء نُسخ جديدة من نفس البيانات التي تَتَضمَّنها تلك الكائنات بالفعل. إذا كانت x عبارة عن قيمة من أيّ نوع، وكان builder هو المُتْغيِّر المُعرَّف بالأعلى، فإن الأمر builder.append(x)‎ سيُضيف تمثيل x النصي (string representation) إلى نهاية البيانات الموجودة بالمُتْغيِّر builder، وهو ما يُعدّ أكثر كفاءة عمومًا من الاستمرار بنَسْخ البيانات مع كل مرة نُلحِق خلالها شيئًا جديدًا. تستطيع إنشاء سِلسِلة نصية طويلة باِستخدَام كائن من الصَنْف StringBuilder، وعبر مُتتالية من أوامر append()‎، وعندما تَكتمِل السِلسِلة النصية، ستُعيد الدالة builder.toString()‎ نُسخة من السِلسِلة النصية كقيمة عادية من النوع String. يَتوفَّر الصَنْف StringBuilder ضِمْن الحزمة (package) القياسية java.lang، ولذلك يُمكِن اِستخدَام الاسم البسيط للصَنْف بدون استيراده (import).

تَحتوِي حزمة java.util على عدد من الأصناف المفيدة، فمثلًا، تَتَوفَّر أصناف للتَعامُل مع تجميعات (collections) من الكائنات، والتي سنتناولها تفصيليًا بالفصل العاشر. تَعرَّضنا للصَنْف java.util.Scanner المُعرَّف ضِمْن نفس الحزمة بالقسم الفرعي ٢.٤.٦. يَتوفَّر أيضًا الصَنْف java.util.Date، والذي يُستخدَم لأغراض تمثيل الوقت، فمثلًا، عندما تُنشِئ كائنًا (object) من النوع Date بدون مُعامِلات (parameters)، تُمثِل النتيجة كُلًا من التاريخ والوقت الحالي. يُمكِن عَرْض تلك المعلومة كالتالي:

System.out.println( new Date() );

لمّا كان الصَنْف Date مُعرَّفًا بحزمة java.util، كان لابُدّ من إِتاحته أولًا بالبرنامج عن طريق استيراده، وذلك بكتابة أي من التَعْليمَتين import java.util.Date;‎ أو import java.util.*;‎ ببداية البرنامج، وبَعْدها ستَتَمكَّن من اِستخدَام الصَنْف. (ناقشنا الحزم والاستيراد بالقسم الفرعي ٤.٦.٣).

لنَفْحَص أيضًا الصَنْف java.util.Random. تُعدّ كائنات ذلك الصَنْف مصدرًا للأعداد العشوائية، فيُمكِن لكائن من النوع Random أن يُنتج أعدادًا عشوائية سواء كانت صحيحة (integers) أو حقيقية (real). وفي الحقيقة، تَستخدِم الدالة القياسية Math.random()‎ كائنًا من ذلك الصَنْف وراء الكواليس؛ لإنتاج أعدادها العشوائية. اُنظر الشيفرة التالية لإنشاء كائن من الصَنْف Random:

Random randGen = new Random();

بفَرْض أن N هو عدد صحيح موجب، سيُنتج randGen.nextInt(N)‎ عددًا صحيحًا عشوائيًا ضِمْن نطاق يتراوح من صفر وحتى N-1. يُمكِننا استخدام ذلك بتجربة رمِي حجري النَّرد لتَسهيل المُهِمّة؛ فبدلًا من كتابة die1 = (int)(6*Math.random())+1;‎، يُمكِننا كتابة die1 = randGen.nextInt(6)+1;‎. قد لا تتفق على أن تلك الطريقة أسهل نوعًا ما خاصة مع اضطرارنا لاستيراد (import) الصَنْف java.util.Random، وإنشاء كائن منه. يُمكِننا أيضًا اِستخدَام كائن من النوع Random لإنتاج ما يُعرَف باسم "التوزيع الاحتمالي الغاوسي (gaussian distribution)".

تُستخدَم الكثير من أصناف جافا القياسية ببرمجة واجهات المُستخدِم الرسومية (GUI)، وسنَمُرّ عبر الكثير منها بالفصل السادس، ولكن سنَمُرّ هنا سريعًا على الصَنْف Color من حِزمة javafx.scene.paint حتى نَستخدِمه بالمثال التالي. يُمثِل أي كائن من الصَنْف Color لونًا يُستخدَم أثناء الرسم، ولقد تَعرَّضنا بالفعل لعدة ألوان ثابتة (constants) مثل Color.RED بالقسم ٣.٩. في الواقع، تلك الثوابت ما هي إلا مُتْغيِّرات أعضاء ساكنة (static) ونهائية (final) مُعرَّفة بالصَنْف Color، وقيمها عبارة عن كائنات من النوع Color. بالإضافة إلى تلك الألوان المُعرَّفة مُسْبَقًا، يُوفِّر الصَنْف Color مجموعة من البَوانِي (constructors) تَسمَح لك بإنشاء كائنات جديدة من الصَنْف لتَمثيل أيّ لون آخر. يَستقبِل إحداها ٣ مُعامِلات (parameters) من النوع double ويُمكِنك استدعائه باِستخدَام new Color(r,g,b)‎. تُمثِل تلك المُعامِلات كُلًا من اللون الأحمر والأخضر والأزرق بنظام الألوان RGB، ويَنبغي أن تَقع قيمها ضِمْن نطاق يتراوح من ٠ وحتى ١. تَعنِي القيمة ٠ للمُعامِل r أن اللون الفعليّ لا يَتَضمَّن اللون الأحمر نهائيًا، بينما تَعنِي القيمة ١ أنه يَتَضمَّن أكبر قدر مُمكن من اللون الأحمر. يَتوفَّر باني آخر يُستدعَى على الصورة new Color(r,g,b,t)‎، والذي يَستقبِل مُعامِلًا إضافيًا من النوع double، ويَنبغي أن تَقع قيمته ضِمْن نطاق يتراوح من ٠ وحتى ١. يُحدِّد ذلك المعامل درجة شفافية اللون (transparency)، بحيث تُمثِل القيم الأكبر من المُعامِل t لونًا أقل شفافية، فمثلًا، عندما تَرسم بلون شفاف جزئيًا، تَظهَر الخلفية عبر اللون إلى حد معين.

.تَتَضمَّن الكائنات من الصَنْف Color عددًا قليلًا من توابع النسخ (instance methods). على سبيل المثال، تَتَوفَّر دوال (functions) مثل getRed()‎ لجَلْب قيمة اللون الأحمر بنظام RGB، وبالمثل للون الأخضر والأزرق. مع ذلك، لا يُوفِّر الصَنْف أي توابع ضَبْط (setter methods) لتَعْديل قيم تلك الألوان، حيث تُعدّ الكائنات من الصَنْف Color كائنات ثابتة أو غَيْر قابلة للتعديل (immutable)، بمعنى أن جميع مُتْغيِّرات النُسخ (instance variables) المُعرَّفة بداخلها هي نهائية -مُعرَّفة باِستخدَام المُبدِّل final-، وبالتالي لا يُمكِن تَعْديلها بَعْد إنشاء الكائن. لاحِظ أن السَلاسِل النصية من النوع String هي مثال آخر على الكائنات غَيْر القابلة للتعديل (immutable).

النقطة الرئيسية مما سبق هو التأكيد على أن أصناف جافا القياسية تُوفِّر حلولًا للكثير من المشاكل التي قد تواجهها، لذا إذا تَعرَّضت لمُهِمّة (task) شائعة نوعًا ما، فلرُبما من الأفضل أن تَبحَث قليلًا بمَرجِع الجافا، لتَرى ما إذا كان هناك صَنْف يؤدي الغرض الذي تحتاج إليه.

الصنف Object

تُعدّ القدرة على إنشاء أصناف فرعية (subclasses) مُشتقَّة من صَنْف واحدة من أهم سمات البرمجة كائنية التوجه (object-oriented programming). يَرِث (inherit) الصَنْف الفرعي جميع خاصيات (properties) الصَنْف الأصلي وسلوكياته (behaviors)، ولكن بإِمكانه تَعْديلها وكذلك الإضافة إليها. ستَتَعلَّم طريقة إنشاء الأصناف الفرعية (subclasses) في القسم ٥.٥. في الواقع، أيّ صَنْف بلغة الجافا -باستثناء صَنْف وحيد- هو بالنهاية صَنْف فرعي من صَنْف آخر، فحتى لو أنشأت صَنْفًا، ولم تُصرِّح عن كَوْنه صَنْفًا فرعيًا من صَنْف آخر، فإنه تلقائيًا يُصبِح صنفًا فرعيًا من صَنْف خاص اسمه Object مُعرَّف بحزمة java.lang، وهو الصَنْف الاستثنائي الوحيد الذي لا يُعدّ صَنْفًا فرعيًا من أي صَنْف آخر.

يُعرِّف الصنف Object مجموعة من توابع النُسخ (instance methods)، والتي تَرثها (inherit) جميع الأصناف الآخرى، لذا يُمكِن لأيّ كائن (object) مهما كان أن يَستخدِم تلك التوابع. سنَذكُر واحدة منها فقط بهذا القسم، وسنَتَعرَّض لمجموعة آخرى منها لاحقًا.

يُعيد تابع النسخة toString()‎ المُعرَّف بالصنف Object قيمة من النوع String، والتي يُفْترَض أن تَكُون بمثابة تمثيلًا نصيًا (string representation) للكائن. في أي مرة نَطبَع فيها كائنًا أو نَضُمّه (concatenate) إلى سِلسِلة نصية، أو بصورة أعم نَستخدِمه بسياق يَتطلَّب سِلسِلة نصية، يُستدعَى هذا التابع ضمنيًا ليُحوِّل ذلك الكائن تلقائيًا إلى النوع String.

تُعيد نسخة toString المُعرَّفة بالصنف Object اسم الصنف الذي ينتمي إليه الكائن مع ترميز التجزئة (hash code) الخاص به، وهو ما قد لا يَكُون مفيدًا. لذا عندما تُنشِئ صَنْفًا، تستطيع إعادة تعريف التابع toString()‎ بحيث تَحلّ النسخة الجديدة مَحلّ النسخة الموروثة، فمثلًا، يُمكِننا إضافة التابع (method) التالي لأيّ من أصناف PairOfDice المُعرَّفة بالقسم السابق:

/**
 * يعيد تمثيلًا نصيًا لحجري نرد
 */
public String toString() {
   if (die1 == die2)
      return "double " + die1;
   else
      return die1 + " and " + die2;
}

إذا كان dice مُتْغيِّرًا يُشير إلى كائن من الصَنْف PairOfDice، فسيُعيد dice.toString()‎ سَلاسِل نصية مثل "‎3 and 6" و "‎5 and 1" و "double 2‎" بِحَسْب الأعداد الظاهرة على حجري النَّرد. يُستدَعى هذا التابع تلقائيًا لتَحْوِيل المُتْغيِّر dice إلى النوع String بالتَعْليمَة التالية:

System.out.println( "The dice came up " + dice );

قد يَكُون خَرْج التعليمة -بالأعلى- "The dice came up 5 and 1" أو "The dice came up double 2". سنَتَعرَّض لمثال آخر للتابع toString()‎ بالقسم التالي.

كتابة صنف واستخدامه

سنَكْتُب الآن برنامج تَحرِيكة (animation)، بالاعتماد على نفس المنصة المُستخدَمة بالقسم الفرعي ٣.٩.٣، سيَكُون بمثابة مثالًا على تصميم صَنْف (class) جديد واِستخدَامه. ستُظهِر التَحرِيكة عددًا من الأقراص شبه الشفافة، والمُلوَّنة عشوائيًا، والمُتموضِعة بأماكن عشوائية عَبْر النافذة. عند تشغيل التحريكة، سيَزدَاد حجم تلك الأقراص، وعندما يَصِل حجم أي منها إلى حد معين، فإنه سيَختفِي ليَحلّ مَحلّه قرص آخر جديد بمكان عشوائي. قد يَحدُث ذلك -أيّ الاختفاء والظهور- بصورة عشوائية أيضًا بعيدًا عن حجم القرص. تَستعرِض الصورة التالية لقطة للشاشة أثناء تَشْغِيل البرنامج:

001Growing_Circles.png

سيَكُون كل قرص ضِمْن التَحرِيكة عبارة عن كائن (object)، ولأن أيّ قرص يَملُك عدة خاصيات (properties)، مثل اللون والموقع والحجم، فسنَستخدِم مُتْغيِّرات نُسخ (instance variables) لتمثيل كُلًا منها ضِمْن الكائن. أما بالنسبة لتوابع النُسخ (instance methods)، فيَنبغِي لنا التفكير أولًا بالكيفية التي سنَستخدِم بها القرص عَبْر البرنامج. سنَحتاج عمومًا إلى رسم القرص، لذا سنُضيف تابع النسخة draw(g)‎، حيث g هو كائن السِّياق الرُسومي (graphics context) المُستخدَم للرسم. يُمكِن للصَنْف أيضًا أن يَتَضمَّن بَانِي كائن (constructors) واحد أو أكثر لتهيئة الكائنات مبدئيًا. لا تَكُون البيانات المُفْترَض تمريرها للباني كمُعامِلات (parameters) واضحة دومًا. في هذا المثال، سنكتفي بتمرير كُلًا من موقع الدائرة وحجمها كمُعامِلات، أما لونها فسيَصنَعه الباني باِستخدَام قيم عشوائية للألوان الثلاثة بنظام RGB. اُنظر تعريف الصَنْف كاملًا:

import javafx.scene.paint.Color;
import javafx.scene.canvas.GraphicsContext;

public class CircleInfo {

    public int radius;    // نصف قطر الدائرة
    public int x,y;       // موقع مركز الدائرة
    public Color color;   // لون الدائرة

    /**
     * Create a CircleInfo with a given location and radius and with a
     * randomly selected, semi-transparent color.
     * @param centerX   The x coordinate of the center.
     * @param centerY   The y coordinate of the center.
     * @param rad       The radius of the circle.
     */
    public CircleInfo( int centerX, int centerY, int rad ) {
        x = centerX;
        y = centerY;
        radius = rad;
        double red = Math.random();
        double green = Math.random();
        double blue = Math.random();
        color = new Color( red,green,blue, 0.4 );
    }

    /**
     * Draw the disk in graphics context g, with a black outline.
     */
    public void draw( GraphicsContext g ) {
        g.setFill( color );
        g.fillOval( x - radius, y - radius, 2*radius, 2*radius );
        g.setStroke( Color.BLACK );
        g.strokeOval( x - radius, y - radius, 2*radius, 2*radius );
    }
}

لاحِظ أننا قد صَرَّحنا عن مُتْغيِّرات النُسخ (instance variables) على أساس كَوْنها عامة (public)؛ لتبسيط الأمور، لكن يُفضَّل عمومًا كتابة ضوابط (setters) وجوالب (setters) لكُلًا منها.

سنُعرِّف البرنامج main داخل الصنف GrowingCircleAnimation. ولأن البرنامج سيَستخدِم ١٠٠ قرص، كُلًا منها عبارة عن كائن من الصنف CircleInfo، فإنه سيَحتاج إلى مصفوفة من الكائنات لتَخْزِينها. لذا يُعرِّف البرنامج مُتْغيِّر مصفوفة (array variable) كمُتْغيِّر نُسخة (instance variable) ضِمْن الصنف، كالتالي:

private CircleInfo[] circleData; // يحمل بيانات 100 قرص

لاحِظ أن المُتْغيِّر circleData ليس ساكنًا (static). تَعتمِد برمجة واجهات المُستخدِم الرسومية (GUI) في العموم على الكائنات (objects) بدلًا من المُتْغيِّرات والتوابع الساكنة. يُمكِننا عمومًا تَخيُّل وجود عدة كائنات من الصنف GrowingCircleAnimation تَعمَل بصورة مُتزامنة. لذا ينبغي لكُلًا منها أن يمتلك مصفوفته الخاصة من الأقراص. بصيغة آخرى، كل تحريكة (animation) عبارة عن كائن، وكل كائن يَمتلك نسخته الخاصة من مُتْغيِّر النسخة circleData. إذا كان circleData ساكنًا، فسيَكُون هناك مصفوفة واحدة فقط، وستبدو جميع التَحرِيكات مُتطابقة تمامًا.

الآن، لابُدّ أن نُنشِئ المصفوفة ونَملؤها بالبيانات. في البرنامج التالي، قُمنا بذلك حتى قَبْل رسم أول إطار (frame) ضِمْن التَحرِيكة. أولًا، اِستخدَمنا التعبير new CircleInfo[100]‎ لإنشاء المصفوفة، ثُمَّ أنشأنا مائة كائن من النوع CircleInfo لمَلئ المصفوفة. لاحِظ أن الكائنات الجديدة تَكُون عشوائية فيما يَتعلَّق بحجمها ومَوضِعها. بفَرْض أن width و height هي أبعاد مساحة الرسم (drawing area)، اُنظر الشيفرة التالية:

circleData = new CircleInfo[100];  // أنشئ المصفوفة

for (int i = 0; i < circleData.length; i++) { // أنشئ الكائنات
    circleData[i] = new CircleInfo( 
                            (int)(width*Math.random()),
                            (int)(height*Math.random()),
                            (int)(100*Math.random()) );
}

يَزداد نصف قطر القرص بكل إطار (frame)، ثم يُعاد رسمه كالتالي:

circleData[i].radius++;
circleData[i].draw(g);

قد تبدو التَعْليمَات -بالأعلى- مُعقدة نوعًا ما، لذا دَعْنَا نَفْحَصها عن قرب. أولًا، يُمثِل circleData‎ أحد عناصر المصفوفة circleData، أيّ أنه مُتْغيِّر من النوع CircleInfo. يُشير ذلك المُتْغيِّر إلى كائن من النوع CircleInfo، والذي لابُدّ أن يَحتوِي على مُتْغيِّر النُسخة (instance variable) العام radius، ويَكُون اسمه الكامل هو circleData.radius. لمّا كان مُتْغيِّر النسخة ذاك من النوع int، نستطيع تطبيق عامل الزيادة ++ عليه لزيادة قيمته بمقدار الواحد، أيّ أن تأثير التَعْليمَة circleData.radius++‎ هو زيادة نصف قطر الدائرة بمقدار الواحد. يُعدّ السطر الثاني من الشيفرة مشابهًا للأول، باستثناء أن circleData.draw يُمثِل تابع نُسخة (instance method) بالكائن. تَستدعِي التَعْليمَة circleData.draw(g)‎ تابع النُسخة ذاك، وتُمرِّر له المُعامِل g، والذي يُمثِل كائن السِّياق الرسومي المُستخدَم للرسم.

يُمكِنك الإطلاع على الشيفرة المصدرية للبرنامج بالملف GrowingCircleAnimation.java إذا كنت مهتمًا. ولأن البرنامج يَستخدِم الصَنْف CircleInfo، ستحتاج أيضًا إلى نسخة الملف CircleInfo.java لتَصْرِيف البرنامج وتَشْغِيله.

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

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

من الجهة الآخرى، تُعدّ الأصناف قطعًا برمجية قابلة لإعادة الاِستخدَام بدون الحاجة لإجراء أي تَعْديلات عليها بشَّرْط أن تَكُون قد صُمّمت تصميمًا جيدًا. مثلًا، لا تُعدّ الأصناف المصنوعة خصيصًا لإنجار مُهِمّة مُحدَّدة جدًا ضِمْن برنامج مُحدَّد مُصمّمة تصميمًا جيدًا، وإنما لابُدّ أن يَكُون الصنف مصنوعًا بحيث يُمثِل نوعًا واحدًا محددًا من الأشياء أو مفهومًا واحدًا مترابطًا. لمّا كانت تلك المفاهيم والأشياء عامة كفاية لأن تَتكرَّر ضِمْن العديد من المشاكل، تزداد إمكانية الحاجة لاِستخدَام تلك الأصناف ضِمْن عدة مشروعات وبدون إجراء أي تَعْديلات.

ولأننا نَتعامَل مع لغة كائنية التوجه (object-oriented)، يُمكِننا أيضًا إنشاء أصناف فرعية (subclasses) من صَنْف موجود، وهو ما يُزيد من قابلية الأصناف لإعادة الاستخدَام. فمثلًا، إذا احتاج صَنْف معين لمزيد من التَخْصيص، يُمكِننا ببساطة إنشاء صَنْف فرعي (subclass) منه، وإجراء أي إضافات أو تَعْديلات على الصَنْف الفرعي بدون تَعْديل الصَنْف الأصلي. نستطيع القيام بذلك حتى لو لم يَكُن لدينا صلاحية وصول لشيفرة الصَنْف، ولا نعلم أي شيء عن التفاصيل التَّنْفيذية (implementation) الداخلية الخاصة به.

في الواقع، يُعدّ الصنف PairOfDice -من القسم السابق- مثالًا على قطعة برمجية مُعمَّمة، حيث يُمثِل الصنف مفهومًا واحدًا متماسكًا هو "حجري نَّرد". تَحمِل مُتْغيِّرات النُسخ (instance variables) بيانات مُتعلِّقة بحالة حجري النَّرد أي العدد الظاهر بكل حجر، كما يُمثِل تابع النُسخة (instance method) سلوك حجري النَّرد أي القدرة على رَميهما. على الرغم من إمكانية تحسِّين ذلك الصَنْف، فإنه قابلًا لإعادة الاستخدام بالكثير من المشروعات البرمجية المختلفة.

في المقابل، لا يُعدّ الصنف Student -من القسم السابق- قابلًا لإعادة الاستخدام؛ حيث يُبدو وكأنه قد صُمّم خِصيصًا لتمثيل مجموعة طلبة ضِمْن دورة تدريبية من نوع خاص، تَعتمِد درجاتها على ثلاثة اختبارات فقط، فإذا كان هناك حاجة لمزيد من الاختبارات أو الأوراق، فسيُصبِح الصنف عديم الفائدة. علاوة على ذلك، ستَحدُث مشكلة في حالة وجود شخصين لهما نفس الاسم، لذا عادةً ما يُستخدَم رقم هوية عددي بدلًا من الاسم. ولكن للإنصاف، فإن إنشاء صَنْف مُتعدد الأغراض لتمثيل الطلبة هو أمر أصعب كثيرًا من مُجرّد إنشاء صَنْف مُتعدد الأغراض لتمثيل حجري نَّرد.

تَتَكوَّن عملية تطوير أيّ مشروع برمجي ضخم من مجموعة من المراحل، بدايةً من مرحلة توصيف (specification) المشكلة المطلوب حلّها، ثُمَّ تحليلها (analysis)، وتصميم (design) البرنامج اللازم لحلّها، ثُمَّ تأتي مرحلة كتابة الشيفرة (coding)، والتي يَتحوِّل خلالها التصميم إلى لغة برمجية فعليّة، وبعد ذلك تأتي مرحلة الاختبار (testing)، وتَنْقيح الأخطاء (debugging). يَتبَع ذلك فترة طويلة من الصيانة (maintenance)، والتي تَتَضمَّن إصلاح أي مشاكل جديدة عُثر عليها بالبرنامج، وكذلك تَعْديله بحيث يَتكيَّف مع أي تَغْيير بمُتطلبات البرنامج. تُسمَى مُتتالية المراحل تلك باسم "دورة حياة تطوير البرمجيات (software life cycle)". نادرًا ما تَتتابَع تلك المراحل على نحو مُتتالي تمامًا، فمثلًا، قد يَتَّضِح أن التوصيف مُتعارض أو غَيْر مُكتمل أثناء مرحلة التحليل، أو قد يَتطلَّب العُثور على مشكلة أثناء مرحلة الاختبار (testing) عودة سريعة إلى مرحلة كتابة الشيفرة (coding) على الأقل، أو حتى تصميمًا جديدًا إذا كانت المشكلة كبيرة بما فيه الكفاية. وأخيرًا، عادةً ما تَتَضمَّن مرحلة الصيانة (maintenance) إعادة بعض الأعمال من المراحل السابقة.

نجاح أيّ مشروع برمجي ضخم عادةً ما يَكُون مَشْرُوطًا بتبَنّي أسلوب منهجي دقيق خلال جميع مراحل التطوير. يُعرَف ذلك الأسلوب المنهجي، والذي يَستخدِم مبادئ التصميم الجيد، باسم "هندسة البرمجيات (software engineering)". يُحاوِل مهندسي البرمجيات عمومًا بناء برامج مستوفية لتوصيفاتها (specifications)، وبحيث تَكُون مكتوبة بطريقة تُسهِل من تَعْدِيلها إن اقْتَضَت الضرورة. هنالك الكثير من الأساليب التي يُمكِن تطبيقها لتصميم البرامج بطريقة منهجية، وتَشتمِل غالبيتها على رسم صناديق صغيرة تُمثِل مُكوِّنات البرنامج مع أسهم مُعنوَنة لتمثيل العلاقات بين تلك الصناديق.

لقد ناقشنا كائنية التوجه (object orientation) فيما يَتعلَّق بمرحلة كتابة الشيفرة (coding)، لكن تَتوفَّر أيضًا أساليب كائنية التوجه لمرحلتي التحليل والتصميم. يَكُون السؤال بهذه المرحلة من مراحل تطوير البرمجيات: كيف نَتَمكَّن من اكتشاف أو اختراع البنية الكلية للبرنامج؟ اتبع النصيحة التالية، والتي تُعدّ بمثابة أسلوبًا بسيطًا كائني التوجه لمرحلتي التحليل والتصميم: سَجِّل توصيف المشكلة، ثُمَّ ارسم خطًا أسفل جميع الأسماء الموجودة بذلك التوصيف. ينبغي أن تَكُون تلك الأسماء مُرشَّحة كأصناف (classes) أو ككائنات (objects) ضِمْن تصميم البرنامج. بالمثل، ارسم خطًا أسفل جميع الأفعال (verbs) الموجودة بالتوصيف. ينبغي أن تَكُون تلك الأفعال مُرشَّحة كتوابع (methods). هذه هي نقطة البداية فقط، فقد يَكشِف مزيد من التحليل (analysis) عن الحاجة لإضافة أصناف أو توابع آخرى، كما قد يَكشِف عن إمكانية اِستخدَام أصناف فرعية (subclasses) تَتشارَك الخاصيات والسلوكيات المُتشابهة بينها.

"حلّل المشكلة لاكتشاف المفاهيم المُتضمَّنة، ثم عرِّف أصنافًا لتمثيل تلك المفاهيم. ينبغي أن يُحاكِي التصميم المشكلة، أو بتعبير آخر، ينبغي أن تَعكِس بنية البرنامج بنية المشكلة نفسها بصورة طبيعية." قد يَكُون ذلك تبسيطًا مُخِلًّا، ولكنه واضح وفعال عمومًا.

ترجمة -بتصرّف- للقسم Section 3: Programming with Objects من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...