July 13, 2024

טעויות שאתם עושים - עם תבניות עיצוב

הקדמה

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

אז לפני שנתחיל עם הבעיות - מה הן תבניות עיצוב?

תבנית עיצוב או באנגלית - Design Pattern היא פתרון לבעיה מוכרת המאפיין איך בדיוק הפתרון הזה ממוממש.

תבנית עיצוב מאוד פופלרית היא התבנית Strategy.

בהינתן אלגוריתם כלשהו, אנחנו יכולים להזריק אלגוריתם אחר כדי לשנות את ההתנהגות.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Action:
def __init__(self, operator):
self.Operator = operator

def Calculate(self, a, b):
return self.Operator(a,b)


class Calculator:
def __init__(self):
self.FancyCalculate = Action(lambda a,b: a + b + 5)

c = Calculator()
c.FancyCalculate(1,2)
c.FancyCalculate = Action(lambda a,b: a + b + 10)
c.FancyCalculate(1,2)

תבניות עיצוב הן חלק אינטגרלי מהעבודה שלנו כי אנו לומדים אותן בשלבים הראשונים ונוטים לראות אותם כמעט בכל מקום:
Factory, Visitor, StateMachine, Builder וכדו’…

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

איזה בעיות מורכבות יכולות להיווצר ואילו טעויות קריטיות אסור לנו לעשות - בואו נצלול.

טעות 1 - להשתמש בתבנית בשביל להשתמש בתבנית

הטעות

להשתמש בתבנית רק בשביל להשתמש בתבנית כלשהי.

דוגמא

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

1
2
3
4
5
6
7
8
9
10
class Singleton:
_instance = None

def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance

def __init__(self):
self.value = None

הפתרון

העקרון התומך שעוזר לא להפר את הכלל הוא - KISS - Keep it simple stupid.
קודם השתמשו בגרסה הפשוטה של הפתרון ורק אחר כך תחשבו אם אתם רוצים ליצור מורכבות.

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

1
2
3
4
5
class MyClass:
def __init(self):
self.value = None

myInstance = MyClass()

טעות 2 - לממש תבניות בצורה הכי גנרית\כללית

הטעות

לעיתים נתקלתי בקוד שממש תבנית עיצוב בלי התנהגות דומיינית\עסקית בכלל!

דוגמא

מימוש State Machine כפתרון גנרי על מנת להוסיף ניהול מצבים.
בדרך כלל נראה פתרון שדומה לזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class State:
def __init__(self,name, nextStateName, operator):
self.name = name
self.nextStateName = nextStateName
self.operator = operator

class StateMachine:
def __init__(self, states):
self.states = {}

def AddState(self, name, operator, nextState):
self.states[name] = State(name, nextState, operator)

def RemoveState(self,name):
del self.states[name]

פתרון

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from abc import ABC, abstractmethod

class LightState(ABC):
@abstractmethod
def switch_on(self, light_switch):
pass

@abstractmethod
def switch_off(self, light_switch):
pass

class OnState(LightState):
def switch_on(self, light_switch):
print("The light is already ON.")

def switch_off(self, light_switch):
print("Turning the light OFF.")
light_switch.set_state(OffState())

class OffState(LightState):
def switch_on(self, light_switch):
print("Turning the light ON.")
light_switch.set_state(OnState())

def switch_off(self, light_switch):
print("The light is already OFF.")

class LightSwitch:
def __init__(self):
self.state = OffState() # Initial state is "Off"

def set_state(self, new_state):
self.state = new_state

def switch_on(self):
self.state.switch_on(self)

def switch_off(self):
self.state.switch_off(self)

def get_state(self):
return self.state.__class__.__name__

light_switch = LightSwitch()

print(f"Initial State: {light_switch.get_state()}")
light_switch.switch_on()
print(f"Current State: {light_switch.get_state()}")
light_switch.switch_off()
print(f"Current State: {light_switch.get_state()}")


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

טעות 3 - חוסר הבנה של הבעיה

הטעות

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

דוגמא

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Observer:
def update(self, temperature):
pass


class WeatherStation:
def __init__(self):
self.observers = []
self.temperature = None

def add_observer(self, observer):
self.observers.append(observer)

def remove_observer(self, observer):
self.observers.remove(observer)

def set_temperature(self, temperature):
self.temperature = temperature
self.notify_observers()

def notify_observers(self):
for observer in self.observers:
observer.update(self.temperature)


class Display(Observer):
def update(self, temperature):
print(f"Current temperature: {temperature}°C")

weather_station = WeatherStation()
display = Display()

weather_station.add_observer(display)

weather_station.set_temperature(25)

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

הפתרון

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class WeatherStation:
def GetTemperature(self):
self.get_temp_from_service()

def get_temp_from_service(self):
# Some http request
return 27


class Display(Observer):
def __init__(self, weatherStation):
self.weatherStation = weatherStation

def Display(self):
print(f"Current temperature: {self.weatherStation.GetTemperature()}°C")

weather_station = WeatherStation()
display = Display(weather_station)

# Some other location of code
display.Display()

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

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

הטעות

נפוץ בעיקר אצל שפות עם אקו-סיסטם עשיר כמו C#.
כאשר אנו רוצים ליצור תת-מערכת כלשהי אך במקום לחשוב אם זה כבר קיים אנו חושבים על איך לממש משהו משלנו בעזרת תבנית.
חשוב להבין - תבנית עיצוב זה צריך להיות מקום 5 או 8 בקו המחשבה שלנו.

דוגמא

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface IMachine
{
void OnUpdate();
}

public class Sensor
{
private IList<IMachine> mMachines = new List<IMachine>();

public void AddMachine(IMachine machine)
{
mMachines.Add(machine);
}

private void OnDetected()
{
foreach(var machine in mMachines)
{
machine.OnUpdate();
}
}
}

פתרון

לא למהר לפתור בעיות פתורות!
במקום זה ניתן להשתמש במנגנון האירועים שקיים כבר ב-C#:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

public class MyMachine
{
public void DoOnActivate()
{
Console.WriteLine("Machine is working");
}
}

public class Sensor
{
public event Action OnActivation;

public void TriggerActivation()
{
OnActivation?.Invoke();
}
}

public class Program
{
public static void Main(string[] args)
{
var sensor = new Sensor();
var myMachine = new MyMachine();

sensor.OnActivation += myMachine.DoOnActivate;

// Trigger the event
sensor.TriggerActivation();
}
}

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

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

תודה על הקריאה!

על הפוסט

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

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#DesignPatterns#Design