July 22, 2021

פייתון 19 - מחלקות חלק ג


פרק קודם:

פייתון 18 - מחלקות חלק ב

מה נלמד

  • עיצוב מונחה עצמים
  • עקרונות עיצוב בסיסיים
    • אבסטרקציה
    • הכלה מול ירושה
    • KISS
    • לזהות מה משתנה ומה לא
    • עקרון הוליווד

עיצוב תכנות מונחה עצמים

כמו בציור שהאלמנטים הבסיסיים שלנו הם צבעים, בתכנות מונחה עצמים האלמנט הבסיסי שלנו הוא המחלקה!
מחלקות מורכבות ממידע והתנהגות.

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

עקרון 1 - לחשוב באבסטרקציה

יש שני דרכי חשיבה כאשר מסתכלים על מערכת - הרכיבים הספציפיים או הרעיונות מאחורי הרכיבים.
מתכנת צעיר בדרך כלל יחשוב בדרכים ספציפיים - למערכת יש חיישני חום, חיישני תנועה, בקר לכל חדר וכדו’…
מתכנת יותר בוגר ייחשוב על הקונספטים מאחורי המערכת - בתים מסודרים בחדרים, לכל חדר יש “רמת אבטחה” מסוימת שכוללת מכשור שונה.
בכללי רמת האבסטרקציה והמורכבות עולים - ומתכנת מנוסה ידע איך לנהל את המורכבות כך שתהיה פשוטה יותר.

למשל, ניקח רעיון כללי - חיישנים
ואז נחשוב על חיישנים ספציפיים:

  • חום
  • תנועה
  • לחצנים
  • מזלגות בתוך פחית מתכת

תעלו אסוציאציות נוספות באותו סגנון “רעיון” ו”דברים ספציפיים”.

עקרון 2 - הכלה מול ירושה

מחלקות יכולות להיות בסדר שונה ומשונה - מחלקה א’ יכולה לרשת מחלקה ב’,
כמו כן מחלקה א’ יכולה להכיל את מחלקה ב’.
מה ההבדל?

  • מחלקה ב’ היא סוג של מחלקה א’.
  • למחלקה א’ יש מחלקה ב’.

למשל:

  • יונדאי הוא רכב
  • לרכב יונדאי יש גלגלים.

אזי:

  • יודנאי יורש מ”רכב”.
  • רכב יונדאי מכיל מחלקת גלגלים.

הכלה:

1
2
3
4
5
6
7
8
9
10
11
12
class Engine:
pass

class Wheel:
pass

class Car:
def __init__(self):
self.Engine = Engine()
self.Wheels = [Wheel(),Wheel(),Wheel(),Wheel()] # 4 גלגלים

car = Car()

ירושה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Engine:
pass

class InlineEngine(Engine):
pass

class CylinderEngine(Engine):
pass

class Car:
def __init__(self, engine):
self.Engine = engine
self.Wheels = [Wheel(),Wheel(),Wheel(),Wheel()] # 4 גלגלים

engine = CylinderEngine()
car = Car(engine)

יש שני עקרונות כאן:

  1. יש לבחור בין הכלה לירושה - כאשר המחלקה היא מממשת התנהגות העיצוב הנכון הוא הכלה.
    ניתן לבדוק זאת בעזרת המשפט:
    “האם X הוא Y”
    למשל - האם תפוח הוא פרי אזי זו ירושה.
    לתפוח יש גרעינים - אזי זו הכלה.
  2. פולימורפיזם עובד פה שעות נוספות - הכלה וירושה עובדים יד ביד.
    בדוגמת המנוע ניתן לראות שהשתמשנו בהכלה של מנוע - והחלפת המנוע בסוג אחר של מנוע.

עקרון 3 - KISS - Keep it simple stupid

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

בתור מתכנתים עלינו לנהל מורכבויות ולהפוך אותן לפשוטות יותר.
המטרה היא לכתוב קוד עובד ומובן - לא מסובך.


עוד מטרה של העקרון הזה הוא בעצם לצמצם את השימוש בעקרונות האחרים ולא להגזים איתם.

עקרון 4 - להפריד בין מה שמשתנה למה שלא משתנה

ההפרדה בין שינויי קוד הוא מה שהופך קוד פשוט לקוד מורכב.
העקרון הזה הוא אחד הכלים שלנו להוריד את המורכבות בקוד.

למשל אם יש לנו 3 סוגי מחלקות שמממשות את אותה הלוגיקה אך משהו מסוים משתנה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Enemy:
def Execute(self, player):
GoTo(player)
Attack(player)

class DefendingEnemy(Enemy):
def Execute(self, player):
nearLocation = CalculateNearLocation(player)
GoTo(nearLocation)
DefendStance()

Attack(player)

class AttackingEnemy(Enemy):
def Execute(self, player):
SwitchToStrongerWeapon()
GoTo(player)
AttackStance()

Attack(player)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Enemy:
def Stategize(self):
GoTo(palyer)

def Execute(self, player):
Stategize()
Attack(player)

class DefendingEnemy(Enemy):
def Stategize(self):
nearLocation = CalculateNearLocation(player)
GoTo(nearLocation)
DefendStance()

class AttackingEnemy(Enemy):
def Stategize(self):
SwitchToStrongerWeapon()
GoTo(player)
AttackStance()

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

עקרון 5 - להוריד Coupling לעלות Cohesion

  • Coupling - מדד לכמה שני יישויות תלויות אחת בשנייה.
  • Cohesion - מדד לכמה שני יישויות קשורות אחת לשנייה.

Coupling

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
def Print(self):
print("A")

class B:
def __init__(self):
self.a = A()

def Do(self):
self.a.Print()

a = A()
b = B(a)

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

1
2
3
class A:
def ToStr(self) -> str:
return "A"

לכן העקרון בא ומגדיר - יש להוריד את התלות בין שני ישויות, למשל:

1
2
3
4
5
6
7
8
9
10
11
12
class A:
def ToStr(self) -> str:
return "A"

class B:
def __init__(self, str):
self.str = str
def Do(self):
print(self.str)

a = A()
b = B(a.ToStr())

מחלקת B כבר לא שומרת את A בתוכה אלה מקבלת מחרוזת במקום זה.

Cohesion

1
2
3
4
5
6
7
8
9
10
11
12
class Printer:
def PrintDocument(self, document):
pass

def SaveFile(self, document):
pass

def ChangeTitle(self, document, newTitle):
pass

def CancelCurrentPrint(self):
pass

מתוך כל המתודות של מחלקת Printer, מהן הפונקציות שקשורות ומה לא?

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

עקרון 5 - הוליווד - אל תקרא לי, אני אקרא לך

העקרון עוזר להפריד ישויות ולהוריד את ה-Coupling ביניהם.

למשל בקטע הקוד הבא:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person:
def __init__(self, newsPaperProvider):
self.newsPaperProvider = newsPaperProvider

def Read(self):
while not self.newsPaperProvider.HasNews():
Wait()
news = self.newsPaperProvider.GetNews()
Read(news)

class NewsPaperProvider:
def HasNews(self):
pass

def GetNews(self):
pass

provider = NewsPaperProvider()
person = Person(provider)

מה לא בסדר בו?
כדי לקרוא את החדשות האחרונות אנחנו מחכים ל-provider שיהיה לו חדשות.
במקום זה - אנחנו צריכים ליצור תבנית שונה שמתארת “רושם-נרשם-ספק”.

ספק עיתונים שנותן לכל הנרשמים שלו את החדשות האחרונות.
לתבנית הזו יש גם שם - The Observable Pattern.

ככה זה נראה בקוד:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person:
def RecieveNews(self, news):
Read(news)

class NewsPaperProvider:
def __init__(self):
self.subscribers = []

def Subscribe(self, person):
self.subscribers.append(person)

def SpreadNews(self):
news = self.GetNews()
for subscriber in self.subscribers:
subscriber.RecieveNews(news)

provider = NewsPaperProvider()
p = Person()
provider.Subscribe(p)
provider.SpreadNews()

מה היתרונות בתבנית הזו?

  • מוריד Coupling בין מחלקות, ניתן לממש מחלקה שלישית כדי לנהל את התלות בין שני המחלקות.
  • ניתן לשנות בקלות יותר את ה-NewsPaperProvider מבלי לחשוף יותר מדי מידע החוצה.
  • ניתן לממש צורות שונות של הרשמה כך לתת את המידע הרלוונטי רק לישויות שרוצות אותן.
  • התבנית הזו תומכת בריבוי ישויות
  • התבנית תומכת בהלכה במקום ירושה

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

יש באתר מאמרים נוספים שמדברים על עיצוב נכון ותכנון נכון אז מומלץ למי שמתעניין ללכת ולקרוא אותם :)

הפרק הבא חוזר לקוד פייתוני ולפונקציות:

פייתון 20 - למבדות

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#Python