חזור לתיקייה: 02340124 - מבוא לתכנות מערכות

6 - עיצוב תוכנה ו-UML

חיפוש בתיקיה
Ctrl K
לחץ כאן או הקש Ctrl K כדי לחפש בכל התיקיה
  • UML
  • דיאגרמת מחלקות
  • שימוש בדיאגרמת מחלקות
  • דיאגרמת רצף
  • תבניות עיצוב תוכנה
  • Composite
  • תבניות עיצוב התנהגותיות
  • תבנית עיצוב Strategy
  • תבנית עיצוב Command
  • תבניות עיצוב מבניות
  • תבנית עיצוב Adapter
  • תבנית עיצוב Decorator
  • תבניות עיצוב יצרניות
  • תבנית עיצוב Factory
  • תבנית עיצוב Singleton

6 - עיצוב תוכנה ו-UML

02340124

מבוא לתכנות מערכות

רשימות הרצאה לקריאה בשבת

עיצוב תוכנה ו-UML

הופק מתוך תקיית התואר של שקד

uni-folder.vercel.app

UMLUMLUML

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

האם אנו מתחילים ישר בקוד? !לא\large \color{red}\R{\text{!לא}}!לא
אנו נתחיל בלעצב אותו באמצעות UMLUMLUML שזה ראשי תיבות של Unified  Modeling  LanguageUnified\;Modeling\;LanguageUnifiedModelingLanguage שהיא הדרך הסטנדרטית להסתכל על עיצוב תוכנה ללא בחירת שפת תכנות.
יש פרק גדול יותר בהמשך על UMLUMLUML אבל כרגע נגדיר את הדיאגרמת המחלקות ודיאגרמת השלבים(SequenceSequenceSequence)

דיאגרמת מחלקות

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

דוגמה

עבור המחלקה:

class Player
{
	double healthPoints;
	double money;

public:
	void playCard(const Card& card);
};

נקבל את ה-UMLUMLUML הבא:

Player+healthPointsdouble+moneydouble+playCard()void
דוגמה

עבור כמה מחלקות, נניח:

class Card
{
	double price;
	double healthGain;
};

class CardGame
{
	Player* currentPlayer;
	
	Array<Player> players;
	Array<Card> cards;
	
public:
	void playTurn();
};

בנוסף למחלקה PlayerPlayerPlayer ממקודם, אזי נקבל:

Player+healthPointsdouble+moneydouble+playCard()voidCard+pricedouble+healthGaindoubleCardGame+currentPlayerPlayer*+playersArray<Player>+cardsArray<Card>+playTurn()void currentPlayer players cards
הערה

ניתן להשמיט שדות שאנו רואים בדיאגרמה כך:

Player+healthPointsdouble+moneydouble+playCard()voidCard+pricedouble+healthGaindoubleCardGame+playTurn()void currentPlayer players cards

שימוש בדיאגרמת מחלקות

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

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

נסתכל על ה-UMLUMLUML של היצור הזה:

System+getYearlyCost()void+giveRaise()voidEmployee-birth-name-salaryint+giveRaise()voidSalesPerson-commisionRate+giveRaise()voidEngineer-degrees+giveRaise()void employees

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

דיאגרמת רצף

דיאגרמת רצף מתארת את הזרימה הדינמית של מערכת.
​מתאאאאאאאמ 1.png
האדם העומד בהתחלה נקרא actoractoractor והוא מגדיר את המשתמש או מערכת אחרת או מכשיר אשר משתמש במערכת שלנו.

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

תבניות עיצוב תוכנה

למה אנו מסתכים על תבניות עיצוב?

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

ישנם שלושה סוגים של תבניות עיצוב:

  1. התנהגותיות: מתעסק בתקשורת בין מבנים שונים.
    דוגמה: איטרטורים, Strategy,  CommandStrategy,\;CommandStrategy,Command.
  2. מבנה: מתעסק עם חיבור של מבנים ואובייקטים ואיחודם לתוכנה אחת.
    דוגמה: Composite,  Adapter,  DecoratorComposite,\;Adapter,\;DecoratorComposite,Adapter,Decorator.
  3. יצירתי: מתעסק עם יצירה וגישה למבנים.
    דוגמה: Factory,  Abstract  Factory,  SingletonFactory,\;Abstract\;Factory,\;SingletonFactory,AbstractFactory,Singleton.

CompositeCompositeComposite

אנוחנו כבר ראינו את זה והוא מתאר היררכיה.
המקרה הכללי ביותר הינו:

ComponentLeafComposite

תבניות עיצוב התנהגותיות

כמעט בכל הפרק הזה נסתכל על דוגמה של משחק פוקר בו השחקן יכול לבצע את הפעולות call,fold,raisecall,fold,raisecall,fold,raise.
יהיו לנו את המחלקות הבאות:

class Player
{
public:
	string getName();
	int getMoney();
	
	void makeMove(Game& game);
};

class Game
{
	Player *players;
	Player *currentPlayer;
	
public:
	void fold();
	void call();
	void raise(int amount);
};

אנו נוסיף מחשב עם סוגי ההתנהגויות הבאות: Cautious,  Aggressive,  BluffingCautious,\;Aggressive,\;BluffingCautious,Aggressive,Bluffing וכו'.
כל שחקן מממש את makeMovemakeMovemakeMove בתצורה מעט שונה ⟸ מה אנו צריכים לעשות עם PlayerPlayerPlayer?

  • נוכל לשים enumenumenum אשר יכיל את סוגי המשחק ⟸ זה כמובן לא טוב במיוחד כי זה דורש הרבה התעסקות.
  • נוכל להפוך את השחקן לאבסטרקטי ולרשת ממנו לכל סוג שחקן, אך מה אם נרצה לשנות את סוג המשחק שלנו על בסיס אירועים במשחק? לא ניתן לרשת מכמה מחלקות(ניתן בעצם אבל לא בקורס הזה)

תבנית עיצוב StrategyStrategyStrategy

הפתרון שנבחר הוא לבצע decompositiondecompositiondecomposition ל-PlayerPlayerPlayer ולהוציא את ההתנהגות ממנו כך:

Player+makeMove(Game&)void+setBehaviour(Behaivour*)voidBehaviour+makeMove(Game&)voidAggressive+makeMove(Game&)voidBluffing+makeMove(Game&)voidCautious+makeMove(Game&)void behaviour

לדבר הזה קוראים באופן כללי אסטרטגיה.

המימוש של זה יראה כך:

class Behaviour
{
	virtual void makeMove(Game& game) = 0;
};

class Aggressive : public Behaviour
{
public:
	void makeMove(Game game&) override;
};

class Player
{
	Behaviour *behaviour;
	
public:
	void setBehaviour(Behaviour *behaviour) { this->behaviour = behaviour; }
	void makeMove(Game& game) { behaviour->makeMove(game); }
};

תבנית עיצוב CommandCommandCommand

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

הלוגיקה של ביצוע מהלך היא זהה לכל המהלכים מלבד האשכרה מהלך והלוגיקה של undoundoundo היא גם זהה לכל המהלכים.
בכך אנו נצור מחלקה GameMoveGameMoveGameMove כך:

GameGameMove+apply(Game&)voidCall+apply(Game&)voidFold+apply(Game&)voidRaise+apply(Game&)void

וכך נתכנת אותו:

class GameMove
{
public:
	virtual void apply(Game&) cont = 0;
	virtual void undo(Game&) cont = 0;
};

class Fold : public GameMove
{
public:
	void apply(Game&) const override;
	void undo(Game&) const override;
};

class Call : public GameMove
{
public:
	void apply(Game&) const override;
	void undo(Game&) const override;
};

class Raise : public GameMove
{
	int amount;
	
public:
	void apply(Game&) const override;
	void undo(Game&) const override;
};

class Game
{
	vector<GameMove*> moves;
public:
	void step()
	{
		currentPlayer->makeMove(*this);
	}
	
	void makeMove(GameMove* move)
	{
		cout << "Applied: " << move;
		moves.push_back(move);
		move->apply(*this);
		currentPlayer = nextPlayer();
	}
	
	void undo()
	{
		int n = moves.size();
		if (n > 0)
		{
			currentPlayer = prevPlayer();
			moves[n - 1]->undo(*this);
			moves.pop_back();
		}
	}
};

אם נשלב CommandCommandCommand ו-StrategyStrategyStrategy נקבל את ה-UMLUMLUML הבא:
​מתאאאאאאאמ 3.png
ודיאגרמת הרצף של היצור הזה יראה כך:
​מתאאאאאאאמ 4.png

תבניות עיצוב מבניות

תבנית עיצוב AdapterAdapterAdapter

נניח והיינו רוצים להוסיף GUIGUIGUI למשחק שלנו ונניח ויש ספריה שמישהו יצר עם GUIGUIGUI מושלם, והיא ממומשת כך:

namespace GuiLib
{
	typedef int Suite;
	typedef int Rank;
	struct Card;
	class Cards;
	class Agent;
	class CardGame;
	
	class GuiWindow;
}

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

GuiLib::Agent+getCards()CardsPlayerAdapter+getCards()CardsPlayer+getHand()Hand

וכך זה ימומש בקוד:

namespace Poker
{
	class PlayerAdapter : public GuiLib::Agent
	{
		Player* m_player;
	public:
		PlayerAdapter(Player* player) : m_player(player) {}
		
		string getName() const override
		{
			return m_player->getName();
		}
		
		GuiLib::Cards getCards() const override
		{
			return convertHandToCards(m_player->getHand());
		}
	}
}

תבנית עיצוב DecoratorDecoratorDecorator

נניח כעת כי ויתרנו על שימוש בספרייה ואנו רוצים לממש בעצמנו את ה-GUIGUIGUI.
בנוסף, נרצה להוסיף כמה פיצרים מעניינים:

  • תמונת פרופיל לכל שחקן
  • אנימציות
  • סטטיסטיקות

איך נוכל להוסיף את הדברים האלו בתצורה טובה?

  • הפתרון הבנאלי הוא לבצע ירושה, אבל אנו לא רוצים לעשות את זה כי זה לא תומך בכמה פיצרים שונים.
  • הפתרון האמיתי הוא CompostionCompostionCompostion

נסתכל על איך זה נראה:
​מתאאאאאאאמ 6.png
יש גם ירושה וגם שדה של מחלקת האב וכך זה נראה בקוד:

class GuiWindow
{
public:
	virtual void display(Game& game);
};

class AnimatingWindow : public GuiWindow
{
	GuiWindow* window;
public:
	AnimatingWindow(GuiWindow* window) : window(window) {}
	
	void display(Game& game) override
	{
		window->display(game);
		animate(game);
	}
};

ההתנהגות של זה נראה כך:

GuiWindow* animatingWindowWithStatistics = new StatisticsWindow(new AnimatingWindow(new GuiWindow()));
animatingWindowWithStatistics->display(game);

​מתאאאאאאאמ 7.png

תבניות עיצוב יצרניות

תבנית עיצוב FactoryFactoryFactory

אנו רוצים לקחת את המשחק שלנו ולהפוך אותו למשחק רשת.

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

בתכלס, אנו צריכים דרך ליצור הודעות מאוד שונות על בסיס המצב שלנו בתוכנה ובחוץ וזה הפתרון:
​מתאאאאאאאמ 8.png
בקוד זה יראה כך:

class MessageFactory
{
	bool m_isSecurityNeeded = false;
	
public:
	MessageFactory(bool isNeeded) : m_isSecurityNeeded(isNeeded) {}
	
	Message* create() const
	{
		if (!m_isSecturityNeeded)
			return new Message();
		else
			return new CryptoMessage();
	}
};

class Game
{
	MessageFactory* m_factory;

public:
	Game(MessageFactory* factory) : m_factory(factory) {}
	
	void sendMove(const GameMove& move) const
	{
		Message *message = m_factorty->create();
		message->sendMessage(move.description());
	}
};
הערה

ניתן כמובן לעשות גם מפעל אבסטרקטי וזה עדיף בהרבה מלא אבסטרקטי:
​מתאאאאאאאמ 9.png

תבנית עיצוב SingletonSingletonSingleton

לפעמים נרצה להבטיח כי למחלקה מסויימת יהיה רק מופע אחד. (נניח מחלקה של systemsystemsystem או מחלקה של מסך ראשי)
הבעיה ש-SingletonSingletonSingleton בה לפתור היא איך אנו מבטיחים שיש רק משתנה אחד ואיך נוכל לגשת אליו.

מימוש אחד אפשרי הינו משהו כזה:

class Game
{
	Game() {}
public:
	static Game& getInstance()
	{
		static Game instance;
		return instance;
	}
	
	Game(const Game& other) = delete;
	Game& operator=(const Game& other) = delete;
};

נשתמש בזה כך:

Game::getInstance().step();