הבנת תכנות מונחה עצמים (OOP) ב-TypeScript

Logos of TypeScript and OOP side by side.

תכנות מונחה עצמים (OOP) הוא סגנון של תכנות שמתמקד ביצירת קוד מסודר בחלקים נפרדים (מודולריות), שימוש בקוד חוזר על מנת לחסוך בכתיבת קוד (code reuse) והגנה על הנתונים (אנקפסולציה). במאמר הזה, אנחנו נבדוק את הרעיונות המרכזיים של OOP ונראה איך הם עוזרים לבנות תוכנות טובות ויעילות. אבל לפני שנדבר על OOP, אני רוצה להסביר לכם מה זה סגנון תכנות, או בשמו המקצועי יותר פרדיגמת תכנות (programming paradigm).

פרדיגמת תכנות (programming paradigm)

פרדיגמת תכנות זו הדרך בא אנחנו ניגשים ובונים תוכנה. היא מגדירה כללים, דפוסים ושיטות שונות שמאפשרות למתכנתים לפתח את התוכנה שלהם בצורה הטובה ביותר. קיימות פרדיגמות תכנות שונות, וכל אחת מציעה גישות שונות לפתרון בעיות ולכל אחת יש יתרונות וחסרונות. היום נדבר על פרדיגמת תכנות חשובה במיוחד - תכנות מונחה עצמים (OOP).

תכנות מונחה עצמים Object-oriented programming (OOP)

תכנות מונחה עצמים (OOP) היא פרדיגמת תכנות המבוססת על שימוש באובייקטים (objects) וקלאסים (classes) לניהול נתונים בתוכנה. היא מעודדת את הרעיון של ארגון הקוד ליחידות מודולריות, כמו classes שנלמד עליהם בהמשך, וכן תומכת בשימוש חוזר בקוד (code reuse). היסודות המרכזיים של תכנות מונחה עצמים הם:

  • אנקפסולציה (Encapsulation)

    כמו בחיים האמיתיים, חלוקת תפקידים היא חייונית לניהול טוב יותר. לדוגמה, ארגון כמו צבא, בית חולים, חברות הייטק וכדומה, צריכות שתהיה להם חלוקת תפקידים ברורה כדי לנהל את עצמם טוב יותר. בדיוק אותו דבר גם בתכנות. האפליקציות שלנו יכולות להיות מאוד גדולות ולכן אנחנו רוצים לבנות אותם בצורה מסודרת עם חלוקת תפקידים ואחריות ברורים. הרעיון הזה נקרא אנקפסולציה בתכנות והיא אחת מהתכונות החשובות ב-OOP.

    כדי לממש אנקפסולציה בתוכנה שלנו, אנחנו משתמשים במושג הנקרא "קלאס" (class). קלאס הוא כמו יחידה בצה"ל, כלומר יחידה שיש לה תפקיד, מטרה, תכונות ופעולות משלה, וכמובן שיכול להיות לנו הרבה קלאסים בדיוק כמו בארגונים שונים שיש להם הרבה מחלקות (מחלקת כספים, משאבי אנוש, משווקים וכדומה). אז לדוגמה, ניתן ליצור קלאס שנקרא "אדם" שמגדיר תכונות כמו שם, גיל ומשקל, ופעולות כמו הליכה או ריצה.

    בעצם אנקפסולציה מדגישה את הרעיון שלכל קלאס צריך להיות אחריות ברורה, עם סט מאפיינים (properties) ושיטות (methods) משלו לניהול יעיל של הנתונים שהוא מכיל. גישה זו מאפשרת למפתחים לשלוט בצורה יעילה יותר בהתנהגות האפליקציה ולהקל על התקשורת בין רכיבים (יחידות) שונים באפליקציה.

    מימוש אנקפסולציה (Encapsulation)

    בתמונה למטה, אנחנו יוצרים קלאס בשם Person שמיועד לנהל את הפעולות והמידע שקשור לבני אדם באפליקציה שלנו. בתוך הקלאס יש כמה דברים חשובים:

    `constructor` - היא פונקציה מיוחדת שפועלת כמו הוראות ההרכבה כשאנו יוצרים אובייקט חדש מהקלאס. היא אומרת לתוכנה איך להכין אדם חדש מסוג Person, עם שם, שם משפחה, מספר כרטיס אשראי, ויתרה בבנק, ובסיום מחזירה אובייקט חדש מאותו קלאס, שבמקרה שלנו זה אובייקט מסוג Person.

    `public & private` - שימו לב שיש לנו שלושה properties שהם (firstName, lastName ו-bank) ו-2 methods שהן (introduceYourself ו-makePayment). מה שסומן עם public (ברירת מחדל גם אם לא מסמנים במפורש) מאפשר לנו לגשת לאותם properties & methods ולהשתמש בהם כשאנחנו יוצרים Blueprint חדש מסוג Person באמצעות המילה new. ומה שמסומן כאל private זה מידע שיהיה זמין רק ב-scope של אותו קלאס, ולכן שאנחנו מנסים לגשת ל-john.bank אנחנו לא מצליחים.

    Encapsulation concept in TypeScript code.
  • ירושה (Inheritance)

    אז חלוקת תפקידים זה חשוב, אבל גם לדעת איך לנצל קוד שכתבתם ולא לכתוב אותו פעמיים או אפילו יותר זה גם חשוב. הנושא הזה נקרא ירושה, ובדיוק כמו בחיים האמיתיים, גם בתכנות יש לנו דרך להשתמש או להעביר קוד שכתבנו למישהו אחר, במקרה שלנו זה קלאס. כלומר ירושה בתכנות מאפשרת לנו לקחת קלאס קיים שכבר מוגדר בתוכו properties ואו methods שהם נכונים גם לקלאס החדש שאנחנו רוצים לבנות, מבלי לכתוב את אותם properties ואו methods פעם שנייה, וזה כמובן מה שמאפשר לנו code reuse.

    אז גם ל-Person וגם ל-Employee יש שם פרטי ושם משפחה, ואני מכניס גם מספר כרטיס אשראי, ויתרה בבנק כאל משהו שמגדיר מבחינתי בן אדם בוגר, וגם יש לנו את ה-methods שיש לאדם בוגר כמו introduceYourself ו-makePayment, ולכן גם ל-Person וגם ל-Employee יש מכנה משותף שאותו אני יכול לשתף.

    במודל הירושה, הקלאס שמהווה את הבסיס לירושה נקרא Parent / Base / Super class. הקלאסים היורשים, אשר מקבלים ומרחיבים את התכונות והמתודות מהקלאס הבסיסי, נקראים Child / Derived / Sub class. לדוגמה, אם נתייחס לקלאס Person כקלאס הבסיס, אז הוא יחשב ל-Parent / Base / Super class. במקרה שלנו, קלאס כמו Employee, שיורש מPerson, יחשב ל-Child / Derived / Sub class.

    מימוש ירושה (Inheritance)

    בתמונה למטה, תוכלו לראות שאנחנו יוצרים קלאס חדש בשם Employee שיורש מ-Person באמצעות המילה extends. זה אומר של-Employee יש את כל מה של-Person יש, פלוס עוד דברים נוספים שהוספנו ל-Employee כמו תפקיד (position) ומשכורת (salary).

    אם תשימו לב, בתוך ה-constructor של Employee אנחנו משתמשים במילה super. באמצעות המילה הזו, אנחנו קוראים ל-constructor של Person שיאתחל את הנתונים ש-Person כבר יודע לעשות, ואז נותר לנו רק להוסיף את מה שאנחנו צריכים ב-Employee שזה position ו-salary. כך אנחנו חוסכים בקוד ושומרים על תכנות נקי ויעיל.

    כשאנחנו קוראים לפונקציה introduceYourself של Employee, היא קודם כל עושה את מה ש-Person עושה (בזכות `()super.introduceYourself`), ואז מוסיפה על זה מידע נוסף שקשור ל-Employee, מה שמוביל אותנו לפולימורפיזם (Polymorphism).

    Inheritance example in TypeScript code.
  • פולימורפיזם (Polymorphism)

    פולימורפיזם הוא היכולת של אובייקטים מקלאסים יורשים שונים כמו Employee להשתמש באותן methods ו-properties שהם ירשו מקלאס אב (Person), אך עם התנהגות שונה בכל אחד מהם.

    במילים פשוטות, אם Employee יורש מ-Person, זה אומר שכל ה-methods וה-properties של Person זמינות גם ל-Employee. אבל, כיוון ש-Employee הוא גם קלאס בפני עצמו, הוא יכול לבצע את אותן פעולות בצורה מעט או מאוד שונה – הוא מתאים את הפעולות לצרכים שלו.

    לשם כך, נעשה שימוש במילת המפתח override. כאשר קלאס יורש כמו Employee מעוניין לשנות את התנהגות של method שהוא ירש מהקלאס האב Person, הוא יכול להגדיר מחדש את הלוגיקה של ה-method על ידי שימוש במילה override. זה מאפשר ל-Employee להגדיר מחדש את התנהגות ה-method בהתאם ללוגיקה הספציפית לו, מבלי לשנות את התנהגות המקורית של ה-method בקלאס Person.

    מימוש פולימורפיזם (Polymorphism)

    בשורה מספר 19, אני משתמש במילת המפתח override על המתודה ()introduceYourself כדי להתאים את ההתנהגות שלה לצרכים הייחודיים של הקלאס Employee. במקרה זה, אני גם בוחר לקרוא למתודה `()super.introduceYourself`, שמבצעת את הפונקציונליות המקורית של הקלאס Person. הדבר אינו חובה, אך היא מאפשרת לנו לשלב את הלוגיקה מה-Super class עם ההתאמות שהוספנו. לאחר קריאת המתודה מה-Super class, הקוד ממשיך ומוסיף פלט המציג את התפקיד והשכר של העובד.

    בשורות 29 ו-30 אני יוצר אובייקט חדש מקלאס Person, ובשורה 31 אני יוצר אובייקט מקלאס Employee, וכמובן שבשניהם אני מעביר את המידע הדרוש ל-constructor. בשורות 42 ו-43 אני מראה מה התוצאה של person1 ושל person2 כשאני קורא ל-method של ()introduceYourself שנמצאת בקלאס Person. ובשורה 44 אני מראה מה התוצאה של employee1 כשאני קורא ל-method של ()introduceYourself שנמצאת בקלאס Employee.

    תוכלו לראות שהפעולה והתוצאה שאנחנו מקבלים הם שונים מקלאס Person לעומת קלאס Employee בגלל שאנחנו יישמנו פולימורפיזם (Polymorphism).

    Polymorphism implementation in TypeScript.
  • הפשטה (Abstraction)

    הפשטה (Abstraction) היא רעיון שמאפשר לנו להתמקד במה שחשוב ולהתעלם מהפרטים הטכניים. זה כמו להגיד שכל צורה גאומטרית יש שטח והיקף, אבל הדרך לחשב אותם שונה בין צורה לצורה. לדוגמה, הנוסחה לחישוב שטח מרובע שונה מהנוסחה לחישוב שטח מעגל.

    בתכנות, אנחנו משתמשים בהפשטה כדי להגדיר מה צריך לעשות אבל לא איך לעשות את זה. למשל, בקלאסים Person ו-Employee, שניהם יכולים לממש את הפונקציה introduceYourself, אבל כל אחד יעשה את זה בדרך שלו. Person יכול להציג את שמו ושם משפחתו, בעוד ש-Employee יכול להוסיף גם את תפקידו בחברה.

    מימוש הפשטה (Abstraction)

    קודם כל נייצר קלאס Shape שהוא אבסטרקטי שמכיל רק את הרעיון של שטח והיקף אבל אין לו בעצמו מימוש כי כל צורה יש לה את המימוש שלה.

    Abstract class in TypeScript.

    לאחר מכן, נייצר שני קלאסים שיורשים מקלאס Shape שהם (Circle & Rectangle). שימו לב ששני הקלאסים חייבים לממש את הפונקציות האבסטרקטיות של הקלאס Shape. עוד שימו לב ששני הקלאסים (Circle & Rectangle) ממשים באופן שונה את הפונקציות האבסטרקטיות של הקלאס Shape:

    Screenshot of JavaScript code.

    ניצור שני אובייקטים חדשים משני הקלאסים שלנו ונממש את הפונקציות האבסטרקטיות של הקלאס Shape:

    Screenshot of JavaScript code.

נפלא! הגענו לסיום! המושגים של תכנות מונחה עצמים (OOP) הם כלליים ומתקיימים ברוב שפות התכנות, כמו Python, C++ ועוד. למרות שה-syntax עשוי להשתנות משפה לשפה, העקרונות הבסיסיים, כפי שהדגמנו במאמר זה, נותרים זהים.