English 🇬🇧

مقدمة في البرمجة كائنية التوجه

من التفكير بالخطوات إلى التفكير بالكائنات

CPCS203 - الوحدة 04: الكائنات (Objects) والأصناف (Classes)

1 القصة: كيف وصلنا إلى هنا

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

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

تطوير البرمجيات سلك المسار نفسه تمامًا.

في العقود الأولى من علم الحاسوب، كُتبت معظم البرامج كـتسلسل من التعليمات. تقول للحاسوب: "نفّذ الخطوة 1، ثم الخطوة 2، ثم الخطوة 3." هذا ما يُسمى البرمجة الإجرائية (Procedural Programming)، وهي تعمل بشكل ممتاز للبرامج الصغيرة.

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

أربعة مفاهيم أساسية في تصميم الأنظمة

1. إعادة الاستخدام (Reusability)

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

2. النمطية (Modularity)

تخيّل سيارة مطبوعة بالطباعة ثلاثية الأبعاد كـقطعة صلبة واحدة. إذا تعطل أي جزء — مرآة، مقبض باب، مصباح أمامي — ستضطر إلى استبدال السيارة بأكملها. هذا تصميم سيئ! في الحياة الحقيقية، نحب المنتجات المكونة من أجزاء مستقلة قابلة للاستبدال. إذا تعطل المصباح الأمامي، تستبدل المصباح فقط. في البرمجة كائنية التوجه، النمطية تعني تقسيم النظام المعقد إلى كائنات (Objects) مستقلة وقابلة للإدارة. كل كائن مسؤول عن سلوكه الخاص، مما يسهّل فهمه وتعديله أو استبداله دون التأثير على بقية النظام.

3. قابلية الصيانة (Maintainability)

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

4. قابلية التوسع (Scalability)

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

البرمجة الإجرائية ليست عاجزة عن تحقيق هذه المفاهيم الأربعة — فأنظمة كبيرة ومُصانة جيدًا مثل نواة لينكس (Linux Kernel) بُنيت باستخدام لغات إجرائية مثل C. لكن مع نمو الأنظمة وازدياد تعقيدها، أصبح تحقيق إعادة الاستخدام والنمطية وقابلية الصيانة وقابلية التوسع أكثر صعوبة بدون بُنى لغوية مدمجة تدعمها. كان على المطورين الاعتماد على الانضباط والاصطلاحات بدلًا من أن تفرض اللغة نفسها التصميم الجيد.

هذا التحدي أشعل البحث عن طرق أفضل لتنظيم الكود. وفي النهاية، ظهرت فكرة قوية: ماذا لو توقفنا عن التفكير في البرامج كتسلسلات من التعليمات، وبدأنا التفكير فيها كمجموعات من الكائنات (Objects) التي تتفاعل مع بعضها البعض؟ هذا النموذج يوفر آليات مدمجة — الأصناف (Classes)، الكائنات (Objects)، الوراثة (Inheritance)، التغليف (Encapsulation) — تجعل تحقيق هذه المفاهيم الأربعة أكثر طبيعية وسهولة.

هذه الفكرة أصبحت تُعرف بـ البرمجة كائنية التوجه (Object-Oriented Programming - OOP).

نبذة تاريخية

1
البرمجة الإجرائية (Procedural Programming)
الخمسينيات–السبعينيات — C, Fortran, COBOL

تُنظَّم البرامج كتسلسلات من التعليمات مُجمَّعة في دوال/إجراءات. البيانات والدوال منفصلة. ممتازة للبرامج الصغيرة، لكن الأنظمة الكبيرة تصبح صعبة الإدارة.

2
البرمجة الوظيفية (Functional Programming)
الخمسينيات–الآن — Lisp, Haskell, Clojure

تُبنى البرامج من دوال نقية (Pure Functions) بدون آثار جانبية وبيانات غير قابلة للتغيير (Immutable). ممتازة للحسابات الرياضية وتحويلات البيانات والتزامن. الدوال مواطنات من الدرجة الأولى يمكن تمريرها كقيم.

3
البرمجة كائنية التوجه (Object-Oriented Programming)
الستينيات–الآن — Simula, Smalltalk, Java, C++, Python

تُنظَّم البرامج حول كائنات — وحدات تجمع البيانات والسلوك معًا. وُلدت في مركز أبحاث زيروكس بارك (Xerox PARC) — نفس المكان الذي أعطانا واجهة المستخدم الرسومية — وغيّرت طريقة بناء الأنظمة المعقدة الكبيرة.

2 النقاش الكبير: البرمجة كائنية التوجه مقابل الوظيفية مقابل الإجرائية

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

كما يقول مهندس البرمجيات الشهير ديف فارلي (Dave Farley): هذه النماذج البرمجية هي أدوات وليست أديان. أنت لا تخوض حربًا حول ما إذا كان المطرقة أفضل من المفك. أنت تختار الأداة المناسبة للمهمة.

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

— ديف فارلي (Dave Farley)، "OOP vs Functional Programming"، قناة Continuous Delivery على YouTube

ديف فارلي هو رائد في هندسة البرمجيات، والمؤلف المشارك للكتاب الحائز على جوائز "Continuous Delivery"، ومؤسس قناة Continuous Delivery على YouTube. لديه أكثر من 40 عامًا من الخبرة في بناء أنظمة برمجية واسعة النطاق.

دعونا نفهم ما يجعل كل نموذج برمجي فريدًا:

البرمجة الإجرائية
  • تنظم الكود في دوال/إجراءات
  • البيانات والدوال منفصلة
  • تنفذ التعليمات خطوة بخطوة
  • فكّر: "افعل هذا، ثم افعل ذلك"
البرمجة كائنية التوجه
  • تنظم الكود في كائنات
  • البيانات والسلوك مجمّعان معًا
  • الكائنات تتفاعل عبر الرسائل (استدعاءات الدوال)
  • فكّر: "ما هي الأشياء، وماذا يمكنها أن تفعل؟"
البرمجة الوظيفية
  • تنظم الكود في دوال نقية (بدون آثار جانبية)
  • تُركز على البيانات غير القابلة للتغيير (البيانات لا تتغير)
  • الدوال يمكن تمريرها كمعاملات وإرجاعها كقيم
  • فكّر: "ما هو التحويل من المدخلات إلى المخرجات؟"

مثال عملي: حساب مساحات الدوائر

لنرَ كيف تبدو نفس المشكلة في كل نموذج برمجي:

المنهج الإجرائي
// البيانات منفصلة عن الدوال double radius1 = 5.0; double radius2 = 10.0; // الدوال تعمل على بيانات خارجية static double computeArea(double radius) { return Math.PI * radius * radius; } System.out.println(computeArea(radius1)); // 78.54 System.out.println(computeArea(radius2)); // 314.16
المنهج الوظيفي
// الدوال كقيم، بيانات غير قابلة للتغيير، بدون آثار جانبية Function<Double, Double> computeArea = radius -> Math.PI * radius * radius; // تطبيق الدالة على القيم List<Double> radii = List.of(5.0, 10.0); radii.stream() .map(computeArea) .forEach(System.out::println); // 78.54, 314.16
المنهج كائني التوجه
// البيانات والسلوك مجمّعان داخل الكائن Circle c1 = new Circle(5.0); Circle c2 = new Circle(10.0); // كل كائن يعرف كيف يحسب مساحته System.out.println(c1.computeArea()); // 78.54 System.out.println(c2.computeArea()); // 314.16
الفكرة الجوهرية: الفرق الحقيقي

الفرق الحقيقي بين هذه النماذج البرمجية ليس ما تسمح لنا بفعله — أي لغة عامة الأغراض يمكنها حل أي مشكلة (هذا هو مفهوم اكتمال تورنغ - Turing Completeness). الفرق هو مدى سهولة التعبير عن أفكار معينة.

قد يكون من الأسهل كتابة كود غير قابل للتغيير وخالٍ من الآثار الجانبية في Haskell. وقد يكون من الأسهل نمذجة الكيانات الحقيقية في Java. لكن يمكنك فعل كليهما في كلتا اللغتين. النموذج البرمجي يُشكّل طريقة تفكيرك في المشكلة.

لماذا البرمجة كائنية التوجه في هذا المقرر؟

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

"البرمجة كائنية التوجه تبدو كثيرًا مثل البرمجة الوظيفية عندما تُنفَّذ بالشكل الصحيح."

— مايكل فيذرز (Michael Feathers)

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

الجانب الإجرائية الوظيفية كائنية التوجه
الوحدة الأساسية دالة / إجراء دالة نقية كائن (بيانات + سلوك)
البيانات منفصلة عن الدوال غير قابلة للتغيير، يتم تحويلها مغلّفة داخل الكائنات
الحالة عامة أو يتم تمريرها يتم تجنبها (بدون آثار جانبية) تُدار لكل كائن
إعادة الاستخدام نسخ ولصق أو مكتبات دوال دوال عليا، تركيب الوراثة، تعدد الأشكال
الأفضل لـ البرامج الصغيرة، كود مستوى النظام خطوط معالجة البيانات، التزامن الأنظمة الكبيرة، الواجهات الرسومية، النمذجة

3 ما هو الكائن؟

انظر حولك الآن. كل شيء تراه هو كائن: هاتفك، مكتبك، كوب قهوتك، حتى أنت. كل واحد من هذه الأشياء له خصائص (Properties) تصفه وسلوكيات (Behaviors) — أشياء يمكنه فعلها أو يمكن فعلها به.

في البرمجة كائنية التوجه، الكائن (Object) هو كيان برمجي يعكس هذه الفكرة:

تعريف الكائن

الكائن (Object) هو كيان يمتلك:

  • خصائص (Properties/Attributes): صفات تصف الكائن (مثلًا: اللون، الحجم، الحالة).
  • سلوكيات (Methods/Behaviors): أفعال أو عمليات يستطيع الكائن تنفيذها (مثلًا: المشي، الحساب، العرض).
تشبيه من الواقع: أنت كائن!

فكّر في نفسك ككائن:

  • خصائصك: الاسم، العمر، الطول، لون العيون، الرقم الجامعي
  • سلوكياتك: ()speak، ()walk، ()study، ()sleep، ()eat

صديقك كائن مختلف، لكن من نفس النوع (Human). كلاكما لديكما نفس أنواع الخصائص والسلوكيات، لكن بقيم مختلفة. اسمك مختلف، طولك مختلف، لكن كلاكما يستطيع المشي والتحدث.

4 عقلية جديدة: التفكير بالكائنات

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

بالطريقة الإجرائية القديمة، كنا سنقفز مباشرة لكتابة دالة: computeArea(radius). انتهى. لكن مع البرمجة كائنية التوجه، لا نبدأ بالتفكير في الدوال. نبدأ بالتفكير في الكائنات — لأننا موجَّهون بالكائنات. الكائنات هي ما يوجّه ويقود عملية تفكيرنا بالكامل.

إذن كيف يتعامل مبرمج البرمجة كائنية التوجه مع هذه المشكلة؟ باتباع ثلاث خطوات واضحة:

1
الخطوة 1: تحديد الكائنات

أول سؤال نطرحه هو: كم عدد الكائنات المشاركة في هذا البرنامج؟ نحن موجَّهون بالكائنات، لذا تحديدها هو دائمًا نقطة البداية. لمشكلة مساحة الدائرة، الإجابة واضحة: يوجد كائن واحد — Circle (الدائرة).

2
الخطوة 2: تحديد الخصائص والسلوكيات

بمجرد تحديد الكائنات، نسأل: ما هي خصائص وسلوكيات كل كائن؟ لدائرتنا:

  • الخصائص: radius (نصف القطر — ما يصف الدائرة)
  • السلوكيات: computeArea() (ما يمكن للدائرة فعله)
3
الخطوة 3: تحديد العلاقات

إذا كان لدينا أكثر من كائن، يجب أيضًا تحديد العلاقات بينها. في حالتنا، لدينا كائن واحد فقط (Circle)، لذا لا توجد علاقات نحتاج لتعريفها. لكن في نظام أكبر — مثل نظام مكتبة فيه Book وMember وLibrarian وLoan — سنحتاج لتعريف كيف تتصل وتتفاعل هذه الكائنات.

عملية التفكير في البرمجة كائنية التوجه (ملخص)

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

  1. ما الكائنات المشاركة؟
  2. ما الخصائص والسلوكيات لكل كائن؟
  3. ما العلاقات الموجودة بين الكائنات؟

هذا يختلف جذريًّا عن التفكير الإجرائي، حيث نسأل: "ما الدوال التي أحتاجها؟" في البرمجة كائنية التوجه، الكائنات تأتي أولًا. كل شيء آخر يتبعها.

عمل التصميم هذا — تحديد الكائنات وخصائصها وسلوكياتها وعلاقاتها — هو عادةً مسؤولية مهندس البرمجيات (المصمم). لكن الآن يظهر سؤال: بعد أن حددنا كل هذا، كيف نصفه؟ ما اللغة التي نستخدمها لتوصيل هذا التصميم؟

5 UML: لغة التصميم

لقد حددنا كائننا (Circle)، وخصائصه (radius)، وسلوكياته (computeArea()). لكننا نحتاج طريقة لوصف وتوصيل هذا التصميم — لغة معيارية يستطيع مهندس البرمجيات استخدامها للتعبير عن التصميم، ويستطيع المبرمج قراءتها وفهمها.

هذه اللغة تُسمى لغة النمذجة الموحدة (UML - Unified Modeling Language).

UML هي ترميز رسومي — لغة بصرية من المخططات والرموز. أحد أهم مخططاتها هو مخطط الأصناف (Classes Diagram)، الذي يصف خصائص وسلوكيات وعلاقات الكائنات. يحتوي على ثلاثة أقسام:

هيكل مخطط الأصناف (Classes) في UML
  1. الأعلى: اسم الصنف (Class) (نوع الكائن)
  2. الوسط: الخصائص (السمات) مع أنواعها
  3. الأسفل: السلوكيات (الدوال) مع أنواع القيم المُعادة

علامات الرؤية: - تعني خاص (private)، + تعني عام (public).

إليك مخطط الأصناف (Classes) في UML لدائرتنا Circle:

Circle
- radius: double
+ computeArea(): double
UML مثل المخطط المعماري تمامًا

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

هندسة البرمجيات تعمل بنفس الطريقة تمامًا. عندما يرسم مهندس البرمجيات مخطط UML، فهو لا يحدد أي لغة برمجة ستُستخدم. لا يهمه إذا كان المبرمج سيستخدم Java أو C++ أو Python أو أي لغة أخرى. UML تتعلق بـماذا يبدو عليه النظام — وليس كيف سيُبنى. اختيار اللغة هو قرار المبرمج، تمامًا كما اختيار نوع الأسمنت والحديد هو قرار البنّاء.

دوران، لغة واحدة

في تطوير البرمجيات، هناك فصل بين الأدوار:

  • مهندس البرمجيات (المصمم): يحدد الكائنات وخصائصها وسلوكياتها وعلاقاتها. يعبّر عن التصميم باستخدام UML. لا يهتم بأي لغة برمجة ستُستخدم.
  • المبرمج (المنفذ): يقرأ UML، يفهم الترميز الرسومي، ويحوّله إلى كود باستخدام لغة برمجة محددة مثل Java أو C++.

UML هي الجسر بين هذين الدورين — لغة رسومية عالمية يستطيع كلاهما فهمها، بغض النظر عن لغة البرمجة التي ستُستخدم في النهاية.

الآن لدينا مخطط UML. مهمة مهندس البرمجيات انتهت. السؤال التالي هو: كيف يحوّل المبرمج مخطط UML هذا إلى كود عامل فعلي؟

6 من UML إلى الكود: الصنف (Class)

المبرمج الآن يأخذ مخطط UML ويحتاج لتحويله إلى لغة برمجة. لكن مخطط UML يصف خصائص وسلوكيات كائن. فيصبح السؤال: في Java، ما هو البناء البرمجي الذي يتيح لنا وصف خصائص وسلوكيات الكائن؟

الجواب هو الصنف (Class).

ما هو الصنف (Class)؟

الصنف (Class) هو البناء البرمجي — القالب — الذي يصف خصائص وسلوكيات الكائن. إنه المكافئ البرمجي لمخطط الأصناف (Classes) في UML. كل لغة برمجة لها صياغتها الخاصة لكتابة الأصناف (Classes) (Java لها صياغتها، C++ لها صياغتها، Python لها صياغتها)، لكن المفهوم واحد:

  • الصنف (Class) هو قالب/مخطط يُحدد الخصائص والسلوكيات التي سيمتلكها الكائن.
  • الكائن (Object) — ويُسمى أيضًا نسخة (Instance) — هو تجسيد حقيقي يُنشأ من ذلك الصنف (Class)، بقيم محددة لخصائصه.
الصنف (Class) والكائن (Object): قالب البسكويت والبسكويت

الصنف (Class) مثل قالب البسكويت — يُحدد الشكل. كل كائن هو بسكويتة مصنوعة من ذلك القالب. يمكنك صنع بسكويت شوكولاتة وبسكويت فانيلا وبسكويت فراولة — كلها من نفس القالب. جميعها لها نفس الشكل (الهيكل)، لكن بنكهات مختلفة (قيم). قالب واحد، بسكويت كثير. صنف (Class) واحد، كائنات (Objects) كثيرة.

ثلاثة أدوار في تطوير البرمجيات

الآن ننتقل عبر ثلاثة أدوار مختلفة:

  1. مهندس البرمجيات (المصمم): صمّم مخطط UML — مهمته انتهت.
  2. المبرمج (المنفذ): يأخذ مخطط UML ويحوّله إلى كود باستخدام لغة برمجة.
  3. المستخدم: يُنشئ كائنات (Objects) من الصنف (Class) ويستخدمها في برامجه.

لنأخذ الآن دور المبرمج أولًا، ثم دور المستخدم.

الخطوة 1: التنفيذ (دور المبرمج)

المبرمج ينظر إلى مخطط UML. يُظهر صنف (Class) Circle بخاصية واحدة (radius) وسلوك واحد (computeArea()). فيترجم المبرمج ذلك تمامًا إلى Java:

Circle.java — التنفيذ الأول
public class Circle { // Property (data field) private double radius; // Behavior public double computeArea() { return Math.PI * radius * radius; } }

هذا كل شيء. الصنف (Class) يحتوي بالضبط على ما وصفه مخطط UML: خاصية radius (مُعلنة private) ودالة computeArea(). مهمة المبرمج انتهت حاليًّا.

الخطوة 2: إنشاء الكائنات (دور المستخدم)

الآن ننتقل إلى دور مختلف: المستخدم. المستخدم يريد إنشاء كائنات Circle وحساب مساحاتها. لنجرّب:

Main.java — محاولة استخدام الدائرة
public class Main { public static void main(String[] args) { // Create two Circle objects Circle c1 = new Circle(); Circle c2 = new Circle(); // We want to set the radius, but... // c1.radius = 5.0; // ERROR! radius has private access in Circle // c2.radius = 10.0; // ERROR! radius has private access in Circle // We can't set the radius! Let's just try computeArea anyway... System.out.println(c1.computeArea()); // 0.0 System.out.println(c2.computeArea()); // 0.0 } }
المشكلة

أنشأنا كائنين Circle، لكننا لا نستطيع الوصول إلى radius! لا نستطيع تعيين أي بيانات له. لماذا؟ لأن radius مُعلن كـ private. وبما أن نصف القطر لم يُعيَّن أبدًا، فقيمته الافتراضية 0.0، لذا computeArea() تُعيد 0.0 لكلتا الدائرتين.

هذه مشكلة حقيقية. لدينا صنف (Class) Circle، لكن لا نستطيع فعل أي شيء مفيد به! ماذا يمكننا أن نفعل؟

الخطوة 3: اقتراح طالب — اجعله عامًّا (Public)!

طالب يرفع يده: "يا أستاذ، لماذا لا نعرّف radius كـ public بدلًا من private؟"

اقتراح جيد! لنجرّب:

Circle.java — بنصف قطر عام
public class Circle { // Changed to public — anyone can access it directly public double radius; public double computeArea() { return Math.PI * radius * radius; } }
Main.java — الآن يعمل!
public class Main { public static void main(String[] args) { Circle c1 = new Circle(); c1.radius = 5.0; System.out.println(c1.computeArea()); // 78.54 — it works! Circle c2 = new Circle(); c2.radius = 10.0; System.out.println(c2.computeArea()); // 314.16 — it works! } }
يعمل!

مع public radius، يمكننا الآن تعيين القيمة مباشرة وحساب المساحة. المشكلة حُلّت... أم لا؟

الخطوة 4: المشكلة الكبرى

لحظة. إذا كان نصف القطر public، فهذا يعني أن أي شخص يمكنه تعيين أي قيمة. ماذا لو فعل شخص ما هذا؟

c1.radius = -10; // A negative radius?! System.out.println(c1.computeArea()); // 314.16 — area of a circle with radius -10??
هذه مشكلة خطيرة!

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

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

الخطوة 5: الركن الأول — التغليف (Encapsulation)

ما اكتشفناه للتو هو بالضبط السبب وراء وجود الركن الأول في البرمجة كائنية التوجه: التغليف (Encapsulation). لكن قبل أن نعرّفه، انظر إلى الكلمة نفسها:

💊
Encapsulation
الكلمة مشتقة من capsule (كبسولة) — مثل كبسولة الدواء.
الكبسولة تلف وتحمي الدواء بداخلها.
لا أحد يستطيع لمس الدواء مباشرة — إنه محاط ومحمي.
في البرمجة كائنية التوجه، التغليف يلف ويحمي البيانات داخل الكائن.
التغليف (Encapsulation)

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

عندما عرّفنا radius كـ public، انتهكنا التغليف. كسرنا قدرة الكائن على حماية نفسه.

تشبيه: الكائنات مثل الدول ذات السيادة

فكّر في كل كائن كـدولة ذات سيادة. تخيّل المملكة العربية السعودية ومصر. إذا أرادت الحكومة المصرية معلومات عن مواطن سعودي، هل يمكنها مدّ يدها مباشرة والحصول على ذلك المواطن؟ بالتأكيد لا.

الإجراء الصحيح: وزارة الداخلية المصرية تتواصل مع وزارة الخارجية المصرية، التي تتواصل مع وزارة الخارجية السعودية، التي تتواصل مع وزارة الداخلية السعودية. كل شيء يمر عبر القنوات الرسمية — عبر البوابات المناسبة.

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

إذن نحتاج العودة إلى private. لكن كيف نحل المشكلة الأصلية المتمثلة في عدم القدرة على تعيين نصف القطر؟ الجواب: دوال الإرجاع والتعيين (Getters and Setters).

الخطوة 6: الحل — دالّة الإرجاع ودالّة التعيين (Getter and Setter)

نُعيد radius إلى private، ونضيف دالتين عامتين: دالّة تعيين (Setter) لتغيير البيانات ودالّة إرجاع (Getter) لقراءة البيانات. هذه الدوال هي البوابات الرسمية التي يتفاعل العالم الخارجي من خلالها مع بيانات الكائن.

Circle.java — مع دالّة إرجاع ودالّة تعيين
public class Circle { // Property — private (encapsulated) private double radius; // Setter — allows outside code to CHANGE the radius public void setRadius(double radius) { this.radius = radius; } // Getter — allows outside code to READ the radius public double getRadius() { return radius; } // Behavior public double computeArea() { return Math.PI * radius * radius; } }
Main.java — استخدام دالّة الإرجاع ودالّة التعيين
public class Main { public static void main(String[] args) { Circle c1 = new Circle(); c1.setRadius(5.0); System.out.println("Radius: " + c1.getRadius()); // 5.0 System.out.println("Area: " + c1.computeArea()); // 78.54 Circle c2 = new Circle(); c2.setRadius(10.0); System.out.println("Radius: " + c2.getRadius()); // 10.0 System.out.println("Area: " + c2.computeArea()); // 314.16 } }

يعمل مرة أخرى! لكن انتظر — طالب يطرح سؤالًا جيدًا...

الخطوة 7: "أليس هذا نفس الشيء مثل Public؟"

طالب يرفع يده: "يا أستاذ، ماذا لو استدعى شخص ما setRadius(-10)؟ لا تزال لدينا نفس المشكلة، أليس كذلك؟ كيف يختلف هذا عن public؟"

سؤال ممتاز! والجواب هو: لا، ليسا نفس الشيء على الإطلاق.

عندما كان نصف القطر public، كانت البيانات تُعدَّل مباشرة من الخارج — الكائن لم يكن له رأي. لكن الآن، مع دالّة التعيين، التعديل يمر عبر الكائن نفسه. ولأنه يمر عبر دالة داخل الكائن، يمكن للكائن إضافة شروط!

Circle.java — دالّة تعيين مع التحقق
// Setter — with validation! The object protects itself. public void setRadius(double radius) { if (radius >= 0) { this.radius = radius; } else { System.out.println("Error: radius cannot be negative!"); } }
Main.java — اختبار التحقق
Circle c1 = new Circle(); c1.setRadius(5.0); System.out.println(c1.computeArea()); // 78.54 — works! c1.setRadius(-10); // Error: radius cannot be negative! System.out.println(c1.computeArea()); // 78.54 — still 5.0! The object protected itself!
التغليف عمليًّا

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

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

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

7 الأركان الأربعة للبرمجة كائنية التوجه

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

💊
التغليف (Encapsulation)
تجميع البيانات والدوال معًا في صنف (Class) واحد، والتحكم بالوصول إلى الحالة الداخلية. مثل كبسولة تحمي محتوياتها — تتفاعل مع الخارج، لا الداخل.
🎨
التجريد (Abstraction)
إخفاء التعقيد غير الضروري وإظهار ما يهم فقط. مثل قيادة السيارة — تستخدم المقود والدواسات دون معرفة كيف يعمل المحرك داخليًّا.
👪
الوراثة (Inheritance)
إنشاء أصناف (Classes) جديدة مبنية على أصناف (Classes) موجودة، مع إعادة استخدام وتوسيع خصائصها وسلوكياتها. سيارة SportsCar ترث من Car وتضيف التوربو.
🔄
تعدد الأشكال (Polymorphism)
كائنات من أنواع مختلفة تستجيب لنفس الرسالة بشكل مختلف. استدعِ ()draw على دائرة أو مربع أو مثلث — كل منها يرسم نفسه بطريقته الخاصة.

التغليف عمليًّا (مثال الدائرة)

لاحظ أنه في صنف (Class) Circle الخاص بنا، الحقل radius مُعلن بصفة private. هذا يعني أنه لا يمكن لأي كود خارج الصنف (Class) فعل هذا:

// هذا لن يُترجم — radius خاص! c1.radius = -50; // ERROR: radius has private access in Circle

بدلًا من ذلك، يجب على العالم الخارجي استخدام دالّة الإرجاع ودالّة التعيين. هذا يمنح الصنف (Class) التحكم:

// يمكننا إضافة التحقق داخل دالّة التعيين: public void setRadius(double radius) { if (radius > 0) { this.radius = radius; } else { System.out.println("Error: radius must be positive!"); } }
لماذا التغليف مهم

التغليف (Encapsulation) يحمي الحالة الداخلية للكائن من التغييرات غير الصالحة أو غير المقصودة. كما يعني أنه يمكنك تغيير التنفيذ الداخلي دون كسر الكود الذي يستخدم الصنف (Class). العالم الخارجي يعتمد فقط على الواجهة العامة (الدوال)، وليس التفاصيل الداخلية.

التجريد عمليًّا

الركن الثاني في البرمجة كائنية التوجه هو التجريد (Abstraction). لكن ماذا يعني حقًّا؟ قبل أن نعرّفه تقنيًّا، لنفكّر فيه من منظور أوسع.

ماذا لو كان كل شيء يتطلب خبرة؟

اسأل نفسك هذا السؤال: ماذا لو كان كل جهاز، كل آلة، كل نظام في العالم لا يمكن استخدامه إلا بواسطة خبير؟

تخيّل هذا العالم...
  • لكي تقود سيارة، يجب أن تكون مهندسًا ميكانيكيًّا يفهم محركات الاحتراق وأنظمة النقل وحقن الوقود.
  • لكي تستخدم هاتفًا، يجب أن تكون مهندس اتصالات يفهم ترددات الراديو ومعالجة الإشارات وبروتوكولات TCP/IP.
  • لكي تستخدم حاسوبًا، يجب أن تفهم هندسة المعالج وإدارة الذاكرة والأعمال الداخلية لنظام التشغيل.

كيف ستعمل حياتنا؟ لن تعمل! لن يكون هناك سائقون، ولا مستخدمو هواتف، ولا أحد يستخدم أي جهاز. سينهار المجتمع.

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

هذه هي فلسفة التجريد: كيف نطوّر نظامًا يمكن لغير الخبير استخدامه.

التجريد (Abstraction)

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

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

لقد جرّبت التجريد من قبل: صنف (Class) Scanner

تذكّر مقرر CPCS202 (البرمجة 1). جميعكم استخدمتم صنف (Class) Scanner لقراءة مدخلات المستخدم:

Scanner input = new Scanner(System.in); int age = input.nextInt(); String name = input.nextLine();

استخدمتموه بثقة. أنشأتم كائن Scanner، استدعيتم nextInt()، استدعيتم nextLine()، وعمل الأمر. لكن دعوني أسألكم:

"هل تعرفون كيف تم تنفيذ صنف (Class) Scanner داخليًّا؟ هل تعرفون الخوارزميات المعقدة التي يستخدمها لتحليل تدفقات الإدخال، ومعالجة ترميز الأحرف، وتقسيم البيانات إلى رموز؟"

لا. ولم تحتاجوا لذلك. صنف (Class) Scanner له تنفيذ داخلي معقد جدًّا، لكن كل ذلك التعقيد كان مخفيًّا عنكم. تفاعلتم فقط مع الدوال العامة — الواجهة البسيطة. هذا هو التجريد عمليًّا: هناك فصل بين التنفيذ والاستخدام.

التجريد في دائرتنا

الآن انظر إلى صنف (Class) Circle الخاص بنا. المستخدم يمكنه إنشاء كائن، تعيين نصف القطر، وحساب المساحة:

Circle c1 = new Circle(); c1.setRadius(5.0); System.out.println(c1.computeArea()); // 78.54

لكن هل يعرف المستخدم كيف تحسب computeArea() المساحة؟ هل يعرف أنها تستخدم Math.PI * radius * radius؟ لا — ولا يحتاج لذلك. التنفيذ مخفي. المستخدم يرى فقط الدوال العامة. هذا هو التجريد.

علامة التجريد السيئ

كيف تعرف أن التجريد ضعيف؟

إذا طوّرت برنامجًا والمستخدم النهائي يعاني ويصرخ محاولًا استخدامه — فهذه علامة واضحة أن تجريدك ليس جيدًا.

أي نظام برمجي ليس سهل الاستخدام يعني أن ركن التجريد لم يُنفَّذ بشكل جيد. التعقيد لم يُخفَ بشكل صحيح عن المستخدم. فكّر في التطبيقات التي استخدمتها وكانت محبطة — المطورون فشلوا في التجريد.

فكّر دائمًا في المستخدم النهائي عند تصميم برامجك. اجعلها بسيطة الاستخدام، مهما كانت معقدة من الداخل.

الأصناف (Classes) هي بالفعل تجريدات

قد يقول طالب: "يا أستاذ، أليس الصنف (Class) نفسه بالفعل تجريدًا؟"

نعم، بالتأكيد! كل صنف (Class) في البرمجة كائنية التوجه — سواء في Java أو Python أو C++ — هو بحد ذاته تطبيق لمفهوم التجريد. عندما تكتب صنفًا (Class) بحقول private ودوال public، فأنت بالفعل تُنشئ تجريدًا: تخفي التفاصيل الداخلية وتُظهر فقط ما يحتاجه المستخدم.

التجريد على مستوى النظام: الصورة الأكبر

لكن عندما تصمم نظامًا أكبر مكونًا من أصناف عديدة، يصبح التجريد أكثر أهمية. تحتاج لإنشاء صنف (Class) يمثل المستوى المُجرَّد للنظام بأكمله — يُسمى أحيانًا صنف (Class) المتحكم (Controller Class) أو صنف (Class) المحرك (Engine Class). هذا الصنف يخفي كل التعقيد الداخلي ويُظهر فقط العمليات الأساسية، بشكل مشابه لـ واجهة برمجة التطبيقات (API).

مثال: نظام توصيل الطعام

تخيّل أنك تطوّر نظام توصيل طعام (مثل HungerStation). داخليًّا، هذا النظام يحتوي على أصناف (Classes) عديدة: Customer، Restaurant، Order، Driver، Payment، وغيرها الكثير — كل منها بمنطق معقد.

لكنك أيضًا تُنشئ صنف (Class) متحكم يمثل المستوى المُجرَّد للنظام. هذا الصنف (Class) يُظهر دوال عامة بسيطة:

public class DeliverySystem { // The abstract level — simple public methods (like an API) public void registerCustomer(String name, String phone) { ... } public void deleteCustomer(int customerId) { ... } public void placeOrder(int customerId, int restaurantId) { ... } public void cancelOrder(int orderId) { ... } public String trackOrder(int orderId) { ... } // All the complex internal logic is hidden inside... }

الآن، تخيّل أن مطورًا آخر يريد بناء تطبيق ويب أو تطبيق جوال باستخدام نظام التوصيل الخاص بك. لا يحتاج لفهم العشرات من الأصناف (Classes) الداخلية. ببساطة يستورد صنف (Class) المتحكم ويستخدم دواله العامة:

// Another developer building a web application DeliverySystem system = new DeliverySystem(); system.registerCustomer("Ahmed", "0551234567"); system.placeOrder(1001, 55); system.trackOrder(3001); // They don't know or care about the internal complexity!

هناك فصل تام بين التنفيذ والاستخدام. المطور الذي يستخدم نظامك لا يحتاج لمعرفة أي من التفاصيل الداخلية. بل يمكنه حتى استخدام لغة برمجة مختلفة — مثلًا، استدعاء مكتبتك في Java من MATLAB أو Python — وسيعمل الأمر، لأنه يتفاعل فقط مع المستوى المُجرَّد.

لماذا التجريد مهم

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

  • كل صنف (Class) هو بالفعل تجريد — يخفي حقوله الخاصة خلف دوال عامة.
  • نظام من أصناف (Classes) عديدة يحتاج صنف (Class) متحكم/محرك يمثل المستوى المُجرَّد — يخفي كل التعقيد خلف عمليات بسيطة وعامة.
  • إذا كان مستخدموك يعانون لاستخدام برنامجك، فتجريدك يحتاج لتحسين.
  • صمّم دائمًا برامجك على المستوى المُجرَّد — اجعلها بسيطة الاستخدام، مهما كانت معقدة من الداخل.

8 لماذا البرمجة كائنية التوجه مهمة: الصورة الكبرى

قد تفكر: "كان بإمكاني فعل كل هذا بدوال بسيطة. لماذا أتعب نفسي بالأصناف (Classes) والكائنات (Objects)؟" الجواب يتضح مع نمو الأنظمة. برنامج من 100 سطر لا يحتاج البرمجة كائنية التوجه. نظام من 100,000 سطر يحتاجها بشدة.

البرمجة كائنية التوجه تمنحنا أربع قوى خارقة تعالج المشاكل التي حددناها في البداية مباشرة:

المشكلة حل البرمجة كائنية التوجه كيف
إعادة الاستخدام الأصناف (Classes) يمكن استخدامها عبر المشاريع اكتب صنف (Class) Student مرة واحدة، واستخدمه في أي تطبيق يتعامل مع الطلاب
قابلية التوسع الأصناف (Classes) الجديدة تمتد من الموجودة تحتاج GraduateStudent؟ وسّع Student دون تغييره (الوراثة)
قابلية الصيانة التغييرات محصورة محليًّا غيّر كيفية حساب المعدل داخل Student دون لمس أي كود يستخدمه (التغليف)
النمطية كل صنف (Class) وحدة مستقلة أعضاء فريق مختلفون يعملون على Student و Course و Enrollment بشكل مستقل

"القرارات الرئيسية في البرمجيات تقع خارج التفاصيل التقنية للغات الفردية. من المجدي فهم كلٍّ من البرمجة الوظيفية والبرمجة كائنية التوجه. كلتاهما أدوات قيّمة لبناء أنظمة معقدة. استخدمهما في السياق المناسب، وكل منهما سيساعدك على بناء أنظمة أفضل."

— ديف فارلي (Dave Farley)، "OOP vs Functional Programming"، قناة Continuous Delivery على YouTube

9 الملخص

ما تعلمناه
  • النماذج البرمجية (الإجرائية، الوظيفية، كائنية التوجه) هي أدوات وليست أديان — لكل منها نقاط قوة.
  • البرمجة كائنية التوجه تنظم الكود حول كائنات تجمع البيانات (الخصائص) والسلوك (الدوال).
  • الصنف (Class) مخطط هندسي؛ الكائن (Object) نسخة من ذلك المخطط.
  • عملية البرمجة كائنية التوجه: تحديد الكائنات → تحديد الخصائص والسلوكيات → تحديد العلاقات.
  • مخططات أصناف (Classes) UML تمثل الأصناف (Classes) بصريًّا قبل كتابة الكود.
  • الأركان الأربعة للبرمجة كائنية التوجه: التغليف، التجريد، الوراثة، وتعدد الأشكال.
  • التغليف يحمي الحالة الداخلية باستخدام حقول private ودوال إرجاع/تعيين عامة.
  • التجريد يخفي التعقيد — المستخدمون يستدعون الدوال دون معرفة التفاصيل الداخلية.
  • البرمجة كائنية التوجه تتألق في بناء أنظمة كبيرة ومعقدة تحتاج لأن تكون قابلة لإعادة الاستخدام والتوسع والصيانة.
ما التالي؟

في الدرس القادم، سنتعمق أكثر في المُنشئات (Constructors)، وكلمة this، والفرق بين أعضاء النسخة والأعضاء الثابتة (Instance vs. Static)، وكيفية العمل مع مصفوفات الكائنات. سنستكشف أيضًا تصاميم أصناف أكثر تعقيدًا مع كائنات متعددة متفاعلة.

جامعة الملك عبدالعزيز
كلية الحاسبات وتقنية المعلومات (FCIT)
إعداد د. عبدالمحسن المعلوي
آخر تحديث: ١١ فبراير ٢٠٢٦