March 17, 2022

5 כללי אצבע לעיצוב פשוט

הקדמה

אין עיצוב ארכיטקטורי או עיצוב קוד הפותר כל בעיה.
כל פתרון תקף לבעיה ספציפית.

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

איך לעצב קוד כללי - קוד גנרי

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

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


הסעיף הראשון הוא פשוט:

1
2
3
4
int Sum(int a, int b)
{
return a + b;
}

בסעיף השני עלינו “להרחיב” את האופקים של הקוד ולתת מענה גם לפעולות אחרות.
אבל…
כעת נחשוב “רגע, אם אנחנו צריכים פעולת חיסור מה הלאה? כפל? חילוק?
בשלב הזה נתחיל לחשוב במושגים “כלליים” ולהתאים פתרון שיכול לעזור לנו גם בהמשך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

template<class Operation, class NumType>
int Calculate(Operation op ,NumType a, NumType b)
{
return op(a,b);
}

template<class NumType>
int Sum(NumType a, NumType b)
{
return a + b;
}

template<class NumType>
int Substract(NumType a, NumType b)
{
return a - b;
}

int main()
{
std::cout << Calculate(Sum<int>, 1 , 3);
}

ואם נצטרך פעולה נוספת בקלי קלות נוסיף:

1
2
3
4
5
6
7
8
9
10
template<class NumType>
int Multiply(NumType a, NumType b)
{
return a * b;
}

int main()
{
std::cout << Calculate(Multiply<int>, 1 , 3);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

template<class Operation, class ...NumType>
int Calculate(Operation op ,NumType&&... a)
{
return op(a...);
}

template<typename ...Args>
int Sum(Args... args)
{
return (args + ...);
}

int main()
{
std::cout << Calculate(Sum<int,int,int>, 1 , 3, 4);
}

עכשיו נוכל לשבת בשקט כי כתבנו קוד שמעוצב היטב!

לא!
הקוד הזה בעצם “כללי מדי”.

Over-design - עיצוב יתר

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

  1. KISS - Keep it simple stupid
  2. YAGNI - You ain’t gonna need it
  3. אבסטרקצית יתר
  4. שימוש בתבניות עיצוב
  5. מסדי נתונים ושמירת נתונים

Keep it simple stupid - עקרון הפשטות

הפשטות היא תכונה המאפשרת לנו ליצור אינטרקציות בסיסיות מבלי להכליל את הפתרון.

בדוגמא הקודמת, כדי ליצור פשטות נוכל להמיר את הפתרון הכללי למשפט תנאי רגיל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

template<typename ...Args>
int Sum(Args... args)
{
return (args + ...);
}

template<typename ...Args>
int Substract(Args... args)
{
return (args - ...);
}

int main()
{
auto toSum = true;
auto result = toSum? Sum(1 , 3, 4) : Substract(4,3,1);
std::cout << result;
}

Ya aint gonna need it - עקרון מגדת העתידות

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
#include <iostream>

template<typename ...Args>
int Sum(Args... args)
{
return (args + ...);
}

template<typename ...Args>
int Substract(Args... args)
{
return (args - ...);
}

template<typename ...Args>
int Multiply(Args... args)
{
return (args * ...);
}

/*
Fix divide - check if one of them is zero.
template<typename ...Args>
int Divide(Args... args)
{
return (args / ...);
}
*/

int main()
{
auto toSum = true;
auto result = toSum? Sum(1 , 3, 4) : Substract(4,3,1);
std::cout << result;
}

מהן 2 הבעיות הטמונות לנו בקוד הזה?

אבסטרקציית יתר

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

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
53
54
55
56
57
58
59
60
#include <iostream>
#include <chrono>

using KiloWatt = int;

class ElectricProduct
{
public:
virtual KiloWatt CalculateConsumption(std::chrono::hours hours) = 0;
};

class ElectricalFan : public ElectricProduct
{
private:
enum class FanStregnth
{
Off = 0,
One,
Two,
Three,
Four
};
KiloWatt mHourlyConsumption;
FanStregnth mStrengthLevel;

public:
KiloWatt CalculateConsumption(std::chrono::hours hours)
{
// Each Strength adds 10% consumption
auto strengthMultipler = (1.1 * int(mStrengthLevel));
return (mHourlyConsumption * hours.count()) * strengthMultipler;
}
};

class ElectricityCalculator
{
public:
virtual KiloWatt CalculateProduct(std::chrono::hours) = 0;
}

class ProductCalculator : public ElectricityCalculator
{
private:
EletricalProduct* mProduct;

ProductCalculator(EletricalProduct* product) : mProduct(product)
{}

public:
KiloWatt CalculateProduct(std::chrono::hours hours) override
{
return mProduct->CalculateConsumption(hours);
}
};

int main()
{
auto calculator = std::make_unique<ProductCalculator>(GetProduct());
auto kwUsagePerDay = calculator->CalculateProduct(24h);
}

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

לעיתים אין צורך באסטרקציה זו,
תנסו לוותר על ה-Adapter, Factory, Manager וכדו’…

תבניות עיצוב

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface IStrategy
{
void Do();
}

class Strategy1 : IStrategy
{ public void Do() { }}


class Strategy2 : IStrategy
{ public void Do() { }}


class Strategy3 : IStrategy
{ public void Do() { }}

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

הכלה עדיפה מהורשה

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

ולפי העקרון שהזכרתי איך ניתן לפתור את זה?

  • לא להכניס תבניות עיצוב לא מתואמות.
    כל תבנית עיצוב צריכה להיות מותאמת לפתרון הספציפי.
    בלי להתאים אותו התבנית עיצוב תהיה “מגושמת”.
  • הכלה במקום הורשה.

דוגמא קלאסית תהיה הכלה של רשימות:

1
2
3
4
5
6
7
8
class MyObjectList
{
public:
void Add(Object obj);
void Remove(int index);

int CalculateObjectsForm();
};

מעיין מחלקה שמממשת רשימה, במקום זה ניתן להחזיק אובייקט רשימה קיים ועליו לבנות את הקוד:

1
2
3
4
5
6
7
8
class Calculator
{
private:
std::vector<MyObjectList> mList;

public:
int CalculateObjectsForm();
};

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

מסדי נתונים

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

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

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

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

ניתן לראות תבניות רבות בקוד המרוכז במסד נתונים:

  • קוד שקשור לשאילתות נדחף לתוך קוד הלוגיקה
1
2
3
4
5
6
7
MyRow GetData(string nameOfEmployee)
{
var statement = "select * from employes where name = :name";
/// ... some code

return dbLayer.Execute(statement,"name", nameOfEmployee);
}

תחושה שגויה של “חלוקה” אבל ה-dbLayer לא מהווה פה אבסטרקציה מספיק חזקה.

  • אף מידע לא נשמר - כולו נלקח משכבת המסד
1
2
3
4
5
6
7
8
9
10
void DoCalculation(IEnumerable<string> employees)
{
var allEmployees = dbLayer.GetAll(employees);
var totalToPay = allEmployees.Select(e=> e.Salary).Sum();
var funds = dbLayer.GetFunds();
if(funds >= totalToPay)
{
Message.Show($"Able to pay {funds}₪");
}
}
  • קשה לכתוב טסטים כי יש תלות גדולה מדי במסד נתונים
1
2
3
4
5
void UntestableMethod()
{
var all = dbLayer.Execute("SELECT * FROM EMPLOYES");
var lastNames = all.Select(e => e.LastName);
}

דיאגרמה נכונה לשכבות בלתי תלויות

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

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

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

5 עקרונות אלו יסייעו לכם לשמור על עיצוב נקי יותר:

1.

Keep it simple stupid

2.

Ya aint gonna need it

3.

שמרו על מספר נמוך של אבסטרקציות

4.

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

5.

קודם עצבו את הקוד ואחר כך בחרו את מסד הנתונים - עקרון העוגה.

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

על הפוסט

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

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#Design