התכנות הפרוצדורלי – מה הוא נתן לנו?
אם התכנות המובנה היה קפיצת מדרגה, שקשה מאוד לדמיין את עולם התוכנה בלעדיה, אזי גם התכנות הפרוצדורלי הציג וקידם עוד כמה רעיונות שכיום הם ״מובנים מאליהם״.
אין מה הרבה מה לספר על תהליך התגבשותה של הפרדיגמה הזו, בגדול זה היה עוד צעד בשכלול שפות התכנות – בכדי לאפשר לנהל בסיסי קוד גדולים ומורכבים יותר. השלב הטבעי הבא.
Functional Decomposition
הרעיון החזק ביותר שהציג התכנות הפרוצדורלי [א] נקרא Functional Decomposition. הוא הציל את עולם התוכנה מכבלי הקדמוניות – אבל מאז הפך גם ל Anti-Pattern מסוכן ונפוץ.
זוהי טכניקה / תהליך / Pattern שמשפר מערכות בגודל בינוני, אך מסוכן עד מזיק – כאשר מנסים להתאים אותו למערכות גדולות ומורכבות.
הרעיון, הולך בערך כך:
איך מתמודדים עם בעיה גדולה ומורכבת? – נכון: Divide & Conquer. מחלקים לבעיות קטנות יותר, שקל יותר לפתור אותן אחת אחת.
אם אצליח למסגר (to frame) בעיה / פיצ׳ר נדרש במערכת כסט בן שלושה חלקים (שאתאר מיד) – אז יש לנו מסגרת לפתרון – כיצד לממש אותו.
החלקים הם:
- תיאור state ראשוני – נתון.
- תיאור state סופי – רצוי.
- סדרה של טרנספורמציות שיהפכו את ה state הראשוני ל state הסופי.
זה רעיון כ״כ בסיסי והגיוני, שגם מי שלא למד את ״התאוריה של תכנות פרוצדורלי״ – מגיע אליו שוב ושוב.
כנושא במשרת ״ארכיטקט״ כבר שנים רבות, מעניין לי לציין שזה היה שלב מוקדם הוא השתלב תיאור תפקיד של ארכיטקט: ״הארכיטקט מחלק את התהליך לפרוצדורות, והמתכנתים ממשים את הפרוצדורות השונות״. גם זו, גישה שכנראה תרמה / הצליחה עד שלב מסוים, אבל מעבר לו – היא כבר נחשבת בעיקר כבעייתית.
מה הבעיות של Functional Decomposition שהפכו אותו ל Anti-Pattern בעולם ה OO?
- אי התייחסות / מחסור ב Information Hiding ו Encapsulation:
- האם ה State הראשוני חשוף לכלל המערכת? האם הוא גלובאלי, למשל? – כך היה בהתחלה.
- האם כל פונקציה שאפעיל רשאית לעשות כל שינוי ב state?
אם ה state הוא רשימה של מחרוזות – אז אין בעיה. ככל שה state גדול ומורכב יותר (קיצונות אחרת – לב הנתונים של המערכת) – קל יהיה יותר לאבד שליטה ובקרה: איזה פונקציה עושה מה – על הנתונים הללו.
- סיכויים גוברים לארגון נחות של המערכת:
- כאשר הפונקציות מפוזרות במערכת ללא סדר וללא היגיון – קל להגיע לבלאגן.
- כאשר אין סדר במערכת, סביר יותר שלא אמצא פונקציה קיימת ומספקת – ואכתוב אותה מחדש = קוד כפול.
- סיכויים גדולים למידול נחות של המערכת:
- חשיבה על המערכת בצורה של ״נתונים״ ו״טרנספורמציות״ אינו טובות למערכת גדולה ומורכבת. במערכת מורכבת, חשוב מאוד למצוא הפשטות (abstractions) טובות לפרטי המערכת, כך שיתאפשר לנו לחשוב ברמה גבוהה יותר של הפשטה. ה Functional Decomposition לא מוביל אותנו לשם, ואולי אפילו מפריע.
- ביטוי של הסיכון הזה עשוי להגיע בדמות מודל אנמי – דפוס עיצוב שלילי שכתבתי עליו בהרחבה.
עוד מסר חשוב: Functional Decomposition עשוי להישמע כמו Functional Programming – אבל הוא לא. בתכנות פונקציונלי, למשל, פונקציות לא משנות state, בטח לא state חיצוני. ניתן, כמובן, ליישם Functional Decomposition (גם) בשפות של תכנות פונקציונלי – אבל שפות של תכונת פונקציונלי מספקות כלים נוספים, בכדי לא להגיע לשם.
Functional Decomposition היא יוריסטיקה טובה לחיפוש פתרון Design לבעיה במערכת: לפרק בעיה גדולה לגורמים ולהבין מהיכן לתקוף אותה. חשוב מאוד לא לסיים שם, ולהפוך את הפתרון הנאיבי הנדסית, שאליו הגענו בעזרת Functional Composition – לפתרון בר-קיימא.
איזה סדר בדיוק? – אין לכך הנחיה ברורה אחת. חשוב שהסדר במערכת יהיה ברור לכולם, ועקבי – אחרת הוא חסר משמעות.
- כל פיסת קוד שנכתבת תהיה ״מקוטלגת״ או ״מיוחסת״ לאזור מסוים בקוד – להלן ״מודול״.
- הקרבה הזו היא קרבה רעיונית, ולא בהכרח פיסות קוד שקוראות אחת לשנייה.
- אין ברירה, קוד ממודול אחד – יקראו לקוד במודול שני. אבל, אנחנו רוצים לחלק את הקוד למודולים כך, שנצמצם את הקריאות בין המודולים, ונמקסם את הקריאות / ההיכרות בתוך אותו המודול.
- הרעיון הזה נקרא גם: ״High Cohesion / Low Coupling״. בעברית: אחידות גבוה (בתוך המודל) / תלות נמוכה (בין המודולים).
כל אלו הם רק שיפורים – על הרעיון הבסיסי.
הפרדיגמה מונחית-העצמים (OO)
מי שחי בתקופה בה צמחה הפרדיגמה מונחית העצמים, בעיקר בשנות ה-80 וה-90 – חווה דיון נוקב בהבדלים בין תכנות מונחה-עצמים (OOP), ותכנון מונחה-עצמים, Object-Oriented Design, או בקיצור: OOD.
אפילו לא פעם נשמעה הטענה ש”OOD הצליח – אך OOP נכשל”. טענה שנויה במחלוקת, הייתי מוסיף.
אני לא רוצה להשקיע זמן על שחזור הוויכוח, ועל הדקויות הנלוות – ולכן אתייחס בעיקר ל OO כ OOD+OOP.
ב OOP השתרשה ההתייחסות ל-3 עקרונות יסוד מקובלים: הכמסה, ריבוי צורות, והורשה.
הדיון לאורך גיבוש הפרדיגמה, שארך כמה עשורים – היה מורכב בהרבה.
![]() |
על פייטון ניתן לומר שהיא יותר שפת OOD מאשר שפת OOP… |
בחרתי כמה עקרונות של OO שהייתי רוצה לשים עליהם דגש, אלו שאני מאמין שיש עוד מה לחדש לגביהם:
Everything is an Object
על העיקרון הזה לא מדברים היום הרבה, אבל הוא עיקרון מרכזי – וזה שנתן לפרדיגמה את שמה. הרעיון הוא בעצם פיתוח ושיפור של רעיון ה Modules.
לרעיון יש שני פנים:
- לארוז קוד ביחידות קטנות (יותר) של מודולריזציה – להלן מחלקות.
- למדל את המערכת ליחידות המתארות את העולם האמיתי. זה עיקרון ליבה של ה OOD, שלעתים לא מודגש מספיק: זה לא מספיק לחלק את הקוד ל״מחלקות״. חשוב מאוד שהחלוקה הזו תתאר את עולם הבעיה (העסקית) – בצורה נאמנה והגיונית. המיפוי / מידול הזה – מקל על הבנת המערכת, ועל התקשורת לגביה – מה שמקל על לקבוצה גדולה של אנשים להיות שותפים אליה ולעבוד בצורה קוהרנטית.
- אם בעולם שאותו המערכת משרתת יש מונחים שלא מתבטאים במערכת כאובייקטים – אז כנראה עשינו מידול לא מוצלח.
- אם הקשר בין האובייקטים לא תואם להיגיון מקובל – אזי כנראה שהמידול שלנו לא מוצלח.
- אחד מכלי המחשבה למידול לאובייקטים הוא המחשבה על ״תחום אחריות״ (להלן: SRP). אם האובייקט היה בן-אדם, מה הוא היה דורש שרק הוא יעשה? שיהיה רק באחריותו?
- טכניקה מעניינת של מידול שצמחה, למשל, היא CRC cards.
Information Hiding
הרעיון הזה אינו חדש, והתגלגל גם בפרדיגמות מוקדמות יותר.
OO חידשה בכך ששמה את ה Information Hiding כעקרון ליבה מחייב – ולא עוד כהמלצה ״לפעמים כדאי לצמצם את הידיעה של חלק אחד בקוד על חלקים אחרים״. ב OO מדגישים: ״בכל מקום במערכת אנחנו נסתיר כל מה שלא נחוץ – לחלקים האחרים״.
- Encapsulation הוא השילוב של הרעיון הזה, בשילוב הרעיון של Everything in an Object. ההכמסה היא ״קרום התא הביולוגי״ המגן כל פנים התא הרגיש (ה state הפנימי) – מהשפעות חיצוניות.
בשפות OO לרוב יש לנו directives הנותנים לנו לשלוט על הנראות: private, package, protected – וכו’.- אובייקטים הרוצה משהו מאובייקט אחר, שולח לו ״הודעות״. לא עוד אלך ואפשפש לי (ואולי גם אשנה) State של חלק אחר במערכת. יש מעתה Gate Keeper, שלו אני שולח רק את המידע הנדרש (“ההודעה”) – ואקבל בחזרה – תשובה.
- אם אתם יוצרים הודעות בין אובייקטים ושולחים את כל ה State של האובייקט, אז אתם מממשים משהו שקרוב יותר לתכנות פרוצדורלי – מאשר ל OO.
- אובייקטים הרוצה משהו מאובייקט אחר, שולח לו ״הודעות״. לא עוד אלך ואפשפש לי (ואולי גם אשנה) State של חלק אחר במערכת. יש מעתה Gate Keeper, שלו אני שולח רק את המידע הנדרש (“ההודעה”) – ואקבל בחזרה – תשובה.
- יש צורות נוספות של Information Hiding מלבד הכמסה של אובייקטים:
- הפרדה של בסיס הנתונים לסכמות, תוך הגבלת הגישה לסכמות השונות – הוא information hiding
- תכנון המערכת כך שיהיה צורך של פחות אובייקטים להכיר אחד את השני – הוא information hiding מעבר להכמסה של האובייקט הבודד.
- העברה ל Front-End רק את פיסות המידע הנדרשות (לפעמים נדרש עיבוד נוסף לצורך כך) – הוא information hiding.
- המוטיבציה העיקרית ל Information Hiding היא היכולת לבצע שינויים בחלקים מסוימים במערכת, בלי שהדבר יגרור צורך לשינויים באזורים נוספים. להגביל את גודל השינוי.
- אם אני לא יודע על חלקים אחרים – שינוי בהם לא אמור לדרוש ממני להתעדכן.
- זה לא תמיד נכון. לפעמים יש השפעות עקיפות – שכן ידרשו ממני להתעדכן. למשל: סדר שונה של פעולות שמרחש במערכת. לא נחשף לי מידע חדש / שונה, אבל עדיין אני צריך להתאים את עצמי.
הורשה
דיברתי כבר המון על הורשה: הנה ב 2012, הנה הרצאה מוקלטת מכנס רברסים 2016, הנה הרצאה מוקלטת אחרונה בנושא כמפגש של JavaIL.
הורשה היא כלי נחמד, ולעתים מזיק: מזיק להכמסה, ומזיק למידול טוב של המערכת לעולם האמיתי. כלי שנוטה להזיק – לפעמים עדיף להיפטר ממנו, ואכן הרעיון להיפטר מהכלי שנקרא הורשה – עולה מדי פעם.
לפחות הייתי ממליץ להגביל את השימוש בהורשה לאובייקטים קטנים ופשוטים. כן יש משהו קל להבנה בהורשה רדודה ולא-מתוחכמת.
שלא תגידו שלא ידעתם.
non-Objects
מהה?? אם “Everything is an Object” – אז איך יש דברים שאינם אובייקטים?
את הסתירה הזו ניתן ליישב רק בהסתכלות לאורך זמן: בהתחלה ניסו באמת להגדיר שכל דבר יהיה אובייקט – אבל עם הזמן הבינו שיש חריגים. שמדי פעם עדיף שיוגדרו במערכת גם ישויות – שלא יצייתו לחוקי “האובייקטים”.
כש OO אומץ בצורה יותר ויותר רחבה, עלו עוד ועוד קולות שרצו להבחין בין מחלקות (classes) לבין מבני-נתונים (structs). בניגוד למחלקה, מבני-נתונים רק “מחזיקים” כמה נתונים ביחד – ואין עליהן פעולות (כך טענו בתחילה). יותר מאוחר הסכימו שיכולות להיות גם על מבני-נתונים פעולות (פונקציות) – אבל רק כאלו שעוזרות לגשת לנתונים במבנה, ובוודאי לא Business Logic. ההבחנה הזו לא השתנה – ולא נראה שיש קולות לשנות אותה.
ג׳אווה, שפה מאוד פופולארית ומשפיעה – דילגה על ההבחנה בין מחלקות למבני-נתונים. אם אנחנו רוצים להגדיר בה מבנה-נתונים – אנחנו משתמשים ב classes. בעצם כך, שפת ג’אווה “מנעה” במידת מה, מההבחנה בין מחלקות למבני-נתונים לחלחל בתעשייה.
- האם Map או List הם מחלקות? לא – אלו הם מבני-נתונים. כל מטרתם היא להחזיק ולחשוף נתונים. הכמסה? זה לא הקטע שלהן [ב].
- האם DTOs או Enum הם מחלקות? לא – אלו מבני-נתונים. בשפת ג׳אווה נממש DTOs כ Class, אך מבחינה רעיונית הן לא מחלקות: אין להן הכמסה [ב], ואין להם אחריות לתאר פעולות בעולם.
- בג’אווה הרבה פעמים כאשר ממשמשים DTO (לרוב מצוין ע”פ Suffix בשם) – מאפשרים לכל השדות של ה DTO להיות public – ומדלגים על השימוש ב getter/setters. הגיוני.
חוסר ההבחנה בין מחלקות ומבני-נתונים לא מאפיין את כל השפות: ב #C קיימים structs, בקוטלין יש Data Classes, ובפייטון היו named tuples כסוג של ייצוג, ולאחרונה הוסיפו גם Data classes.
אפשר להסתכל על המבנים הללו רק ככלי לאופטימיזציה של קוד/ביצועים בשפה – אבל שימושי הרבה יותר להשתמש בהם ככלי להבחנה סמנטית בין אלמנטים שונים בתוכנה: מחלקות, ומבני-נתונים.
יש כמה סימנים שיעזרו לנו לזהות מבני-נתונים:
- על מבני-נתונים אין פעולות, לפחות לא כאלו שמבצעות Business Logic.
- מחלקות יכולות להכיר מבני-נתונים, אך מבני נתונים לעולם לא יכירו מחלקות.
- אם שדות של מחלקה השתנו, זה יכול להיגמר בלי השפעה סביבתית. אם שדות של מבנה-נתונים משתנים – הם כמעט תמיד יגררו שינויים במחלקות אחרות. למה? כי זו מטרתם בחיים: להעביר נתונים בין מחלקות, וגם מ/אל מערכות חיצוניות (קריאה ל API, שליפה מבסיס נתונים, וכו׳).
בהחלט קיימת נטייה לבלבל ולערבב בין מחלקות ומבני-נתונים. הבלבול הזה לוקח אותנו למימוש פחות נכון של פרדיגמת ה OO – לפחות בצורה העדכנית שלה. אם אנחנו כבר משקיעים את כל העבודה בניהול מחלקות והכמסה – חבל לפספס את ההבחנה החשובה הזו!
ההבחנה בין מחלקות למבני-נתונים תורמת למידול המערכת, ותורמת להכמסה (כי אנחנו לא מבזבזים כוחות היכן שהיא לא נדרשת – וכך מצמצמים את הקולות לוותר עליה).
סיכום
המשכנו לאורך המסלול ההיסטורי, מתכנות פרוצדורלי עד לתכנות מונחה-עצמים.
לאורך התקופה הזו, הייתה והשפיעה פרדיגמה משמעותית נוספת: פרדיגמת התכנות הפונקציונלי – שלה אני אקדיש פוסט נוסף.
שיהיה בהצלחה!
[א] יש כאלו שמסווגים את הרעיון הזה כחלק מהתכנות המובנה. אין פה אמת אחת, ויש חפיפה רעיונית ותקופתית בין שתי הפרדיגמות.
באופן דומה, יש גם לא מעט חפיפה בין תכנות פרוצדורלי ל OO. לצורך הפוסט – אני מפשט את הדברים ופשוט מייחס כל רעיון לפרדיגמה אחת ויחידה.
[ב] זה כמובן, חצי מדויק: לפעמים מבנה הנתונים הוא מורכב (למשל HashMap) ואנו מכמיסים את מורכבות המבנה הפנימי בכדי לחשוף למשתמש מבנה-הנתונים הפשטה פשוטה יותר.
יש מאמר לגבי ענין המבני נתונים מול מחלקות שבוב מרטין פרסם ממש לאחרונה, לא יודע אם כבר ראית, אבל שווה לשתף בכל מקרהhttps://blog.cleancoder.com/uncle-bob/2019/06/16/ObjectsAndDataStructures.html
תודה!
התגובה הוסרה על ידי המשתמש שכתב אותה.
תודה רבה רבה. החכמתי מהמאמר כתמיד. מצפה למאמר על תכנות פונקציונלי…
לא יודע אם פספסתי, הייתי מחדד:עוד תכונות בסיסיות של OOP הם: dynamic dispatch / binding, מנגנון לתיאור סמנטיקה של עצמים ותמיכה בפרסיסטנטיות.קיימים סוגים שונים של פולימורפיזם.מה עם פרדיגמה נוספת של תכנות לוגי (אולי קצת משעממת).—-2 חלקים שכתובים טוב מאוד על פרדיגמות תכנות.
היי ליאור, תודה רבה על התוספת!אני חושב שיש עוד כמה פרדיגמות מעניינות שאפשר להזכיר: תכנות לוגי הוא אחד, אבל יש Reactive Programming ו Event-driven programming, יש TDD שלא מוצג כפרדיגמה – אבל אני חושב שהוא כמעט שם, יש Aspect Oriented Programming ו Model Driven Architecture/Development – שלא ממש צלחו (לפחות לרוב התעשיה). אולי פספסתי עוד כמה…
תודה רבה!
מעניין! תודה!
The most advanced Game Servers Hosting Control Panel in the world for free, Includes: Billing API, One-click Setup, Multiple Remote Servers and more. http://gspanel.net