تطبيقات وأمثلة عملية للـ OOP

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

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

المقدمة

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

لذلك سنقوم بعمل بعض التطبيقات العملية للـ OOP عن طريق عرض بعض الأمثلة والمشاكل وكيفية حلها بالـ OOP
سأبدأ بمثال بسيط ثم نتدرج فيه بالأفكار لتغطية معظم الأفكار والمباديء التي تعلمناها


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

طبعًا عندما تفكر في مدونة فسيأتي في بالك مكونين أساسيين وهما المقالات و مألفوا المقالات
لذلك سنقوم بإنشاء كلاسين لكل منهما

إنشاء كلاس مؤلف المقالات

لنبدأ بالأبسط وهو الشخص الذي يكتب المقالات، سنقوم بإنشاء كلاس يمثل هذا المؤلف
عندما تفكر ما هي خصائص المؤلف ؟ ستجده مثله مثل أي شخص آخر، يحتوي على اسمه وبريده الإلكتروني ووصف قصير عنه

class Author {
    public id: number;
    public name: string;
    public email: string;
    public bio: string;

    constructor(id: number, name: string, email: string, bio: string) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.bio = bio;
    }
}

هذا الكلاس يحتوي على الـ id و name و email و bio تمثل الخصائص التي تكون أي مؤلف
ويحتوي على constructor لتمرير القيم عند إنشاء object جديد من هذا الكلاس

يمكننا الآن إنشاء مؤلف جديد بكل سهولة

const author_1 = new Author(1, 'Ahmed', '[email protected]', 'Backend Developer at Quotem');
const author_2 = new Author(2, 'Ali', '[email protected]', 'Frontend Developer at Example');

حسنًا، الآن لدينا مؤلفين لكن قبل أن نكمل دعونا نتكلم عن بعض النقاط وكيف يمكننا تحسين الكود
أولًا من غير المنطقي أن نجعل الـ id يتم تحديده من قبل المستخدم، علينا جعله يتم توليده تلقائيًا
لذا سنقوم بإضافة خاصية static للكلاس تحتوي على id يتم تحديده تلقائيًا

والـ static كما تعرف هو جعل المتغيرات أو الدوال مرئية على مستوى الكلاس وليس الـ object

class Author {
    private static lastId: number = 0;
    private id: number;

    public name: string;
    public email: string;
    public bio: string;

    constructor(name: string, email: string, bio: string) {
        this.name = name;
        this.email = email;
        this.bio = bio;

        this.setGeneratedId();
    }

    private setGeneratedId() {
        Author.lastId++;
        this.id = Author.lastId;
    }

    public getId(): number {
        return this.id;
    }
}

هنا قمنا بإضافة متغير lastId ولاحظ أنه private لأنه لا يجب أن يتم الوصول إليه من خارج الكلاس
وأيضًا جعلناه static لأنه يجب أن يكون مرئي على مستوى الكلاس وقمنا بإضافة دالة setGeneratedId تقوم بتوليد id جديد
الدالة هنا private لأنها شيء يتم حسابه داخل الكلاس ولا يجب أن يتم الوصول إليه من خارج الكلاس
والدالة تقوم بتزويد الـ lastId بـ 1 ثم تجعل id تساوي lastId الجديدة

ولاحظ أن الـ id هنا أصبح private لأنه لا يجب أن يتم تعديله من خارج الكلاس
ولكننا قمنا بإضافة دالة getId تقوم بإرجاع الـ id للمستخدم فقط
لذا قمنا بعمل getter للـ id ولم نقم بعمل setter له بسبب غرض معين
وهذا من فوائد الـ OOP ومبدأ الـ Encapsulation في جمع البيانات والدوال المتعلقة بها في كلاس واحد وإخفاء البيانات عن المستخدم
والسماح بالوصول لبعض البيانات من خلال دوال معينة مثل ما سمحنا لك بالوصول لمتغير معين مثل الـ id ولكن لا يسمح لك بتعديله

الآن يمكننا إنشاء مؤلف جديد بدون تحديد id

const author_1 = new Author('Ahmed', '[email protected]', 'Backend Developer at Quotem');
const author_2 = new Author('Ali', '[email protected]', 'Frontend Developer at Example');

console.log(author_1.getId()); // 1
console.log(author_2.getId()); // 2

لاحظ أن الـ id تم توليده تلقائيًا مع كل مؤلف جديد أو object جديد من الكلاس
وهذا أحد فوائد الـ static وأحد استخداماته البسيطة

تخزين المؤلفين في قاعدة البيانات

أولًا نحتاج لشيء يمثل قاعدة البيانات الخاصة بالمؤلفين، لذا سنقوم بإنشاء كلاس جديد يمثل قاعدة البيانات

class AuthorDatabase {
    private static authors: Author[] = [];

    private constructor() {}

    public static addAuthor(author: Author): void {
        this.authors.push(author);
    }
}

هنا قمنا بإنشاء كلاس AuthorDatabase يحتوي على أراي من الـ Author تمثل المؤلفين
وهناك دالة addAuthor تقوم بإضافة مؤلف جديد إلى القاعدة

ستلاحظ أننا قمنا بجعل كل شيء static سواء متغير الـ authors أو الدالة addAuthor
لأن الكلاس AuthorDatabase هو كما يوحي اسمه فهو قاعدة بيانات بالتالي لا داعي لإنشاء object منه
بل يمكننا أن نتعامل معه مباشرة من خلال الكلاس نفسه

لذا طالما أننا لا نريد إنشاء object من الكلاس بالتالي نجعل كل شيء static
وأيضًا نجعل الـ constructor يكون private لمنع إنشاء أي object من الكلاس

كلاس بسيط واستخدامه أبسط

const author_1 = new Author('Ahmed', '[email protected]', 'Backend Developer at Quotem');
const author_2 = new Author('Ali', '[email protected]', 'Frontend Developer at Example');

AuthorDatabase.addAuthor(author_1);
AuthorDatabase.addAuthor(author_2);

لاحظ أننا هنا أنشأنا author_1 و author_2 ثم قمنا بإضافتهم إلى قاعدة البيانات AuthorDatabase بشكل مباشر
لاحظ أننا لم نقم بإنشاء object من الكلاس AuthorDatabase بل قمنا بالوصول إلى الدالة addAuthor مباشرة لأنها static

لكن سؤال ... ما فائدة الـ AuthorDatabase ؟ ولما نحتاج لكلاس يمثل قاعدة البيانات ؟ ونخزن فيه المؤلفين ؟

الفائدة هنا شيئين:

  1. لديك مكان واحد به بيانات كل لمؤلفين الذي تضيفهم
  2. يمكنك إضافة دوال أخرى تقوم بالبحث عن مؤلف معين أو تعديل بياناته أو حذفه

فمثًلا يمكنك إضافة دالة داخل AuthorDatabase تقوم بإنشاء مؤلف جديد وإضافته إلى القاعدة

class AuthorDatabase {
    private static authors: Author[] = [];

    private constructor() {}

    public static addAuthor(author: Author): void {
        this.authors.push(author);
    }

    public static createAuthor(name: string, email: string, bio: string): Author {
        const author = new Author(name, email, bio);
        this.authors.push(author);

        return author;
    }
}

هنا قمنا بإضافة دالة createAuthor تقوم بإنشاء مؤلف جديد وإضافته إلى قاعدة البيانات وتقوم بإرجاع المؤلف الجديد بالتالي يمكنك الآن إنشاء مؤلف جديد وإضافته إلى قاعدة البيانات بشكل مباشر

AuthorDatabase.createAuthor('Ahmed', '[email protected]', 'Backend Developer at Quotem');
AuthorDatabase.createAuthor('Ali', '[email protected]', 'Frontend Developer at Example');

عمل بعض العمليات داخل الكلاس

حسنًا أظنك لم تقتنع بعد .. حسنًا فكر معي
الآن لدينا مكان يحتوي على المؤلفين وأشبه بـ Database وكل تعاملك مع المؤلفين تكون من خلال هذا الـ Database
سواء كان إضافة مؤلف جديد أو البحث عن مؤلف معين أو تعديل بياناته أو حذفه
ولكن كيف يمكننا القيام ببعض العمليات عليهم ؟
فمثلًا نريد أن نقوم بالبحث عن مؤلف بالـ id أو بالـ name أو بالـ email

هنا سأقول لك بكل بساطة أنك يمكنك إضافة دوال للكلاس تقوم بالبحث عن المؤلف بناءً على الـ id أو الـ name أو الـ email

class AuthorDatabase {
    // ...

    public static getAuthorById(id: number): Author | null {
        return this.authors.find(author => author.getId() === id);
    }

    public static getAuthorByName(name: string): Author | null {
        return this.authors.find(author => author.name === name);
    }

    public static getAuthorByEmail(email: string): Author | null {
        return this.authors.find(author => author.email === email);
    }
}

لاحظ أننا في كلاس الـ AuthorDatabase قمنا بإضافة دوال تقوم بالبحث عن المؤلف بناءً على الـ id أو الـ name أو الـ email
وهذه الدوال تقوم بإرجاع المؤلف إذا وجده أو null إذا لم يجده

وبهذا يمكنك الآن القيام بالبحث عن مؤلف معين بناءً على الـ id أو الـ name أو الـ email

const author = AuthorDatabase.getAuthorById(1);

if (author) {
    console.log(author.name);
} else {
    console.log('Author not found');
}

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

كلاس الـ AuthorDatabase يعد مثال جيد وبسيط لفكرة الـ Abstraction
وهو اخفاء تفاصيل الـ Implementation والعمليات التي تدور داخل الكلاس عن المستخدم الذي سيستخدم الكلاس

أظننا أنتهينا من الكلاس الأول الذي يمثل المؤلف، والآن سننتقل إلى الكلاس الثاني الذي يمثل المقالات

إنشاء كلاس المقالات

بناءً على ما قمنا به مع كلاس Author, دعنا نطبق نفس الشيءمع كلاس Article يمكنك أن تجلس وتفكر في الخصائص التي يجب أن يحتويها الكلاس
ستقول أنه يجب أن يحتوي على متغيرات مثل: العنوان، المحتوى، تاريخ النشر، ومؤلف المقال ... إلخ

لكن سؤال كيف يمكننا ربط المقال بالمؤلف ؟

هنا سنقوم بإضافة متغير يدعى authorId يحتوي على id للمؤلف المرتبط بالمقالة
وعند إنشاء المقالة الجديدة يجب عليك تمرير id للمؤلف المرتبط بها

class Article {
    private static lastId: number = 0;
    private id: number;

    public title: string;
    public content: string;

    private authorId: number;
    private publishedAt: Date | null;

    constructor(title: string, content: string, authorId: number) {
        this.title = title;
        this.content = content;
        this.authorId = authorId;
        this.publishedAt = null;
        this.setGeneratedId();
    }

    private setGeneratedId() {
        Article.lastId++;
        this.id = Article.lastId;
    }

    public getId(): number {
        return this.id;
    }

    public getAuthorId(): number {
        return this.authorId;
    }

    public publish(): void {
        this.publishedAt = new Date();

        // تخيل هنا كود جميل يقوم بنشر المقالة على موقع ما
    }
}

ستلاحظ عدة أمور منها:

الشيء المثير هو هو الـ authorId الذي يحتوي على id للمؤلف المرتبط بالمقالة
وهذا الـ authorId ستجده انه private لأنه لا يجب أن يتم تعديله من خارج الكلاس .. نفس الفكرة التي قمنا بها مع الـ id
لذا ستجد أننا قمنا بعمل getter للـ authorId وهي دالة الـ getAuthorId التي تقوم بإرجاع الـ authorId فقط

لكن سؤال ... كيف سيفيدنا هذا الـ authorId في ربط المقال بالمؤلف ؟

لنبدأ أولًا بإنشاء المؤلف وإضافته إلى قاعدة البيانات

const author = AuthorDatabase.createAuthor('Ahmed', '[email protected]', 'Backend Developer at Quotem');

الآن لنقم بإنشاء مقال جديد وربطه بالمؤلف

const article = new Article('Introduction to OOP', 'This is an introduction to OOP', author.getId());

هنا لدينا مقال جديد يحتوي على title و content و authorId وهذا الـ authorId يحتوي على id للمؤلف المرتبط به

الآن ستلاحظ شيئن مهمين وهما أننا يمكنك أن نرسل أي رقم للـ authorId حتى وإن كان لا يوجد مؤلف بهذا الـ id
والشيئ التاني كيف نقوم بعمل عمليات على المقالات مثل البحث عن مقال معين أو تعديل بياناته أو حذفه
أو حتى أحضار بيانات المؤلف المرتبط به لأننا ربطنا المقال بالمؤلف بالـ id فقط

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

تخزين المقالات في قاعدة البيانات

سنقوم بإنشاء كلاس ArticleDatabase مشابه لكلاس AuthorDatabase لتخزين المقالات

class ArticleDatabase {
    private static articles: Article[] = [];

    private constructor() {}

    public static addArticle(article: Article): void {
        this.articles.push(article);
    }

    public static createArticle(title: string, content: string, authorId: number): Article {

        // التحقق من وجود المؤلف
        const author = AuthorDatabase.getAuthorById(authorId);
        if (!author) {
            throw new Error('Author not found');
        }

        const article = new Article(title, content, authorId);
        this.articles.push(article);

        return article;
    }
}

هنا قمنا بإنشاء كلاس ArticleDatabase يحتوي على أراي من الـ Article تمثل المقالات
وقمنا بإضافة دالة addArticle تقوم بإضافة مقال جديد إلى قاعدة البيانات

ثم أهم شيء هي دالة createArticle التي تقوم بإنشاء مقال جديد وربطه بالمؤلف المرتبط به
لكن ستلاحظ أنها تقوم بشيء مهم جدًا وهو التحقق من وجود المؤلف
بمعنى هل الـ authorId المرسل موجود بالفعل في قاعدة بيانات المؤلفين ؟
استخدمنا دالة getAuthorById التي قمنا بإضافتها في كلاس ال AuthorDatabase التي تمثل قاعدة بيانات المؤلفين للتحقق من وجود المؤلف
وإذا لم يكن موجودًا فسيتم رمي Exception

الآن يمكنك إنشاء مقال جديد وربطه بالمؤلف بشكل مباشر

const author = AuthorDatabase.createAuthor('Ahmed', '[email protected]', 'Backend Developer at Quotem');
const article = ArticleDatabase.createArticle('Introduction to OOP', 'This is an introduction to OOP', author.getId());

وهكذا يمكنك الآن إنشاء مقال جديد وربطه بالمؤلف بشكل مباشر وبسهولة

عمل بعض العمليات داخل الكلاس

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

الأمر بسيط جدًا ومشابه لما قمنا به مع الـ AuthorDatabase

class ArticleDatabase {
    // ...

    public static getArticleById(id: number): Article | null {
        return this.articles.find(article => article.getId() === id);
    }

    public static getArticleByAuthorId(authorId: number): Article[] {
        return this.articles.filter(article => article.getAuthorId() === authorId);
    }

    public static getAuthorByArticleId(articleId: number): Author | null {
        const article = this.getArticleById(articleId);
        if (!article) {
            return null;
        }

        return AuthorDatabase.getAuthorById(article.authorId);
    }
}

هنا قمنا بإضافة دوال تقوم بالبحث عن المقال بناءً على الـ id أو الـ title أو الـ authorId
وتلك الدوال البسيطة التي قمنا بعمل ما يشبهها في كلاس الـ AuthorDatabase

لكن الجديد هنا هي دالة getAuthorByArticleId التي تقوم بإرجاع المؤلف المرتبط بالمقالة المعينة
ستلاحظ أنها تستقبل id الخاص بالمقالة ثم تقوم بالتأكد من وجود المقالة أولًا عن طريق دالة getArticleById
ثم في النهاية عندما نحصل على object المقالة يمكننا ارسال المتغير authorId الخاص بها إلى دالة getAuthorById للحصول على المؤلف

وبهذا يمكنك الآن القيام بالبحث عن مقال معين أو الحصول على بيانات المؤلف المرتبط به بشكل سهل

const article = ArticleDatabase.getArticleById(1);
const author = ArticleDatabase.getAuthorByArticleId(article.getId());

وهكذا يمكنك الآن القيام بالبحث عن مقال معين أو الحصول على بيانات المؤلف المرتبط به بشكل سهل

يمكننا عمل فكرة أخرى وهي بدلاً من استخدام دالة الـ getAuthorByArticleId المتواجدة في كلاس الـ ArticleDatabase
لماذا لا نقوم بعمل دالة جديدة في كلاس الـ Article تقوم بإرجاع المؤلف المرتبط بها ؟

class Article {
    // ...

    public getAuthor(): Author | null {
        return AuthorDatabase.getAuthorById(this.authorId);
    }
}

هكذا بكل بساطة يمكننا احضار بيانات المؤلف المرتبط بالمقالة بشكل مباشر من الـ object نفسه

const article = ArticleDatabase.getArticleById(1);
const author = article.getAuthor();

هكذا يمكنك الآن القيام بالبحث عن مقال معين من خلال الـ ArticleDatabase
ويمكننا الآن الحصول على بيانات المؤلف المرتبط به من خلال الـ object نفسه عن طريق دالة getAuthor


حسنًا ... أظن أن هذا كل شيء أردت أن أشرحه لك في مثال تطبيق مدونة بسيطة
المثال غرضه الشرح لا أكثر ولا أقل لذا لا أريد أن أسمعك تقول لي ... نحن لم نقم بإنشاء نظام مدونة بشكل متكامل
أنا فقط أردت أن أفكر معك ونحلل ونشرح بعض الأفكار والمفاهيم الأساسية للـ OOP بشكل عملي أكثر

تقليل التكرار في الكود

أولًا ستلاحظ أن هناك تكرار لبعض الأمور في كلاس الـ Author والـ Article
ومن ضمنها توليد id ودالة setGeneratedId لذا يمكننا أن نقوم بعمل كلاس يحتوي على هذه الأمور ونرث منه في الكلاسات الأخرى

abstract class IdGenerator {
    private static lastId: number = 0;
    private id: number;

    constructor() {
        this.setGeneratedId();
    }

    private setGeneratedId() {
        IdGenerator.lastId++;
        this.id = IdGenerator.lastId;
    }

    public getId(): number {
        return this.id;
    }
}

هنا قمنا بعمل كلاس IdGenerator يحتوي على توليد id ودالة setGeneratedId ودالة getId
الأمور الأساسية الخاصة بالـ id والتي يمكننا أن نرث منها في الكلاسات الأخرى

ولاحظ أننا جعلنا الكلاس IdGenerator يكون abstract لأنه لا يجب أن يتم إنشاء object منه، فقط يمكنك أن ترثه لتحصل على المميزات التي يوفرها
ولاحظ أننا قمنا بعمل constructor له لكي يتم توليد id تلقائيًا عند إنشاء object من الكلاس الذي سيرث منه

ثم يمكننا أن نرث منه في الكلاسات الأخرى

class Author extends IdGenerator {
    public name: string;
    public email: string;
    public bio: string;

    constructor(name: string, email: string, bio: string) {
        super();
        this.name = name;
        this.email = email;
        this.bio = bio;
    }
}

class Article extends IdGenerator {
    public title: string;
    public content: string;
    private authorId: number;
    private publishedAt: Date | null;

    constructor(title: string, content: string, authorId: number) {
        super();
        this.title = title;
        this.content = content;
        this.authorId = authorId;
        this.publishedAt = null;
    }
}

وكما ترى الآن يمكننا أن نرث من الكلاس IdGenerator في الكلاسات الأخرى ونحصل على توليد id تلقائيًا
دون الحاجة لتكرار الكود في كل كلاس أو الاهتمام بهذه التفاصيل كل مرة
وهذا يعد مثال جيد على فكرة الـ Abstraction والتي تعني إخفاء التفاصيل الداخلية للـ Implementation
وفكرة الـ Inheritance والتي تعني إعادة استخدام الكود وتقليل التكرار

أهم جزء هنا هو جزء الـ super() الذي نستدعيه في constructor كل كلاس يرث من الـ IdGenerator
لكي ينادي ويستدعي الـ constructor الخاص بالكلاس الأب ويتم توليد id تلقائيًا
لأن الـ super() يمثل الـ constructor الخاص كلاس IdGenerator
وبالطبع طريقة الاستدعاء تختلف من لغة للأخرى ولكن الفكرة هي نفسها فمثلًا في لغة PHP يمكنك استدعاء parent::__construct()


يوجد شيء آخر يمكننا أن نفعله وهو عمل interface للـ Author والـ Article
لماذا ؟ لكي نميز بين الكلاسات وما بعضها فمثلًا الـ Author والـ Article هما كيانات لا يربطها شيء سوى أنهما كيانات يمكننا إنشاء منهما object
ويحتويان على دوال متنوعة مثل getId
لذا نحتاج لشيء لربط هذا النوع من الكلاسات لذا سنستفيد من الـ interface وننشيء واحد يسمى Model على سبيل المثال أو Entity

interface Model {
    getId(): number;
    printInfo(): void;

}

class Author extends IdGenerator implements Model {
    // ...

    public printInfo(): void {
        console.log(`Author: ${this.name} - ${this.email}`);
    }
}

class Article extends IdGenerator implements Model {
    // ...

    public printInfo(): void {
        console.log(`Article: ${this.title} by ${this.getAuthor().name}`);
    }
}

الآن الفائدة هنا أننا نعرف أن كلاس Author و Article ينتميان لنفس العائلة وهى عائلة الـ Model
وأنهما يحتويان على دوال مشتركة مثل getId و printInfo أو أي دالة أخرى مشتركة على مستوى كل Model

وهنا يمكننا تطبيق فكرة الـ Polymorphism

const model_1 : Model = new author('Ahmed', '[email protected]', 'Backend Developer at Quotem');
const model_2 : Model = new article('Introduction to OOP', 'This is an introduction to OOP', model_1.getId());

لاحظ أننا هنا قمنا بإنشاء object من الـ Author والـ Article ولكن قمنا بتخزينهما في متغير من نوع Model
لأن كلاس Author و Article ينتميان لنفس العائلة وهي عائلة الـ Model
بالتالي يمكننا عمل متغير من نوع Model وتخزين فيه أي object من الـ Author أو الـ Article
أو يمكننا عمل array من الـ Model وتخزين فيه الـ Author والـ Article

const models: Model[] = [];

models.push(new author('Ahmed', '[email protected]', 'Backend Developer at Quotem'));
models.push(new article('Introduction to OOP', 'This is an introduction to OOP', 1));

for (const model of models) {
    model.printInfo();
}

هناك أفكار وفوائد كثيرة جدًا للـ interface والـ Polymorphism سنتطرق لها في الأمثلة القادمة


ماذا عن الـ ArticleDatabase و AuthorDatabase ؟

هل يمكننا عمل abstract class ورث منها الـ ArticleDatabase و AuthorDatabase ؟ أو حتى Interface مشترك يكون لهما ؟ مثل ما قمنا به مع الـ Author والـ Article ؟
بصراحة أجل ... هناك Design Pattern يسمى Repository Pattern يمكنك أن تقوم بتطبيقه هنا
وهو يعني أنك تقوم بعمل interface أو abstract class تحتوي على الدوال الأساسية التي يجب أن تحتوي عليها أي قاعدة بيانات

interface Repository {
    add(data: Model): void;
    getAll(): Model[];
    getById(id: number): Model | null;
}

abstract class Database implements Repository {
    protected static data: Model[] = [];

    public static add(data: Model): void {
        this.data.push(data);
    }

    public static getAll(): Model[] {
        return this.data;
    }

    public static getById(id: number): Model | null {
        return this.data.find(item => item.getId() === id);
    }
}

هنا قمنا بعمل interface يدعى Repository يحتوي على الدوال الأساسية التي يجب أن تحتوي عليها أي قاعدة بيانات
ثم قمنا بعمل abstract class يدعى Database يقوم بتنفيذ الـ Repository ويحتوي على الدوال الأساسية التي يجب أن تحتوي عليها أي قاعدة بيانات

ستلاحظ هنا أمور جميلة ومنها أننا استخدمنا Model بدلاً من Author و Article
هكذا نحن عممنا الأمور وجعلنا الـ Database يعمل مع أي كلاس يرث من الـ Model
وأيضًا خصصناه ليعمل مع أي كلاس من نوع Model فقط
وهذه من فوائد أننا استخدمنا الـ interface يدعى Model لربط مجموعة من الكلاسات معًا مثل الـ Author والـ Article
لكي نستطيع الاستفادة منها كما رأينا سابقًا وكما رأينا هنا في كلاس الـ Database

هكذا يمكنك الآن أن تقوم بعمل AuthorDatabase و ArticleDatabase وتجعلهما يرثان من الـ Database والتﻻ تحتوى على الأمور والدوال المشتركة بينهما

class AuthorDatabase extends Database {
    // ...

    public static getByEmail(email: string): Author | null {
        return this.data.find(author => author.email === email);
    }
}

class ArticleDatabase extends Database {
    // ...

    public static getByAuthorId(authorId: number): Article[] {
        return this.data.filter(article => article.getAuthorId() === authorId);
    }
}

هنا كما ترى فقط قمنا بعمل AuthorDatabase و ArticleDatabase ورثنا من الـ Database دوال الـ Repository الأساسية مثل add و getAll و getById
وأيضًا هذا لا يمنع من إضافة دوال جديدة خاصة بكل كلاس مثل getByEmail في AuthorDatabase و getByAuthorId في ArticleDatabase

الختام

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

كنت أود أن أشرح لك المزيد ولكن أظنني أنني قد قمت بشرح الأمور الأساسية والمهمة
يمكنك تاليًا قراءة المقالة المتعلقة بمباديء الـ SOLID من هنا مبادئ الـ SOLID لجعل كودك صلب كالحديد