الـ Polymorphism، اختلاف وتعدد الأشكال لشيء واحد

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

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

المقدمة

يسعدني أننا وصلنا لآخر مقالة من سلسلتنا الصغيرة عن أهم مفاهيم الـ OOP
سنشرح في هذه المقالة عن مفهوم الـ Polymorphism وهو مفهوم بسيط لكن استخداماته وتطبيقاته ستراها في كل شيء
ستجد أنه حين تتعمق في المشاريع الكبيرة وتبدأ في تعلم واستخدام الـ Design Patterns ستجد أنك تتعامل مع الـ Polymorphism بكثرة

في حقيقة الأمر نحن تعاملنا وشرحنا القليل عن الـ Polymorphism عندما كنا نشرح المفاهيم السابقة
هذا بسبب أنك ستجد أن المفاهيم الأربعة الخاصة بالـ OOP متقاربة جدًا وتتداخل مع بعضها البعض
وهذا الأساس أن تفهم هذه المفاهيم وتحاول أن توظفها مع بعضها لتخدم الفكرة والمشروع التي تريد تنفيذه

تعريف الـ Polymorphism

مفهوم الـ Polymorphism يمثل قدرة أي شيء مهما كان سواء كان دالة او object على أخذ عدة أشكال في آن واحد
بمعنى إن رأيت شيء ما مهما كان يستطيع تغير تصرفاته أو افعاله أو خواصه بناءًا على موقف أو حالة معينة فهذا هو مفهوم الـ Polymorphism

من امثلة ان الدالة ممكن تأخذ أكثر من شكل هو فكرة الـ Overloading أو فكرة الـ Overriding أو الـ Generics وسنفهم معنى هذا بالتفصيل

ملحوظة: يجب أن تفهم المفهوم نفسه وليس تطبيقات المفهوم، لأن المفهوم ثابت لكن تطبيقاته تتنوع وتختلف
نحن فقط نشرح التطبيقات لكي نستوعب المفهوم بشكل افضل

أنواع الـ Polymorphism

كما قلت ستجد أنك استعملت مفهوم الـ Polymorphism دون أن تدري

يمكننا تقسيم أنواع الـ Polymorphism إلى نوعين وهما

النوع الأول: Compiler-Time Polymorphism

هي قدرة اللغة او البرنامج على التعرف أو ادراك الـ Polymorphism أثناء كتابتك للكود وقبل تنفيذ البرنامج، لهذا سمى بـ Compiler-Time Polymorphism

بعض تطبيقات المفهوم:

سنشرح ما هو مفهوم الـ Compiler-Time عن طريق شرح الـ Function Overloading و الـ Operator Overloading فقط
أما الـ Template/Generic فلا داعي للاستفاضة فيها
فيكفي شرح الـ Function Overloading والـ Operator Overloading، وأرجوا بعد ذلك أن تفهم وتستوعب جيدًا أصل وفكرة الـ Compiler-Time Polymorphism
ويمكنك أن تبحث عن الـ Template/Generic إن اردت لزيادة فهمك ورؤية تطبيقات اخرى على المفهوم

Function Overloading

وهو أننا لدينا نفس الدالة لكن لها أشكال مختلفة وكل شكل يقوم بوظيفة معينة
أو ببساطة لدينا مجموعة دوال تشترك في إسم الدالة وتختلف في عدد الـ Parameters ونوعها

مثال على هذا

function add(x: number, y: number) {
  return x + y;
}

console.log(add(5, 10)); // OUTPUT: 15

هنا لدينا دالة add تستقبل رقمين وترجع لنا مجموعهما ببساطة

السؤال ماذا ان اردنا ان نقوم بجعل دالة اخرى تجمع لنا ثلاثة ارقام
لكن تلك الدالة نريد ان نسميها أيضًا بـ add أي بنفس اسم الدالة الأولى

function add(x: number, y: number) {
  return x + y;
}

function add(x: number, y: number, z: number) {
  return x + y + z;
}

function add(x: string, y: string) {
  return x + y;
}

console.log(add(5, 10)); // OUTPUT: 15
console.log(add(5, 10, 15)); // OUTPUT: 30
console.log(add('Ahmed', ' Mohamed')); // OUTPUT: Ahmed Mohamed

لاحظ أن لدينا دوال بنفس الإسم لكن يختلفوا في عدد الـ parameters
هنا اللغة أو الـ compiler يستطيع أن يفرق بينهم بعدد الـ parameters أو نوعها سواء number أو string وهكذا
بمعنى أننا لو ارسلنا رقمين فهو سيفهم اننا نريد الدالة الأولى
واذا ارسلنا ثلاث ارقام فسيفهم اننا نريد الدالة الثانية
وإذا ارسلنا كلمتين فسيفهم اننا نريد الثالثة

هذا ما يسمى بـ Polymorphism لان الدالة add أصبح لديها شكلين الآن
وهنا تستطيع دالة add تغير تصرفاتها بناءًا على الحالة
هنا بما أن الـ compiler استطاع ان يفهم ويفرق بين الدوال بسهولة ويعرف متى سيستخدم كل دالة
دون ان نشغل أو ننفذ الكود فهنا سميناها بـ Compiler-Time Polymorphism

كل شيء تراه ينطبق عليه هذا الوصف والمفهوم فهو هكذا Compiler-Time Polymorphism

ملحوظة: الـ Function Overloading نستطيع تطبيقه في جميع الدوال سواء خارج الكلاسات أو داخل الكلاسات
لغة Typescript لا تدعم الـ Function Overloading بشكل شامل
لكن يظل هذا المفهوم مدعوم في اغلب لغات البرمجة

حتى برغم بفقر بعض اللغات لدعم بعض المفاهيم إلا انها لديها حلول أخرى
ففي المثال السابق للـ function overloading الخاص بدالة الـ add
فيمكننا عمل نفس الشيء باسلوب مختلف قليلًا

كاستخدام مميزات أخرى تقدمها اللغة لإيجاد حلول أخرى

function add(...numbers: number[]) {
  return numbers.reduce((a, b) => a + b, 0); // add all numbers
}

console.log(add(1)); // OUTPUT: 1
console.log(add(1, 2)); // OUTPUT: 3
console.log(add(1, 2, 3)); // OUTPUT: 6
console.log(add(1, 2, 3, 4)); // OUTPUT: 10
// ... ect.

لاحظ لاحظت اننا هكذا حددناها لتكون خاصة بالارقام فقط
لكن هنا لا نستطيع ان نرسل string او اي نوع أخر

يوجد حل لهذا الأمر لكن باستخدام مفهوم الـ Generic
أريدك أن تبحث عنها وتحاول أن تطبقها وتفهمها بنفسك إن أردت
لاننا لن نتطرق لها هنا كما قلنا

Operator Overloading

يمكنك أن تتخيله كشكل آخر للـ Function Overloading والـ وهو يهتم بتنفيذ العمليات الحسابات كـ + - * / % ... إلخ على الـ object والكلاسات الجديدة التي تنشئها

على سبيل المثال لو لدينا كلاس يدعى Vector كلاس أنشأناه ونريده أن يقوم بتمثيل العمليات على المتجهات

class Vector {
  public x: number;
  public y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

فنريد أن نقوم بجمع متجهين على سبيل المثال
سننشيء دالة add تقوم بأخذ object متجه آخر وتجمعه مع الحالي
ودالة أخرى تدعى add أيضًا لكن تأخذ رقمًا وتقوم بجمع هذا الرقم بقيم المتجه

class Vector {
  public x: number;
  public y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  // Operator Overloading
  // add two vectors
  public add(vector: Vector): Vector {
    return new Vector(this.x + vector.x, this.y + vector.y);
  }

  // add number with vector
  public add(n: number): Vector {
    return new Vector(this.x + n, this.y + n);
  }
}

هنا ستلاحظ أنشأنا دالتين تحملان نفس الإسم add لاكن تأخذان قيم مختلفة
فالأولى تجمع متجه مع متجه والأخرى تجمع رقم مع متجه
وهذا يذكرك بالـ Function Overloading أليس كذلك
لكن في الـ Operator Overloading نطبق ونقوم بعمل نفس الأمر لكن مع الرموز الحسابية + - * وهكذا

ونستخدمهم بهذا الشكل

let vector1 = new Vector(1, 2);
let vector2 = new Vector(3, 4);

let vector3 = vector1.add(vector2); // Vector {x: 4, y: 6}
let vector4 = vector1.add(10); // Vector {x: 11, y: 12}

أظنك كنت تأمل وتتوقع شكلًا أفضل كـ vector1 + vector2
وأظنك أيضًا تقول الآن أن هذا حل تحايلي لاستخدام دالة لتمثل رمز +
لنخدع أنفسنا أننا أنشأنا جمع حقيقي بين الـ object

بصراحة كما قلت فالـ typescript لا تدعم الـ Function Overloading والـ Operator Overloading بشكل شامل

على عكس لغات أخرى كـ C++ فإنها تدعم الـ Operator Overloading بشكل كامل

فنفس المثال السابق يمكننا كتابته في الـ C++ بهذا الشكل
وسنستخدم رمز + في الجمع هكذا vector + vector2 بدلًا من استخدام الدوال

نفس المثال في الـ C++

class Vector {
  public:
    int x;
    int y;

    Vector(int x, int y) {
        this->x = x;
        this->y = y;
    }

    // Operator Overloading
    // Overload + operator to add two Vector objects
    Vector operator+(const Vector& vector) {
        return Vector(vector.x + x, vector.y + y);
    }

    // Overload + operator to add number with vector objects
    Vector operator+(int n) {
        return Vector(x + n, y + n);
    }
};

لاحظ استخدامنا لـ operator + هنا

استخدامنا للـ Operator Overloading في الـ C++ سيكون هكذا

Vector vector1(1, 2);
Vector vector2(3, 4);

Vector vector3 = vector1 + vector2; // Vector {x: 4, y: 6}
Vector vector4 = vector1 + 10; // Vector {x: 11, y: 12}

تذكر المفهوم يظل ثابت حتى وان تغير شكله من لغة للغة
قد تفقد لغة لبعض جوانب المفهوم أو قد تزيد لغة من مفهوم ما وتطوره أو تحسنه
مهما يكن فيظل فهمك لأساس المفهوم هو المهم، أظن أن رؤيتك للفروق في الـ C++ زاد وحسن نظرتك للأمر بشكل ما

النوع الثاني: Runtime Polymorphism

هي قدرة اللغة او البرنامج على التعرف أو ادراك الـ Polymorphism بعد تنفيذ كودك
أي بعد تنفيذ الكود وأثناء ما البرنامج قيد التشغيل، لهذا سمى بـ Runtime Polymorphism

بعض تطبيقات المفهوم:

سنتعمق في باقي المقالة عن الـ Function Overriding والـ Dynamic dispatch بالأخص
لان هذا هو الجزء الأكثر استخدامًا وأهمية في عالم الـ OOP على وجه الخصوص

أما الـ Virtual Function فلن أتطرق لها لانها ليست تطبيقًا شائعًا للمفهوم
بل هو خاص للغة الـ C++ لانها تفردت به
لذا لن يفيدك بشيء أن عرفته أو تعلمته إلا اذا اردت أن تتخصص في لغة الـ C++

تذكر أننا هنا سنستخدم الـ Inheritance فقط لنحقق بعض التطبيقات على مفهوم الـ Runtime Polymorphism
وتذكر أننا نستطيع تطبيق مفاهيم الـ Polymorphism بطرق اخرى غير الـ Inheritance
إن رأيت شيء ما يستطيع تغير تصرفاته بناءًا على موقف ما فهذا هو مفهوم الـ Polymorphism

Function Overriding

أظنك بالفعل تعرف ما هي الـ Function Overriding من خلال المقالات السابقة
لذا لا داعي لأن اعيد شرحها بالتفصيل

لكن أريدك فقط أن تفهم ما هي الـ Runtime Polymorphism
فأنا هنا سأقوم بشرح مفهوم الـ Runtime Polymorphism من خلال شرحي للـ Function Overriding

استخدامنا للـ Inheritance سيسهل ايصال وتطبيق مفهوم الـ Function Overriding و الـ Dynamic dispatch في الـ Runtime Polymorphism لا أكثر
ولو وجدت لغات اخرى تطبق نفس المفهوم بطرق اخرى، فلن يهمك الكيفية لانك ستكون تفهم المفهوم بالفعل

تتذكر كلاس الـ Employee من المقالة الماضية ؟

abstract class Employee {
  protected name: string;
  protected salary: number;

  constructor(name: string, salary: number) {
    this.name = name;
    this.salary = salary;
  }

  // abstract methods
  public abstract getSalary(): number;
}

لاحظ كيف ان دالة الـ getSalary هي abstract
بالتالي ليس لديها شكل معين أو implementation معين
سنطبق الآن الـ overriding عليها
أريدك هنا ان تحلل وتستنتج بنفسك مفهوم الـ Polymorphism هنا
ولماذا هو Runtime Polymorphism ؟

class CareemEmployee extends Employee {
  public getSalary() {
    return this.salary * 2 - 2000;
  }
}

class SutraEmployee extends Employee {
  public getSalary() {
    return this.salary * 3 - 4000;
  }
}

class SutraEmployee extends Employee {
  public getSalary() {
    return this.salary / 2 + 3000;
  }
}

هل شممت رائحة الـ Polymorphism ؟

سنعيد تعريف الـ Polymorphism مجددًا لتستنتجه بشكل أفضل

مفهوم الـ Polymorphism يمثل قدرة أي شيء مهما كدالة على أخذ عدة أشكال في آن واحد
أو تستطيع هذه الدالة تستطيع تغير تصرفاتها وافعالها بناءًا على موقف أو حالة معينة

أظن أن هذا الكلام ينطبق بشكل واضح على الـ Overriding
وبما أنك تعرف الـ Overriding بالفعل من المقالات السابقة فلا داعي لاعيد شرحها مجددًا هنا
يكفي فقط أن تستوعب أنه تطبيق عملي لمفهوم الـ Runtime Polymorphism

Dynamic dispatch

الآن سنناقش المفهوم الأساسي الأكثر أهمية هنا، وهو الـ Dynamic dispatch للـ object
وهو يعد من أهم الأجزاء في الـ Polymorphism لذا سنستفيض فيه لنهاية هذه المقالة

حسنًا، ما هو الـ Dynamic dispatch بالتحديد ؟

الـ Dynamic dispatch هو مفهوم يركز على إمكانية التغير بشكل سلس ومرن في أي وقت
فعلى سبيل المثال الـ object يمكنه أن يغير نوع الـ constructor أو الـ class الذي يتم إنشاؤه منه في أي وقت، كيف يمكن هذا ؟
لا تستعجلني سأوضح لك الأمر الآن بأمثلة عملية، لكن أريدك أن تركز جيدًا هنا

هذه الميزة موجودة في معظم لغات البرمجة ومرتبطة بمفهومين آخرين هما Inheritance والـ Abstraction

بالطبع قد تجد بعض اللغات تطبق المفهوم بشكل آخر وبشكل مختلف

حسنًا، في التطبيق العملي، إذا كان لدينا كلاس يسمى Employee، وكلاسات أخرى تسمى CareemEmployee و SutraEmployee يرثان من الـ Employee
يمكن للـ object من نوع الـ Employee أن يبنى أو يستدعي أي constructor من الـ CareemEmployee أو SutraEmployee، بشرط أن تكون الكلاسات ترث من الـ Employee

ركز في الجملة السابقة جيدًا وخصوصًا في الشرط

على سبيل المثال

class Employee {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }

  public getInfo() {
    console.log(`I am an employee named ${this.name}`);
  }
  public getName(): string {
    return this.name;
  }
}

class CareemEmployee extends Employee {
  public getInfo() {
    console.log(`I am a Careem employee named ${this.name}`);
  }
}

class SutraEmployee extends Employee {
  public getInfo() {
    console.log(`I am a Sutra employee named ${this.name}`);
  }
}

في هذا المثال، لدينا كلاس Employee الذي يحتوي على constructor ودالة getInfo
ولدينا أيضًا كلاسات CareemEmployeeو SutraEmployee التي ترث من Employee وقامت بعمل overriding لدالة getInfo

لغاية الآن الأمور عادية، لكن أنظر للكود التالي وركز جيدًا
سنقوم بعمل متغير من نوع Employee وهذا المتغير سيستطيع أن يكون object من نوع كلاس الـ CareemEmployee أو SutraEmployee

let employee: Employee;

employee = new CareemEmployee('Ahmed');
employee.getInfo(); // Output: I am a Careem employee named Ahmed

employee = new SutraEmployee('Mohamed');
employee.getInfo(); // Output: I am a Sutra employee named Mohamed

قمنا بإنشاء متغير يدعى employee من نوع Employee
وجعلناه يأخذ الـ constructor الخاص بكلاس الـ CareemEmployee ويستدعي دالة الـ getInfo الخاصة بالكلاس بداخله يتصرف كأنه object من CareemEmployee
ثم جعلناه يأخذ الـ constructor الخاص بكلاس الـ SutraEmployee ويستدعي دالة الـ getInfo الخاصة بالكلاس بداخله يتصرف كأنه object من SutraEmployee

ولاحظ أنه في الأصل كان نوعه Employee

ما السبب ؟

هذه ميزة مدعومة في أغلب لغات البرمجة لدعم مفهوم الـ polymorphism
وجعل إنشاء الـ object وتغيره أكثر سلاسة ومرونة
طبعًا مع وجود شرط معين يحكم هذه الميزة وهو وراثة الكلاسات لنفس الكلاس

ببساطة أنه كما قلنا أن المتغير من نوع كلاس الـ Employee يستطيع أن يأخذ ويستدعي constructor الـ CareemEmployee أو SutraEmployee طالما أنهما ينتميان لكلاس الـ Employee

استخدام abstract class و interface

ملحوظة: عليك أن تعرف أن let employee: Employee; هكذا نحن لا نقوم باستدعاء constructor الـ Employee، لاننا لم نقم بعمل new Employee() بعد

معنى هذا الكلام هو أنه عندما نقوم بعمل let employee: Employee; هكذا نحن نعرف متغير من نوع Employee وليس object
لاننا لم نقم بإنشاء أي constructor من الأساس
بل هو حاليًا يكون متغير عادي تمامًا من نوع Employee فقط لا غير لحين إنشاء constructor له

ولهذا فيمكننا استخدام الـ abstract class والـ interface لتطبيق الـ Polymorphism

ببساطة لاننا لا نحتاج لإنشاء constructor

abstract class Employee {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }

  public abstract getInfo(): void;
  public getName(): string {
    return this.name;
  }
}

class CareemEmployee extends Employee {
  public getInfo() {
    console.log(`I am a Careem employee named ${this.name}`);
  }
}

let employee: Employee = new CareemEmployee('Ahmed');
employee.getInfo(); // Output: I am a Careem employee named Ahmed

لاحظ أن المتغير employee من نوع كلاس Employee وهذا الكلاس كما تلاحظ هو abstract
لكن كما قلنا انه هنا يقوم باستدعاء الـ constructor الخاص بكلاس الـ CareemEmployee

بالطبع كلاس الـ CareemEmployee استخدم هنا الـ default constructor الخاص به

ويمكنك استبدال الـ abstract class وجعلها interface والأمور ستكون نفس الشيء مع مراعاة الاختلافات بين الـ abstract class والـ interface

التطبيق العملي واستخداماته

أظنك تحتاج أن ترى شيء مباشر يتطبق الفكرة بشكل واقعي وعملي
إليك هذا المثال البسيط

abstract class Bank {
  protected amount: number;

  constructor(amount: number) {
    this.amount = amount;
  }

  public abstract deposit(amount: number, employee: Employee): void;
  public getAmount() {
    return this.amount;
  }
}

لدينا هنا abstract class يدعى Bank بها دالة تدعى depositو getAmount
بالطبع يمكنك جعلها interface كما قلنا

لاحظ أن دالة deposit هي دالة تستقبل object من نوع Employee

ثم لدينا كلاس MisrBack و CairoBank سيقومان بوراثة Bank

class MisrBank extends Bank {
  public deposit(amount: number, employee: Employee) {
    this.amount += amount;
    console.log(
      `Deposited ${amount} in MisrBank from ${employee.getName()}. Current bank balance: ${
        this.amount
      }`
    );
  }
}

class CairoBank extends Bank {
  public deposit(amount: number, employee: Employee) {
    this.amount += amount;
    console.log(
      `Deposited ${amount} in CairoBank from ${employee.getName()}. Current bank balance: ${
        this.amount
      }`
    );
  }
}

حسنًا برأيك كيف سيتم استخدام دالة deposit الآن ؟
الدالة الآن كما تلاحظ تستقبل object من نوع Employee

هل هذه الدالة ستستقبل object من نوع CareemEmployee و SutraEmployee أيضًا ؟، بالطبع نعم !
طالما CareemEmployee و SutraEmployee من عائلة Employee فيمكن للدالة أن تستقبلهم دون مشاكل

let bank: Bank = new MisrBank(10000);
let employee: Employee;

employee = new CareemEmployee('Ahmed');
bank.deposit(5000, employee); // Deposited 5000 in MisrBank from Ahmed. Current bank balance: 15000

employee = new SutraEmployee('Mohamed');
bank.deposit(5000, employee); // Deposited 10000 in MisrBank from Mohamed. Current bank balance: 25000

هل تستطيع ان تنظر للكود وتحاول أن تفهم ماذا جرى ؟
لقد طبقنا الكثير من مفهوم الـ Polymorphism هنا

تعليق ChatGPT

عندما تعرف المتغير bank بأنه من نوع Bank ولكنه يتم تهيئته باستخدام MisrBank، فإنه يتم استخدام Polymorphism لأن MisrBank هو نوع مشتق من Bank وبالتالي يمكن استخدامه في متغير من نوع Bank. بالنسبة للمتغير employee، فإنه يعتبر متغيرًا متعدد الاستخدامات أيضًا. يتم تعيينه أولاً بنوع CareemEmployee ثم بنوع SutraEmployee.
ومن خلال الاستفادة من Polymorphism، يمكننا تعيين متغير من نوع Employee لكائنات CareemEmployee أو SutraEmployee، لأن كلاهما هو نوع فرعي من Employee. باستخدام هذا النوع من الهيكلة، يمكننا تحقيق العديد من الفوائد، بما في ذلك القدرة على استخدام واجهات مشتركة لمعاملة كائنات مختلفة بطريقة متجانسة. على سبيل المثال، يمكننا تصميم دوال تقوم بتنفيذ عمليات على كائنات Employee بشكل عام دون الحاجة إلى معرفة نوع الكائن الفعلي.
- ChatGPT

خاتمة

أريدك أن ترى الاستخدامات المحتملة لجعل الدالة تستقبل نوع Employee لكننا نرسل لها أي object ينتمي لعائلة الـ Employee كان نوع CareemEmployee او SutraEmployee

الأمر لا يتوقف هنا، أريدك تتخيل أمثلة أخرى كنظام Authentication دالة تقوم بالتحقق من صلاحيات الـ User
فتقوم باستقبال أي object ينتمي للـ User سواء كان من Admin أو Client أو غيره
وإن لم يكن تقوم الدالة بارسال رسالة بأنك ليست مرخص لك هنا على سبيل المثل

أظن أننا وصلنا لنهاية سلسلة مقالات الـ OOP الجميلة تلك كانت رحلة ممتعة
أردت من تلك المقالات الخمس أريد أرسم وعي وفهم للمفاهيم الأساسية والتقليل من التركيز على التطبيقات العملية لها

لأنه مهما اختلفت التطبيقات تظل المفاهيم ثابتة

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

ماذا بعد الـ OOP

عليك أن تعرف أولًا أن الـ OOP تستخدمها بشكل أساسي في اللغات أو المشاريع التي تتطلب OOP
لكن عليك أن تدرك أن الـ OOP هي مقترح أنت ممكن تستخدمها في مشروعك التي تتطلب وجودها بشكل أساسي
هناك مفاهيم وطرق اخرى تستطيع استخدامها في المشاريع بحسب اللغة أو المشروع
هناك شيء يدعى Functional Programming Paradigm والـ Scripting programming وهي تكون طرق مختلفة لكتابة الكود بشكل آخر وبها مفاهيمها الخاصة واساليبها الخاصة

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

حسنًا سأعطيك فقط بعض العناوين وأريدك أن تبحث عنهم بنفسك
مع العلم أن كل واحدة منهم سيكون عالم لوحده وستخوص فيه كثيرًا
وستكتشف أمور أخرى كثيرة ومختلفة لذا استمتع!