تُعد البرمجة كائنية التوجه (Object-Oriented Programming - OOP) إحدى أكثر نماذج البرمجة استخدامًا على نطاق واسع في تطوير البرمجيات، ولكنها في الوقت نفسه من أكثر المفاهيم التي يساء فهمها. سيساعدك هذا المقال على اكتساب فهم راسخ لـ OOP في TypeScript من خلال استعراض ميزات اللغة التي تدعمها، ثم توضيح كيف تؤدي هذه الميزات بشكل طبيعي إلى المبادئ الأربعة الأساسية: inheritance (الوراثة)، polymorphism (تعدد الأشكال)، encapsulation (التغليف)، و abstraction (التجريد).
المتطلبات المسبقة
لتحقيق أقصى استفادة من هذا المقال، يجب أن تكون على دراية بما يلي:
- أساسيات JavaScript – المتغيرات، الدوال، الكائنات، والمصفوفات.
- بناء جملة TypeScript الأساسي – بما في ذلك types وكيف تختلف عن JavaScript العادية.
جدول المحتويات
- كيف تقرأ هذا المقال
- ميزات لغة TypeScript
- Objects (الكائنات)
- Classes، Attributes، و Methods
- Interfaces (الواجهات)
- Abstract Classes (الفئات المجردة)
- مبادئ البرمجة كائنية التوجه (Object-Oriented Programming Principles)
- Inheritance – Superclass و Subclass
- Polymorphism
- Encapsulation
- Abstraction
- الخاتمة
كيف تقرأ هذا المقال
لقد قمت بتنظيم هذا المقال إلى قسمين. يغطي القسم الأول ميزات لغة TypeScript التي تمكنك من تطبيق البرمجة كائنية التوجه (OOP). يناقش الجزء الثاني المفاهيم المستمدة من هذه الميزات والتي تؤدي إلى مبادئ OOP الأربعة: inheritance، polymorphism، encapsulation، و abstraction.
بينما يبدأ العديد من المعلمين والكتب والدورات بشرح هذه المبادئ، أفضل أن أبدأ بميزات اللغة نفسها. السبب بسيط: إنها هياكل رسمية – بعبارة أخرى، ملموسة. علاوة على ذلك، ستلاحظ طوال المقال أن مبادئ OOP تظهر بشكل طبيعي عند استخدام بنية اللغة بشكل صحيح.
ميزات لغة TypeScript
في هذا القسم، سنستكشف ميزات TypeScript التي تسهل تطبيق OOP. توجد آليات مماثلة في لغات أخرى موجهة للكائنات، مثل Java و C#، على الرغم من أنها قد تختلف في بناء الجملة مع الحفاظ على المفاهيم الأساسية.
Objects (الكائنات)
الكائن (object) هو نوع بيانات يخزن مجموعة من القيم المنظمة في أزواج key/value. قد تتضمن هذه القيم بيانات أولية (primitive data) أو كائنات أخرى. في المثال التالي، يخزن الكائن person معلومات مختلفة، مثل المفتاح name، الذي يحتوي على القيمة "Lucas" من النوع string، والمفتاح address، الذي يحمل كائنًا آخر.
const person = {
name: "Lucas", // primitive value of type string
surname: "Garcez",
age: 28, // primitive value of type number
address: { // object type containing the keys "city" and "country"
city: "Melbourne",
country: "Australia",
},
};
Classes، Attributes، و Methods
تعمل الفئة (class) كمخطط لإنشاء الكائنات. تحدد بنية الكائن وسلوكه من خلال سماته (attributes) وطرقه (methods). تحدد السمات بنية البيانات (المفاتيح وأنواع القيم)، بينما تحدد الطرق الإجراءات التي يمكن تنفيذها على تلك السمات.
class Person {
name: string; // attribute
surname: string; // attribute
age: number; // attribute
// constructor method (special method)
constructor(name: string, surname: string, age: number) {
this.name = name;
this.surname = surname;
this.age = age;
}
// method to obtain the full name: "Lucas Garcez"
getFullName() {
return `${this.name} ${this.surname}`;
}
}
Constructor Method
المنشئ (constructor) هو طريقة خاصة داخل الفئة. يتم استدعاؤه تلقائيًا عند إنشاء كائن جديد. المنشئات مسؤولة عن تهيئة سمات الفئة بالقيم المقدمة أثناء إنشاء الكائن. في TypeScript، يتم تعريف المنشئ باستخدام الكلمة المفتاحية constructor، كما ترى في الكود أعلاه.
Instance
يشير instance (النسخة) إلى كائن تم إنشاؤه من فئة. على سبيل المثال، باستخدام الفئة Person المذكورة أعلاه، يمكنك إنشاء كائن باسم lucas. لذلك، lucas هو instance من الفئة Person. لإنشاء instance من كائن في JavaScript أو TypeScript، تستخدم الكلمة المفتاحية new، كما هو موضح أدناه:
const lucas = new Person("Lucas", "Garcez", 28);
lucas.name; // "Lucas"
lucas.getFullName(); // "Lucas Garcez"
من المهم ملاحظة أنه يمكنك إنشاء كائنات متعددة (instances) من نفس الفئة. على الرغم من أن جميع هذه الكائنات تشترك في نفس البنية (السمات والطرق)، إلا أنها مستقلة وتشغل مساحات ذاكرة منفصلة داخل البرنامج. على سبيل المثال، عند إنشاء كائن جديد:
const maria = new Person("Maria", "Oliveira", 19);
لديك الآن instance جديد من الفئة Person لا يتعارض مع الكائن lucas الذي تم إنشاؤه مسبقًا. يحتفظ كل instance بقيمه وسلوكياته الخاصة، مما يضمن أن معالجة كائن واحد لا تؤثر على الآخرين.
Interfaces (الواجهات)
تحدد الواجهة (interface) عقدًا يحدد السمات والطرق التي يجب على الفئة تنفيذها. في TypeScript، يتم إنشاء هذه العلاقة باستخدام الكلمة المفتاحية implements. عندما تقوم فئة بتطبيق واجهة، يجب أن تتضمن جميع السمات والطرق المحددة بواسطة تلك الواجهة وأنواعها الخاصة.
في المثال التالي، لديك نظام مصرفي حيث يمكن للعميل أن يمتلك حساب CurrentAccount (حساب جاري) أو SavingsAccount (حساب توفير). يجب أن يلتزم كلا الخيارين بقواعد الحساب المصرفي العامة التي تحددها واجهة BankAccount.
// Contract defining the attributes and methods of a bank account
interface BankAccount {
balance: number;
deposit(amount: number): void;
withdraw(amount: number): void;
}
class CurrentAccount implements BankAccount {
balance: number;
// The class can have other attributes and methods
// beyond those specified in the interface
overdraftLimit: number;
deposit(amount: number): void {
this.balance += amount;
}
withdraw(amount: number): void {
if (amount <= this.balance) {
this.balance -= amount;
}
}
}
class SavingsAccount implements BankAccount {
balance: number;
deposit(amount: number): void {
// can have different logic from CurrentAccount
// but must respect the method signature,
// i.e., parameters (amount: number) and return type (void)
}
withdraw(amount: number): void {
// ...
}
}
Abstract Classes (الفئات المجردة)
تمامًا مثل الواجهات، تحدد الفئات المجردة (abstract classes) نموذجًا أو عقدًا يجب على الفئات الأخرى اتباعه. ولكن بينما تصف الواجهة بنية الفئة فقط دون توفير تطبيقات، يمكن للفئة المجردة أن تتضمن إعلانات طرق وتطبيقات ملموسة. على عكس الفئات العادية، لا يمكن إنشاء instance من الفئات المجردة مباشرة – فهي موجودة فقط كقاعدة يمكن للفئات الأخرى أن ترث منها طرقها وسماتها.
في TypeScript، تُستخدم الكلمة المفتاحية abstract لتعريف فئة مجردة. في المثال التالي، ستقوم بإعادة هيكلة النظام المصرفي عن طريق استبدال الواجهة بفئة مجردة لتعريف السلوك الأساسي لجميع الحسابات المصرفية.
// Abstract class that serves as the base for any type of bank account
abstract class BankAccount {
balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
// Concrete method (with implementation)
deposit(amount: number): void {
this.balance += amount;
}
// Abstract method (must be implemented by subclasses)
abstract withdraw(amount: number): void;
}
class CurrentAccount extends BankAccount {
withdraw(amount: number): void {
const fee = 2; // Current accounts have a fixed withdrawal fee
const totalAmount = amount + fee;
if (this.balance >= totalAmount) {
this.balance -= totalAmount;
} else {
console.log("Insufficient balance.");
}
}
}
class SavingsAccount extends BankAccount {
withdraw(amount: number): void {
if (this.balance >= amount) {
this.balance -= amount;
} else {
console.log("Insufficient balance.");
}
}
}
// ❌ Error! Cannot instantiate an abstract class
const genericAccount = new BankAccount(1000); // Error
// ✅ Creating a current account
const currentAccount = new CurrentAccount(2000); // uses the BankAccount constructor
currentAccount.deposit(500); // uses the deposit method from BankAccount
currentAccount.withdraw(300); // uses the withdraw method from CurrentAccount
// ✅ Creating a savings account
const savingsAccount = new SavingsAccount(1500); // uses the BankAccount constructor
savingsAccount.deposit(1100); // uses the deposit method from BankAccount
savingsAccount.withdraw(500); // uses the withdraw method from SavingsAccount
مبادئ البرمجة كائنية التوجه (Object-Oriented Programming Principles)
الآن بعد أن فهمت آليات اللغة الرئيسية، يمكنك إضفاء الطابع الرسمي على ركائز البرمجة كائنية التوجه التي توجه إنشاء أنظمة أكثر تنظيمًا وقابلية لإعادة الاستخدام وقابلية للتوسع.
Inheritance – Superclass و Subclass
الوراثة (Inheritance) هي آلية تسمح لفئة باشتقاق خصائص من فئة أخرى. عندما ترث فئة B من فئة A، فهذا يعني أن B تكتسب تلقائيًا سمات وطرق A دون الحاجة إلى إعادة تعريفها. يمكنك تصور هذه العلاقة كهيكل أب-ابن، حيث A هي الفئة الأب (superclass / base/parent class) و B هي الفئة الابن (subclass / derived/child class). يمكن للفئة الابن استخدام الموارد الموروثة، أو إضافة سلوكيات جديدة، أو تجاوز طرق الفئة الأب لتلبية احتياجات محددة.
لقد ناقشنا بالفعل الوراثة عند تعلم الفئات المجردة، ولكن يمكن تطبيق الوراثة أيضًا على الفئات الملموسة (concrete classes). وهذا يسمح بإعادة استخدام الكود وتخصيص السلوك.
// BankAccount is now a regular class where you define attributes and methods
// that will be reused by the child class CurrentAccount
class BankAccount {
balance: number = 0;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
deposit(amount: number): void {
this.balance += amount;
}
withdraw(amount: number): void {
if (amount <= this.balance) {
this.balance -= amount;
}
}
}
// CurrentAccount is a subclass of BankAccount, meaning
// it inherits its attributes and methods.
class CurrentAccount extends BankAccount {
overdraftLimit: number; // new attribute exclusive to CurrentAccount
// When specifying a constructor method for a subclass,
// we need to call another special method, "super".
// This method calls the superclass (BankAccount) constructor to ensure
// it is initialized before creating the CurrentAccount object itself.
constructor(initialBalance: number, overdraftLimit: number) {
super(initialBalance); // Must match the superclass constructor method signature
this.overdraftLimit = overdraftLimit;
}
// Even though the withdraw method already exists in the superclass (BankAccount),
// it is overridden here. This means every time a CurrentAccount
// object calls the withdraw method, this implementation will be used,
// ignoring the superclass method.
override withdraw(amount: number): void {
const totalAvailable = this.balance + this.overdraftLimit;
if (amount > 0 && amount <= totalAvailable) {
this.balance -= amount;
}
}
}
// Creating a CurrentAccount with an initial balance of $0.00
// and an overdraft limit of $100.
const currentAccount = new CurrentAccount(0, 100);
// Making a $200 deposit by calling the deposit method
// In this case, the method from BankAccount will be invoked
// since deposit was not overridden in CurrentAccount
currentAccount.deposit(200); // balance: 200
// Withdrawing $250 by calling the withdraw method
// In this case, the method from CurrentAccount will be invoked
// as it has been overridden in its definition
currentAccount.withdraw(250); // balance: -50
Polymorphism (تعدد الأشكال)
تعدد الأشكال (Polymorphism) هو مفهوم غالبًا ما يسبب الارتباك في البرمجة كائنية التوجه. ولكن في الممارسة العملية، هو مجرد نتيجة طبيعية لاستخدام الواجهات (interfaces) والوراثة (inheritance). مصطلح polymorphism مشتق من اليونانية ويعني "أشكال متعددة" (poly = many, morphos = forms). يسمح هذا المفهوم للكائنات من فئات مختلفة بالاستجابة لنفس استدعاء الطريقة (method call) ولكن بتطبيقات مميزة، مما يجعل الكود أكثر مرونة وقابلية لإعادة الاستخدام.
لتوضيح هذا المفهوم، دعنا نأخذ مثالًا عمليًا. افترض أن لديك دالة تسمى sendMoney، وهي مسؤولة عن معالجة معاملة مالية، تحويل مبلغ معين من الحساب A إلى الحساب B. الشرط الوحيد هو أن يتبع كلا الحسابين عقدًا مشتركًا، مما يضمن توفر طريقتي withdraw و deposit.
// BankAccount could be an interface, a concrete class,
// or an abstract class. For the sendMoney function, the specific implementation
// does not matter—only that BankAccount includes withdraw and deposit methods.
function sendMoney(sender: BankAccount, receiver: BankAccount, amount: number) {
sender.withdraw(amount);
receiver.deposit(amount);
}
const lucasAccount = new CurrentAccount(500, 200);
const mariaAccount = new SavingsAccount(300);
// transferring $100 from Lucas to Maria
sendMoney(lucasAccount, mariaAccount, 100);
- Polymorphic Methods: يتم استدعاء طريقتي withdraw و deposit داخل الدالة sendMoney دون الحاجة إلى معرفة الدالة ما إذا كانت تتعامل مع CurrentAccount أو SavingsAccount. تنفذ كل فئة withdraw وفقًا لقواعدها الخاصة، مما يوضح مفهوم تعدد الأشكال.
- Decoupling: لا تعتمد الدالة sendMoney على النوع المحدد للحساب المصرفي. يمكن استخدام أي فئة تمتد BankAccount (إذا كانت فئة) أو تطبق BankAccount (إذا كانت واجهة) دون الحاجة إلى تعديلات على الدالة sendMoney. بهذا النهج، تضمن المرونة وقابلية إعادة استخدام الكود، حيث يمكن إدخال أنواع حسابات جديدة دون التأثير على وظيفة sendMoney.
Encapsulation (التغليف)
التغليف (Encapsulation) هو أحد المبادئ الأساسية لـ OOP، ولكن يمكن تطبيق مفهومه على أي نموذج برمجة. يتضمن إخفاء تفاصيل التنفيذ الداخلية لوحدة (module)، فئة (class)، دالة (function)، أو أي مكون برمجي آخر، مع كشف ما هو ضروري فقط للاستخدام الخارجي. وهذا يحسن أمان الكود، قابليته للصيانة، ووحدويته (modularity) عن طريق منع الوصول غير المصرح به وضمان التفاعلات المتحكم بها.
Access Modifiers – public، private، و protected
في OOP، التغليف ضروري للتحكم في رؤية والوصول إلى الطرق والسمات داخل الفئة. في TypeScript، يتم تحقيق ذلك باستخدام معدّلات الوصول (access modifiers)، والتي يتم تعريفها بواسطة الكلمات المفتاحية public، protected، و private.
- public – يسمح بالوصول إلى السمة أو الطريقة من أي مكان، سواء داخل الفئة أو خارجها. هذه هي الرؤية الافتراضية، مما يعني أنه إذا لم يتم تحديد معدّل وصول في الكود، فإن TypeScript يفترضه كـ public.
- protected – يسمح بالوصول داخل الفئة وفئاتها الفرعية (subclasses) ولكنه يمنع الوصول الخارجي.
- private – يقيد الوصول إلى السمة أو الطريقة داخل الفئة نفسها فقط.
export class Person {
private firstName: string; // Accessible only within the class itself
private lastName: string; // Accessible only within the class itself
protected birthDate: Date; // Accessible by subclasses but not from outside
constructor(firstName: string, lastName: string, birthDate: Date) {
this.firstName = firstName;
this.lastName = lastName;
this.birthDate = birthDate;
}
// Public method that can be accessed from anywhere
public getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
// The Professor class inherits from Person and can access
// attributes and methods according to their access modifiers.
class Professor extends Person {
constructor(firstName: string, lastName: string, birthDate: Date) {
super(firstName, lastName, birthDate); // Calls the superclass (Person) constructor
}
getProfile() {
this.birthDate; // ✅ Accessible because it is protected
this.getFullName(); // ✅ Accessible because it is public
this.firstName; // ❌ Error! Cannot be accessed because it is private in the Person class
this.lastName; // ❌ Error! Cannot be accessed because it is private in the Person class
}
}
function main() {
// Creating an instance of Professor
const lucas = new Professor("Lucas", "Garcez", new Date("1996-02-06"));
// Testing direct access to attributes and methods
lucas.birthDate; // ❌ Error! birthDate is protected and can only be accessed within the class or subclasses
lucas.getFullName(); // ✅ Accessible because it is a public method
lucas.firstName; // ❌ Error! firstName is private and cannot be accessed outside the Person class
lucas.lastName; // ❌ Error! lastName is also private and inaccessible outside the Person class
}
جدول معدّلات الوصول (Access Modifiers Table)
- public:
- الوصول داخل الفئة: ✅ نعم
- الوصول في الفئة الفرعية: ✅ نعم
- الوصول خارج الفئة: ✅ نعم
- protected:
- الوصول داخل الفئة: ✅ نعم
- الوصول في الفئة الفرعية: ✅ نعم
- الوصول خارج الفئة: ❌ لا
- private:
- الوصول داخل الفئة: ✅ نعم
- الوصول في الفئة الفرعية: ❌ لا
- الوصول خارج الفئة: ❌ لا
Abstraction (التجريد)
غالبًا ما يسبب مفهوم التجريد (Abstraction) ارتباكًا لأن معناه يتجاوز السياق التقني. إذا بحثت عن تعريف الكلمة في اللغة الإنجليزية، فإن قاموس Cambridge Dictionary يعرف "abstract" على النحو التالي:
"Something that exists as an idea, feeling, or quality, rather than as a material object."
يمكن تطبيق هذا التعريف مباشرة على OOP:
التجريد يمثل فكرة أو مفهومًا دون الخوض في تفاصيل التنفيذ الملموسة.
تصف العديد من المراجع عبر الإنترنت التجريد بأنه "إخفاء تفاصيل التنفيذ"، وهو ما قد يكون مضللاً لأن هذا المفهوم يرتبط ارتباطًا وثيقًا بالتغليف (encapsulation). في OOP، التجريد لا يعني إخفاء التفاصيل، بل يعني تحديد العقود من خلال الفئات المجردة (abstract classes) والواجهات (interfaces).
// Abstraction using interface
interface BankAccountInterface {
balance: number;
deposit(amount: number): void;
withdraw(amount: number): void;
}
// Abstraction using class
abstract class BankAccountClass {
balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
// Concrete method (with implementation)
deposit(amount: number): void {
this.balance += amount;
}
// Abstract method (must be implemented by subclasses)
abstract withdraw(amount: number): void;
}
في الأمثلة أعلاه، كل من BankAccountInterface و BankAccountClass هي أمثلة على التجريد حيث تحدد عقودًا يجب تنفيذها من قبل أولئك الذين يستخدمونها.
الخاتمة
على الرغم من أن تعلم البرمجة كائنية التوجه ليس بالأمر السهل، آمل أن يكون هذا المقال قد ساعد في توضيح أساسيات OOP والمواضيع المتقدمة. إذا كنت ترغب في مواصلة تعلم TypeScript و OOP، فإنني أوصي بشدة بقراءة كتاب Martin Fowler بعنوان Refactoring: Improving the Design of Existing Code. يحتوي هذا الكتاب على كتالوج ضخم من تقنيات إعادة الهيكلة، وتحتوي الطبعة الثانية على جميع أمثلة الكود مكتوبة بلغة TypeScript، ويستخدم العديد منها ميزات ومبادئ OOP المذكورة هنا.