فكرة الـ Classes والـ Objects
السلام عليكم ورحمة الله وبركاته
الفهرس
المقدمة
سنبدأ سلسلة أو دورة صغيرة مكونة من 5
مقالات نتحدث عن أهم المفاهيم في عالم الـ OOP
وسنبدأ بأول مقالة بالحديث عن الـ Classes
الكلاسات هي بداية او مدخل للـ OOP
البرمجة الشيئية او برمجة كائنية التوجه
أحب أن تكون هذه المقالة مقدمة لبعض أساسيات الكلاسات
ملحوظة
: سنتطبق مفاهيم الـOOP
باستخدام لغةTypescript
، لكن لن نتطرق لتفاصيل اللغة او مميزات اللغة أو أي زيادة تقدمها اللغة في الـOOP
، كل ما سنركز عليه هو المفهوم العام للـOOP
الثابتة في عالم الـOOP
وليست الأمور المستحدثة الخاصة بلغة معينة
ما هو الـ OOP
الـ OOP
كمفهوم فهو يسمح لك بتصنيف أو تمثيل أشياء معينة لها خواص او مميزات داخل البرمجة
على سبيل المثال البرنامج لا يفهم ما هي الحيوانات البرمجة بشكل عام لا تعرف شيء يدعى Animal
انه مصطلح نستعمله نحن البشر، لكن البرنامج لا يعرفه، الأمر مماثل للسيارات البرنامج لا يعرف شيء يدعى سيارة
بمعنى اخر أي شيء نفهمه نحن البشر البرنامج لن يفهمه
فلذلك نحتاج لشيء لوصفه للبرنامج ليعرف كيف يتعامل معاه
هنا يأتي دور الـ class
هو الذي من خلاله نصنف أو نمثل للبرنامج الشيء الغريب عليه ونبدأ في وصفه
نمثله أي نعطيه خواص وافعال وهي المتغيرات التي تمثل خواصه والدوال التي ستمثل الافعال
إنشاء كلاس يمثل الـ Animal
لنأخذ مثال، نحن نريد ان نصف للبرنامج ما هو الـ Animal
class Animal {}
نبدأ بتعريف الـ Animal
لذا نكتب class
ثم اسم الشيء الذي نريد أن نمثله للبرنامج وهو في حالتنا هذه سيكون الـ Animal
الـ class
هو كحاوية يحتوي على المتغيرات والدوال التي تصف الشيء
- تعريف الكلاسات والمفاهيم الذي تدور حوله ومفاهيم الـ
OOP
بشكل عام كلها نفس الشيء في جميع اللغات، نحن سنطبق باستخدام لغةTypescript
لكن كل المفاهيم التي ستتعلمها ستجدها في كل اللغات التي تدعم الـOOP
إن شاء الله
الآن البرنامج يعرف أن هناك شيء يدعى Animal
لكن هو حاليًا لا يفهم ما هو وما هي مواصفاته وخواصه، لأننا لم نحددها أو نعرفها للبرنامج بعد
نريد أن نعطي صفة لهذا الكلاس ليعرف البرنامج كيف يتعامل معه لذا لنعطيه متغير يمثل الاسم
class Animal {
public name: string | undefined;
}
هنا أنشأنا كلاس جديد يدعى Animal
وكتبنا بداخله متغير وحيد يعبر عن خواصه كالاسم name
وطبعا نوعه string
، وهنا كتبنا أن هذه الصفة تكون public
سنشرح ما معنى public
فيما بعد في هذه المقالة
لأننا نتعامل مع الـ Typescript
فيجب علينا قول أن قيمة المتغير name
ستكون أما string
أو undefined
وسبب أننا نقول undefined
لأننا لم نعطي قيمة له بعد
وبطبع عندما ندخل ونشرح الـ constructor
سنعطي قيمة له وبالتالي لن نحتاج لكتابة undefined
بعد ذلك
لكن ما هو الـ constructor
؟ سيأتي ذكره في المقالة فيما بعد
دعونا نجعلها هكذا مؤقتًا
كيف نستخدم الكلاس
class Animal {
public name: string | undefined;
}
let cat = new Animal();
console.log(cat.name); // OUTPUT: undefined
هنا كتبنا let cat = new Animal();
هذا معناه انك عملت متغير جديد اسمه cat
من نوع Animal
هنا الـ cat
اسمه instance
من الـ Animal
ومعنى instance
أنه نسخة من الكلاس
وستجد أنه يطلق عليه أيضًا object
ومعناه شيء يحتوى على خواص معينة
يمكنك أن تتخيل الكلاس كأنه مصنع لديه مخطط بتفاصيل الاشياء التي سيصنعها
والـ object
هو إنتاج هذا المصنع
بمعنى آخر لدينا كلاس يدعى Car
ويحتوى على خصائص مثل model
,color
وتاريخ التصنيع و ... إلخ من الصفات والخواص
عندما ننشيء object
فالمصنع ينشيء سيارة جديدة بنفس المخطط وتحتوي على المواصفات التي في الكلاس
- أي شيء داخل الكلاس نستطيع استدعاءه أو الوصول إليه عن طريق
dot
النقطة.
التي نكتبها بعد اسم الـobject
بالتالي المتغير name
الذي داخل الكلاس نستطيع استدعاءه أو الوصول إليه عن طريق الـ dot
class Animal {
public name: string | undefined;
}
let cat = new Animal();
cat.name = 'Khalid Kashmiri';
console.log(cat.name); // OUTPUT: Khalid Kashmiri
هنا أنشأنا object
اسمه cat
من النوع Animal
ثم أسندنا قيمة للمتغير name
الذي بداخل cat
وسمينا هذا القط الجميل خالد كشميري Khalid Kashmiri
وكما قلنا للوصول لخواص الـ object
نستعمل الـ dot
كهذا كما فعلنا cat.name = 'Khalid Kashmiri';
نستطيع إنشاء أكثر من object
من نفس الكلاس، مثل أن المصنع ينشيء أكثر من نموذج من نفس المخطط
class Animal {
public name: string | undefined;
}
let cat = new Animal();
cat.name = 'Khalid Kashmiri';
console.log(cat.name); // OUTPUT: Khalid Kashmiri
let dog = new Animal();
dog.name = 'Khadir Karawita';
console.log(dog.name); // OUTPUT: Khadir Karawita
هنا أنشأنا object
اسمه cat
وواحد اسمه dog
وكل واحد عدلنا في اسمه بشكل مستقل عن الأخر
وما أقصده بشكل مستقل أن التعديلات في cat
لا تأثر على الـ dog
، كل واحد له متغير name
مستقل عن الاخر
هنا في الـ cat
اعطيناه اسم Khalid Kashmiri
والـ dog
اسمه Khadir Karawita
يمكنك إنشاء دوال داخل الكلاس فالأمر لا يختصر على المتغيرات فقط بل يمكنك إنشاء عدة دوال
والدوال عندما تكون داخل الكلاس تدعى method
class Animal {
public name: string | undefined;
public age: number | undefined;
public printInfo() {
console.log(`Name is: ${this.name}, his age: ${this.age}.`);
}
}
let cat = new Animal();
cat.name = 'Khalid Kashmiri';
cat.age = 3;
cat.printInfo(); // OUTPUT: Name is: Khalid Kashmiri, his age: 3.
هنا لدينا دالة تدعى printInfo
وظيفتها طباعة جملة تعطينا معلومات عن هذا الـ object
هنا this
تمثل الـ object
الذي سيستخدم الدالة، وفي حالتنا الـ object
الذي استخدم دالة الـ printInfo
كان الـ cat
لذا الـ this
هنا تمثل الـ cat
في هذه اللحظة
ملحوظة
: للوصول لأي متغير او دالة داخل الكلاس في لغة الـjavascript
والـTypescript
من داخل الكلاس يجب أن نستعمل الـthis
دئمًا عندما نريد الوصول لاي عنصر داخل الكلاس في عالم الـoop
الـthis
دائما ما تشير للـobject
الذي يستخدم عناصر الكلاس مثل الدوال في حالتنا كان لديناobject
يدعىcat
وقمنا باستدعاء دالةprintInfo
من خلال الـcat
كهذاcat.printInfo()
بالتالي فالـthis
داخل دالة الـprintInfo
ستكون تمثلobject
الـcat
في لغات الـ
oop
الأخرى ليس الزاميًا أن نستعمل الـthis
، ففي اللغات الأخرى يمكننا أن نستغني عنها ويمكننا الوصول لعناصر الكلاس بدونها
فيمكننا استخدامname
دون الحاجة لاستخدامthis.name
وهذا الشائع في باقي اللغات
أما في لغة الـTypescript
والـjavascript
إلزامي أن نستعملthis
معلومة إضافية وهي أننا عندما ننشئ object
من الكلاس هكذا
let cat = new Animal();
فأسم الـ object
يتم تخزينه في الذاكرة Memory Ram
في مكان تعرفه يسمى Stack
والـ object
نفسه يتم تخزينه في مكان آخر يسمى Heap
واسم الـ object
في الـ Stack
يكون مجرد reference
للـ object
الذي يتم تخزينه في الـ Heap
Stack | Heap
|
|
| new Animal()
+------------+ | +------------+
| cat | ----------> | name: null |
+------------+ | | age: null |
| +------------+
|
|
|
إنشاء الـ Constructor
الـ constructor
معناه الحرفي هو مُنشيء بمعنى أنه هو الذي يُنشيء الـ object
من الكلاس
class Animal {
public name: string | undefined;
}
let cat = new Animal(); // this is a constructor
لنركز على هذا السطر let cat = new Animal();
هنا أنت تستخدم constructor
وهو Animal()
الـ constructor
عند استدعاءه يحمل نفس اسم الكلاس
فيكون new Animal();
معناه كالتالي أنشيء object جديد من الكلاس Animal
وليكن cat
مجرد حاوية لهذا الـ object
بالتالي عندما نكتب هذا السطر new Animal();
نكون قد أنشأنا object
بالفعل في الـ memory ram
واستعملنا cat
ليحمل عنوان الـ object
في الـ memory ram
فالـ cat
هنا مجرد reference
للوصول للـ object
لنعود للرسالة التي ظهرت لنا عندما أنشأنا متغير بهذا الشكل
Property 'name' has no initializer and is not definitely assigned in the constructor.
ترجمة الرسالة الحرفية هي كالآتي:
المتغير
name
لا يملك قيمة وأيضًا لا يتم إسناد قيمة له في الـconstructor
- كيف يتم إسناد قيمة من خلال الـ
constructor
؟
let cat = new Animal();
أول شيء ستلاحظه في الـ constructor
هو أنه مثل الدالة له اقواس ()
- بالتالي هل يمكننا أن نرسل له قيم المتغيرات من خلاله؟ الاجابة نعم
علينا أن ندرك شيء مهم هنا
class Animal {}
let obj = new Animal(); // Animal() is a default constructor (empty constructor)
عندما تنشيء كلاس حتى ولو كان فارغًا فاللغة تضع في داخله constructor
خفي لا تراه
هو يكون constructor
فارغ لا يفعل شيء سوء إنشاء object
فارغ
يسمى بـ default constructor
أو empty constructor
يمكنك أن تتخيله كهذا
class Animal {
constructor() {} // hidden constructor, doesn't do anything
}
let obj = new Animal(); // we are use the default constructor here already
default constructor
لا يستقبل أي شيء ولا يقوم بعمل أي شيء
- إذًا يمكننا إنشاء
constructor
آخر ينشيءobject
آخر بالشكل الذي نريده ؟ الإجابة نعم
class Animal {
public name: string;
constructor(name: string) {
this.name = name;
}
}
let cat = new Animal('Khalid Kashmiri'); // this is our constructor
console.log(cat.name); // OUTPUT: Khalid Kashmiri
الـ constructor
يشبه الدالة كثيرًا, لكنه لا يرجع أي شيء
نكتب constructor
ثم نتعامل معه معاملة الدالة، هنا الـ constructor
الذي أنشأناه يقوم بأخذ name
من نوع string
ثم يسندها للـ name
الخاص بالكلاس نفسه
لاحظ أننا جعلنا الـ name
يقبل string
وأزلنا الـ undefined
, لماذا ؟
لأنه أصبح لدينا constructor
يسند قيمة للـ name
هنا استعملنا الـ this
مجددًا للوصول لعناصر الكلاس وقلنا أنها تمثل الـ object
الذي يستخدم عناصر الكلاس
وقلنا أن في لغة الـ javascript
والـ Typescript
يجب أن نستعمل الـ this
هنا لاحظ أننا استطعنا التفريق بين الـ name
الذي بداخل الكلاس والـ name
الخاص بالـ constructor
عن طريق الـ this
تذكر أن في لغة الـ
Typescript
والـjavascript
إلزامي أن نستعملthis
حتى ولو كان اسم المتغير في الـconstructor
مختلف عن اسمه في الكلاس هكذاconstructor(n: string) { this.name = n; }
هنا المتغيرين مختلفين ولا يوجد أي تصادم بين الاسمين لكن يجب أن نستعملthis
- ملخص ما أريدك أن تعرفه عن الـ
this
أنها دائما ما تشير للـobject
الذي استعملها - و يجب أن نستعملها دائما عندما نريد أن نصل لاي عنصر داخل الكلاس سواء متغير أو دالة
فمعنى هذا السطر let cat = new Animal('Khalid Kashmiri')
أننا أنشأنا object
باستخدام الـ constructor
الذي أنشأناه نحن
والذي يستقبل متغير يدعى name
من النوع string
لذا أرسلنا مع الـ constructor
قيمة لهذا المتغير وهو Khalid Kashmiri
ثم قام الـ constructor
في داخله بعمل اسناد بقيمة الـ name
المرسلة في الـ constructor
لقيمة المتغير name
الخاص بالـ object
وللتفريق ما بينهما استعملنا this
وهي تمثل الـ object
الذي تم إنشاءه للتو
- هل هذا معناه أنه لا يمكننا إنشاء
object
من الـconstructor
الفارغ ؟ لنجرب!
class Animal {
public name: string;
constructor(name: string) {
this.name = name;
}
}
let cat = new Animal('Khalid Kashmiri');
let dog = new Animal(); // ERROR!!
هنا سيعترض البرنامج ويقول لك
Expected 1 arguments, but got 0.
هنا هو يتوقع أن ترسل له قيمة للمتغير الذي يستقبله الـ constructor
لأنه يستقبل قيمة للـ name
وبالنسبة للـ constructor
الفارغ الذي يتم إنشاءه تلقائيا لنا عند تعريف الكلاس، يختفي من الوجود عندما ننشيء constructor
خاص بنا
- مع العلم أن في اغلب لغات الـ
oop
يمكنك أن تنشيء أكثر منconstructor
ونفرق بينهم عن طريق مفهوم الـfunction overloading
، لكن بما أن الـTypescript
لا تدعم أكثر منconstructor
فلن نتطرق لهذا الأمر
لننظر إلى الكود التالي
class Animal {
public name: string;
public age: number;
public weight: number;
constructor(name: string, age: number, weight: number) {
this.name = name;
this.age = age;
this.weight = weight;
}
public printInfo() {
console.log(
`Name is: ${this.name}, his age: ${this.age}, his weight: ${this.weight} kg.`
);
}
}
let cat = new Animal('Khalid Kashmiri', 3, 4.1);
cat.printInfo(); // OUTPUT: Name is: Khalid Kashmiri, his age: 3, his weight: 4.1 kg.
ستلاحظ أننا كلما أنشأنا متغيرات داخل الكلاس، نقوم بإسناد قيمة لها داخل الـ constructor
يمكننا اختصار الأمر في خطوة واحدة كالتالي
class Animal {
constructor(
public name: string,
public age: number,
public weight: number
) {}
public printInfo() {
console.log(
`Name is: ${this.name}, his age: ${this.age}, his weight: ${this.weight} kg.`
);
}
}
let cat = new Animal('Khalid Kashmiri', 3, 4.1);
cat.printInfo(); // OUTPUT: Name is: Khalid Kashmiri, his age: 3, his weight: 4.1 kg.
هكذا ننشيء متغيرات داخل الـ constructor
نسند لها قيمة في نفس اللحظة
هذه الميزة موجودة في بعض اللغات
- سنفعل نفس الأمر لكن على متغير واحد لتبسيط الشكل لا أكثر
كل ما فعلناه هو أننا حولنا هذا
class Animal {
public name: string;
constructor(name: string) {
this.name = name;
}
}
لهذا
class Animal {
constructor(public name: string) {}
}
Access Modifiers
الـ Access Modifiers
لها انواع وهي الـ public, private, protect, readonly
نضعها قبل المتغيرات أو الدوال
public
المتغيرات او الدوال من نوع public
يستطيع الـ object
الوصول لها عن طريق الـ dot
من خارج الكلاس بشكل اعتيادي
class Animal {
public name: string | undefined;
public printInfo() {
console.log(`Name is: ${this.name}.`);
}
}
let cat = new Animal();
cat.name = 'Khalid Kashmiri';
cat.printInfo();
// printInfo and name are public, so the object cat can access it outside the class
- واذا لم نحدد النوع، يكون المتغير أو الدالة
public
بشكل افتراضي
class Animal {
name: string | undefined; // public by default
// public by default
printInfo() {
console.log(`Name is: ${this.name}.`);
}
}
let cat = new Animal();
cat.name = 'Khalid Kashmiri';
cat.printInfo();
// printInfo and name are public by default,
private
هو كما يوحي الاسم فهو يمنع الـ object
من الوصول لاي شيء نوعه private
class Animal {
private name: string; // It is private!!!
constructor(name: string) {
this.name = name; // allow write & edit it inside class
}
public printInfo() {
console.log(`Name is: ${this.name}.`);
}
public setName(name: string) {
this.name = name; // allow write & edit it inside methods
}
}
let cat = new Animal('Khalid Kashmiri');
cat.printInfo(); // No Error because is a public
console.log(cat.name); // ERROR!!
cat.name = 'Khadir Karawita'; // ERROR!!
cat.setName('Khadir Karawita'); // Ok, we can edit it inside a method
هنا جعلنا الـ name
يكون private
بالتالي أي object
الكلاس لن يستطيع استدعاءه
وإن حاول سيقال له:
Property 'name' is private and only accessible within class 'Animal'.
ستلاحظ أن الدوال والـ constructor
هم فقط من يستطيعون الوصول للـ name
والتعديل عليه
- بالتالي أي شيء
private
يمكن الوصول اليه وتعديله داخل الكلاس فقط لكن لا يمكن لأيobject
استدعاءه او التعديل عليه
ويفضل دائمًا أن تجعل جميع المتغيرات private
والـ object
يستطيع الوصول له عن طريق الدوال كوسيط بينه وبين المتغيرات مثل ما فعلنا هنا cat.setName('Khadir Karawita');
- وسنتكلم عن هذا الأمر بالتفصيل في مقالة الـ
Encapsulation
ملحوظة
: حتى لو كان هناك كلاس يرث من كلاس آخر فالكلاس التاني لا يستطيع هو كذلك الوصول الى الـprivate
الخاص بالكلاس الأول وهذا سنناقشه أكثر وبالتفصيل في مقالة عن الـInheritance
protect
هو مثله مثل الـ
private
لكن يمكن للكلاس الذي ورثه أن يصل ويستدعي أي شيءprotect
وهذا أيضًا سنناقشه أكثر وبالتفصيل في مقالة عن الـInheritance
readonly
هو كما يوحي الاسم فهو يمنع الـ object
من تعديله لكن يسمح له بالوصول له وطباعته
class Animal {
readonly name: string; // It is read-only
constructor(name: string) {
this.name = name; // allow write & edit it ONLY inside the constructor
}
public printInfo() {
console.log(`Name is: ${this.name}.`);
}
public setName(name: string) {
this.name = name; // ERROR!!, we can't write it inside methods
}
}
let cat = new Animal('Khalid Kashmiri');
cat.printInfo(); // OUTPUT: Name is: Khalid Kashmiri.
console.log(cat.name); // Ok, we can read it
cat.name = 'Khadir Karawita'; // ERROR!!, we can't write it
cat.setName('Khadir Karawita'); // ERROR!!, we can't write it inside methods also
لاحظ أننا يمكننا طباعة cat.name
لكن لا يمكننا تعديله حتى داخل الدوال الخاصة بالكلاس
يمكننا تعديله فقط مرة واحدة داخل الـ constructor
وهذا بديهي جدًا
يمكننا استخدام الـ Access Modifiers
الأخرى مع الـ readonly
, لو لم نكتب أي شيء معها فسيفترض أنها public
مثل الأمثلة السابقة، لكن يمكننا جعلها private
وستتصرف مثل أي شيء private
لا يمكننا استخدامه مع الـ object
لكن يمكننا استخدامه داخل الكلاس
الكود التالي يوضح الأمر private readonly
، بمجرد القراءة المفترض أنك فهمت الفكرة الأساسية
class Animal {
private readonly name: string; // It is a private read-only
constructor(name: string) {
this.name = name; // allow write & edit it ONLY inside the constructor
}
public printInfo() {
console.log(`Name is: ${this.name}.`); // We can read it inside methods
}
public setName(name: string) {
this.name = name; // ERROR!!, we can't write it inside methods
}
}
let cat = new Animal('Khalid Kashmiri');
cat.printInfo(); // OUTPUT: Name is: Khalid Kashmiri.
console.log(cat.name); // ERROR!!, name is private now
cat.setName('Khadir Karawita'); // ERROR!!, we can't write it inside methods also
Static
الـ static
هو جعل المتغيرات أو الدوال ثابتة على مستوى الكلاس وليس الـ object
بمعنى أن أي شيء سواء متغيرات أو دوال تكون static
فإننا نستطيع الوصول لها عن طريق اسم الكلاس نفسه وليس عن طريق object
class Animal {
public name: string;
public static numberOfAnimal: number = 0; // Only the class itself can access it
constructor(name: string) {
this.name = name;
Animal.numberOfAnimal += 1; // Use the class itself instead of 'this'
}
public printInfo() {
console.log(`Name is: ${this.name}.`);
}
}
let cat = new Animal('Khalid Kashmiri');
let dog = new Animal('Khadir Karawita');
console.log(Animal.numberOfAnimal); // OUTPUT: 2
هنا لدينا متغير يدعى numberOfAnimal
وكما يوحي الاسم فهو سيكون عدد الحيوانات التي أنشأناها
جعلناه static
بالتالي يمكن استدعاءه عن طريق اسم الكلاس نفسه وليس عن طريق الـ object
ويكنك أن تلاحظ ذلك في الـ constructor
لقد استعملنا Animal.numberOfAnimal
بدلًا من this.numberOfAnimal
وهذا منطقي أن يكون numberOfAnimal
مرئي على مستوى الكلاس
لانه يحسب عدد الـ object
التي يُنشيء من الكلاس
وليس من المنطق هنا أن تقول cat.numberOfAnimal
أن حاولت أن تجرب أن تستدعي numberOfAnimal
عن طريق الـ object
فهذه الرسالة سترحب بك
Property 'numberOfAnimal' does not exist on type 'Animal'. Did you mean to access the static member 'Animal.numberOfAnimal' instead?
بمعنى أن المتغير numberOfAnimal
نوعه static
بالتالي تستطيع الوصول إليه من خلال اسم الكلاس
باختصار إن أردت أن تنشيء متغيرات أو دوال تستطيع الوصول لها عن طريق اسم الكلاس وليس عن طريق الـ
object
لأنك ربما تريد أن تجعلها معلومات عامة وثابتة على مستوى الكلاس لأي سبب من الأسباب
الدوال بالطبع يمكنها أن تكون static
الأمر ينطبق عليها مثل المتغيرات
class Animal {
public name: string;
private static numberOfAnimal: number = 0; // Only the class itself can access it
constructor(name: string) {
this.name = name;
Animal.numberOfAnimal += 1; // Use the class itself instead of 'this'
}
public printInfo() {
console.log(`Name is: ${this.name}.`);
}
public static getNumberOfAnimal() {
// Only the class itself can access it
return Animal.numberOfAnimal; // Use the class itself instead of 'this'
}
}
let cat = new Animal('Khalid Kashmiri');
let dog = new Animal('Khadir Karawita');
console.log(Animal.getNumberOfAnimal()); // OUTPUT: 2
console.log(cat.getNumberOfAnimal()); // ERROR!!
هنا جعلنا الـ numberOfAnimal
يكون private
ثم أنشأنا دالة تدعى getNumberOfAnimal
تكون public
وstatic
بالتالي نستطيع الوصول لها عن طريق اسم الكلاس نفسه وليس عن طريق الـ object
وهكذا نكون انتهينا من أول مقالة في سلسلتنا الصغيرة المكونة من 5
مقالات نتحدث عن أهم المفاهيم في عالم الـ OOP
تحدثنا عن أهم النقاط في الـ Classes
والـ Objects
و في المقالة التالية سنتحدث عن الـ Encapsulation