فكرة الـ Classes والـ Objects

السلام عليكم ورحمة الله وبركاته

وقت القراءة: ≈ 15 دقيقة

المقدمة

سنبدأ سلسلة أو دورة صغيرة مكونة من 5 مقالات نتحدث عن أهم المفاهيم في عالم الـ OOP

وسنبدأ بأول مقالة بالحديث عن الـ Classes
الكلاسات هي بداية او مدخل للـ OOP البرمجة الشيئية او برمجة كائنية التوجه أحب أن تكون هذه المقالة مقدمة لبعض أساسيات الكلاسات

ملحوظة: سنتطبق مفاهيم الـ OOP باستخدام لغة Typescript، لكن لن نتطرق لتفاصيل اللغة او مميزات اللغة أو أي زيادة تقدمها اللغة في الـ OOP، كل ما سنركز عليه هو المفهوم العام للـ OOP الثابتة في عالم الـ OOP وليست الأمور المستحدثة الخاصة بلغة معينة

ما هو الـ OOP

الـ OOP كمفهوم فهو يسمح لك بتصنيف أو تمثيل أشياء معينة لها خواص او مميزات داخل البرمجة

على سبيل المثال البرنامج لا يفهم ما هي الحيوانات البرمجة بشكل عام لا تعرف شيء يدعى Animal
انه مصطلح نستعمله نحن البشر، لكن البرنامج لا يعرفه، الأمر مماثل للسيارات البرنامج لا يعرف شيء يدعى سيارة
بمعنى اخر أي شيء نفهمه نحن البشر البرنامج لن يفهمه

فلذلك نحتاج لشيء لوصفه للبرنامج ليعرف كيف يتعامل معاه هنا يأتي دور الـ class هو الذي من خلاله نصنف أو نمثل للبرنامج الشيء الغريب عليه ونبدأ في وصفه
نمثله أي نعطيه خواص وافعال وهي المتغيرات التي تمثل خواصه والدوال التي ستمثل الافعال

إنشاء كلاس يمثل الـ Animal

لنأخذ مثال، نحن نريد ان نصف للبرنامج ما هو الـ Animal

class Animal {}

نبدأ بتعريف الـ Animal لذا نكتب class ثم اسم الشيء الذي نريد أن نمثله للبرنامج وهو في حالتنا هذه سيكون الـ Animal الـ class هو كحاوية يحتوي على المتغيرات والدوال التي تصف الشيء

الآن البرنامج يعرف أن هناك شيء يدعى 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 فالمصنع ينشيء سيارة جديدة بنفس المخطط وتحتوي على المواصفات التي في الكلاس

بالتالي المتغير 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

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 لا يستقبل أي شيء ولا يقوم بعمل أي شيء

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

فمعنى هذا السطر let cat = new Animal('Khalid Kashmiri') أننا أنشأنا object باستخدام الـ constructor الذي أنشأناه نحن
والذي يستقبل متغير يدعى name من النوع string لذا أرسلنا مع الـ constructorقيمة لهذا المتغير وهو Khalid Kashmiri
ثم قام الـ constructor في داخله بعمل اسناد بقيمة الـ name المرسلة في الـ constructor لقيمة المتغير name الخاص بالـ object وللتفريق ما بينهما استعملنا this وهي تمثل الـ object الذي تم إنشاءه للتو

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 خاص بنا

لننظر إلى الكود التالي

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
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 يستطيع الوصول له عن طريق الدوال كوسيط بينه وبين المتغيرات مثل ما فعلنا هنا cat.setName('Khadir Karawita');

ملحوظة: حتى لو كان هناك كلاس يرث من كلاس آخر فالكلاس التاني لا يستطيع هو كذلك الوصول الى الـ 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