הרכבה לעומת הורשה
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 76 מתוך 104.
כשמתכננים קשרים בין מחלקות, יש שתי אפשרויות עיקריות: ירושה ("הוא-") והרכבה ("יש לו"). הבחירה בגישה הנכונה משפיעה במידה רבה על הגמישות ועל יכולת התחזוקה של הקוד שלך.
ירושה יוצרת צימוד הדוק בין מחלקות. Car שיורשת מ־Vehicle קשורה לצמיתות לקשר הזה:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};הרכבה משלבת אובייקטים כחברים, וכך יוצרת קשר גמיש יותר. ל-Car יש Engine במקום להיות מנוע:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // ל-Car יש Engine
public:
void start() { engine.start(); }
};היתרון המרכזי של הרכבה הוא הגמישות. אפשר להחליף רכיבים בקלות, לשנות התנהגות בזמן ריצה או לשלב כמה יכולות, בלי המגבלות של היררכיית מחלקות קשיחה. ברוב המקרים, עיצוב מודרני ב-C++ מעדיף הרכבה על פני ירושה.
השתמשו בירושה כשיש קשר אמיתי מסוג "הוא-סוג-של" ונחוצה לכם פולימורפיות. השתמשו בהרכבה כשרוצים לעשות שימוש חוזר בפונקציונליות, או כשהקשר מתואר טוב יותר כ"יש-לו" או "משתמש-ב".
אתגר
קלבואו נבנה סימולטור של מערכת מחשב שמדגים מתי להשתמש בהרכבה לעומת בירושה. תדגמנו מחשב שיש לו רכיבים (הרכבה), במקום להיות רכיב, וכך נראה כיצד הרכבה מאפשרת להחליף חלקים בזמן ריצה.
תארגנו את הקוד בשלושה קבצים:
Components.h: הגדירו את הרכיבים הנפרדים שמחשב יכול להכיל.צרו מחלקת
CPUעםstd::string modelו-int coresפרטיים. ספקו בנאי שמקבל את שני הערכים, מתודתprocess()שמדפיסהCPU [model] processing with [cores] cores, ומתודתgetModel().צרו מחלקת
Memoryעםint sizeGBפרטי. ספקו בנאי, מתודתload()שמדפיסהLoading data into [sizeGB]GB RAM, ומתודתgetSize().צרו מחלקת
Storageעםstd::string typeפרטי (כמו "SSD" או "HDD") ו-int capacityGB. ספקו בנאי, מתודתread()שמדפיסהReading from [capacityGB]GB [type], ומתודות לקבלת הערכים של שני השדות.Computer.h: הגדירו מחלקתComputerשמשתמשת בהרכבה כדי לשלב רכיבים.למחלקת
Computerצריכים להיות אובייקטים פרטיים של משתני חבר:CPU,Memoryו-Storage. השתמשו ברשימת האתחול של הבנאי כדי לאתחל את הרכיבים האלה מתוך הפרמטרים.ממשו את המתודות הבאות:
boot(): מדפיסהBooting computer..., ואז קוראת ל-load()על הזיכרון, ל-read()על האחסון ול-process()על המעבד, כל אחת בשורה נפרדתspecs(): מדפיסה את מפרט המחשב בפורמט הבא:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: קראו שש קלטים (כל אחד בשורה נפרדת):- שם דגם המעבד (מחרוזת)
- מספר ליבות המעבד (מספר שלם)
- גודל הזיכרון ב-GB (מספר שלם)
- סוג האחסון (מחרוזת, למשל "SSD")
- קיבולת האחסון ב-GB (מספר שלם)
- פקודה: "boot" או "specs"
צרו את אובייקטי הרכיבים, ואז צרו אובייקט
Computerבאמצעות הרכבה. בהתאם לפקודה, קראו ל-boot()או ל-specs().
לדוגמה, עבור הקלטים Intel i7, 8, 16, SSD, 512 ו-boot:
Booting computer...
Loading data into 16GB RAM
Reading from 512GB SSD
CPU Intel i7 processing with 8 coresעבור הקלטים AMD Ryzen, 6, 32, HDD, 1000 ו-specs:
Computer Specs:
- CPU: AMD Ryzen (6 cores)
- RAM: 32GB
- Storage: 1000GB HDDשימו לב שמחלקת Computer אינה יורשת מאף רכיב. היא מכילה אותם. קשר מסוג "יש-לו" פירושו שתוכלו ליצור בקלות מחשבים עם שילובים שונים של רכיבים, או אפילו להחליף רכיבים בהמשך אם תוסיפו מתודות setter. הגמישות הזאת היא היתרון המרכזי של הרכבה על פני ירושה.
נסו בעצמכם
#include <iostream>
#include <string>
#include "Computer.h"
using namespace std;
int main() {
// קריאת קלטים
string cpuModel;
getline(cin, cpuModel); // שם דגם ה-CPU (עשוי להכיל רווחים)
int cpuCores;
cin >> cpuCores; // מספר ליבות ה-CPU
int memorySize;
cin >> memorySize; // גודל הזיכרון ב-GB
string storageType;
cin >> storageType; // סוג האחסון (למשל, "SSD")
int storageCapacity;
cin >> storageCapacity; // קיבולת האחסון ב-GB
string command;
cin >> command; // פקודה: "boot" או "specs"
// TODO: ליצור אובייקטים של רכיבים (CPU, Memory, Storage)
// TODO: ליצור אובייקט Computer באמצעות קומפוזיציה
// TODO: בהתאם לפקודה, לקרוא ל-boot() או ל-specs()
return 0;
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP
קבצים חיצונייםבנייה וקומפילציה של C++קובצי Header וקובצי מקורמרחבי שמות והיקףמבוא ל-OOP ב-C++מחלקות לעומת אובייקטיםמצביע thisמתודות (פונקציות איבר)תכונות (חברי נתונים)יסודות הבנאים וההורסיםסיכום – מחשבון פשוט4מאפייני מחלקה
חברי מופע לעומת חברים סטטייםגטרים וסטריםפונקציות איבר קבועותמילת המפתח mutableמתודות ומשתנים סטטייםפונקציות ומחלקות ידידותסיכום – מנהל חשבונות בנק7ירושה
ירושה בסיסיתרמות גישה בירושהסדר הקריאות לבנאי ולמפרקדריסת מתודותפונקציות וירטואליות ו־VTableירושה מרובהירושה וירטואליתסיכום – היררכיית עובדים2ניהול זיכרון
זיכרון Stack לעומת Heapמצביעים והפניותזיכרון דינמי (new/delete)מצביעים חכמים ב-C++RAII ב-C++סיכום – מנהל מערכים דינמיים5כימוס
מצייני גישה ב-C++מצייני גישה לעומקהסתרת מידעמבנה לעומת מחלקהמחלקות מקוננות ופנימיותסיכום – מערכת רשומות תלמידים8רב־צורתיות
רב־צורתיות בזמן קומפילציה לעומת בזמן ריצההעמסת פונקציותפונקציות וירטואליות — מבט נוסףפונקציות וירטואליות טהורותמחלקות מופשטותתכנון ממשקים ב־C++המרה דינמית ו־RTTIסיכום — מחשבון צורות11מושגי OOP מתקדמים
הרכבה לעומת הורשהMixins באמצעות CRTPאידיום Pimplמחיקת טיפוסיםמחלקות Enum וטיפוסיות חזקהטיפול בחריגות ב-OOPהיררכיות חריגות מותאמות אישית3בנאים והורסים
בנאי ברירת מחדלבנאי עם פרמטריםבנאי העתקהבנאי העברהרשימות אתחול בבנאיםבנאים מאציליםמעמיקים בנושא הורסיםכלל השלושה / החמישה / האפססיכום – מחלקת String6העמסת אופרטורים
מבוא להעמסת אופרטוריםהעמסת אופרטורים אריתמטייםהעמסת אופרטורי השוואהאופרטורי זרםהעמסת אופרטור השמההעמסת האופרטורים [] ו־()אופרטורים להמרת טיפוסיםסיכום – מחלקת Matrix9תבניות
תבניות פונקציהתבניות מחלקההתמחות תבניותתבניות ארגומנטים משתנותיסודות SFINAE ותכונות טיפוסיםסיכום – מכל גנריתרגלו בעצמכם: קומפיילר C++ אונליין