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

מהו דפוס עיצוב?

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

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

סוגי דפוסי עיצוב

יש כרגע 26 דפוסים שהתגלו (אני כמעט לא חושב שאעשה את כולם ...).

ניתן לסווג את 26 אלה לשלושה סוגים:

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

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

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

בפוסט זה נעבור על תבנית עיצוב בסיסית אחת לכל סוג מסווג.

סוג 1: יצירה - דפוס העיצוב של סינגלטון

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

שיעור המשתמש בדפוס העיצוב של הסינגלטון יכלול,

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

יש הרבה מימושים שונים של עיצוב יחיד. היום אעבור על היישומים של;

1. Instantiation להוט

2. מיישר עצלן

3. מייצב בטיחות חוט

ביבר נלהב

public class EagerSingleton { // create an instance of the class. private static EagerSingleton instance = new EagerSingleton(); // private constructor, so it cannot be instantiated outside this class. private EagerSingleton() { } // get the only instance of the object created. public static EagerSingleton getInstance() { return instance; } }

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

ימים עצלים

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

public class LazySingleton { // initialize the instance as null. private static LazySingleton instance = null; // private constructor, so it cannot be instantiated outside this class. private LazySingleton() { } // check if the instance is null, and if so, create the object. public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }

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

(חוט) בטיחות היא המפתח

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

public class ThreadSafeSingleton { // initialize the instance as null. private static ThreadSafeSingleton instance = null; // private constructor, so it cannot be instantiated outside this class. private ThreadSafeSingleton() { } // check if the instance is null, within a synchronized block. If so, create the object public static ThreadSafeSingleton getInstance() { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } return instance; } }

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

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

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

// double locking is used to reduce the overhead of the synchronized method public static ThreadSafeSingleton getInstanceDoubleLocking() { if (instance == null) { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } } return instance; }

עכשיו אל הסיווג הבא.

סוג 2: מבני - תבנית עיצוב הקישוטים

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

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

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

"אפשר בבקשה קפה חלב עם סוכר?"

???

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

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

אבל רגע, יש עוד!

העולם נגדך שוב. מתחרה נפתח מעבר לרחוב, עם לא רק 4 סוגי קפה, אלא גם יותר מ -10 תוספות! ?

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

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

"למה, זה יהיה הרבה יותר קל וקטן אם ישתמש בדוגמת הקישוטים."

מה זה לכל הרוחות?

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

בואו ניתן למתמטיקה הזדמנות (לרעוד?) להביא את כל זה לפרספקטיבה;

קח 4 תערובות קפה ו -10 תוספות. אם עמדנו בדור מחלקות המשנה לכל שילוב אחר של כל התוספות לסוג קפה אחד. זה;

(10–1) ² = 9² = 81 מחלקות משנה

We subtract 1 from the 10, as you cannot combine one add-on with another of the same type, sugar with sugar sounds stupid. And that’s for just one coffee blend. Multiply that 81 by 4 and you get a whopping 324 different subclasses! Talk about all that coding…

But with the decorator pattern will require only 16 classes in this scenario. Wanna bet?

If we map out our scenario according to the class diagram above, we get 4 classes for the 4 coffee blends, 10 for each add-on and 1 for the abstract component and 1 more for the abstract decorator. See! 16! Now hand over that $100.?? (jk, but it will not be refused if given… just saying)

As you can see from above, just as the concrete coffee blends are subclasses of the beverage abstract class, the AddOn abstract class also inherits its methods from it. The add-ons, that are its subclasses, in turn inherit any new methods to add functionality to the base object when needed.

Let’s get to coding, to see this pattern in use.

First to make the Abstract beverage class, that all the different coffee blends will inherit from:

public abstract class Beverage { private String description; public Beverage(String description) { super(); this.description = description; } public String getDescription() { return description; } public abstract double cost(); }

Then to add both the concrete coffee blend classes.

public class HouseBlend extends Beverage { public HouseBlend() { super(“House blend”); } @Override public double cost() { return 250; } } public class DarkRoast extends Beverage { public DarkRoast() { super(“Dark roast”); } @Override public double cost() { return 300; } }

The AddOn abstract class also inherits from the Beverage abstract class (more on this below).

public abstract class AddOn extends Beverage { protected Beverage beverage; public AddOn(String description, Beverage bev) { super(description); this.beverage = bev; } public abstract String getDescription(); }

And now the concrete implementations of this abstract class:

public class Sugar extends AddOn { public Sugar(Beverage bev) { super(“Sugar”, bev); } @Override public String getDescription() { return beverage.getDescription() + “ with Mocha”; } @Override public double cost() { return beverage.cost() + 50; } } public class Milk extends AddOn { public Milk(Beverage bev) { super(“Milk”, bev); } @Override public String getDescription() { return beverage.getDescription() + “ with Milk”; } @Override public double cost() { return beverage.cost() + 100; } }

As you can see above, we can pass any subclass of Beverage to any subclass of AddOn, and get the added cost as well as the updated description. And, since the AddOn class is essentially of type Beverage, we can pass an AddOn into another AddOn. This way, we can add any number of add-ons to a specific coffee blend.

Now to write some code to test this out.

public class CoffeeShop { public static void main(String[] args) { HouseBlend houseblend = new HouseBlend(); System.out.println(houseblend.getDescription() + “: “ + houseblend.cost()); Milk milkAddOn = new Milk(houseblend); System.out.println(milkAddOn.getDescription() + “: “ + milkAddOn.cost()); Sugar sugarAddOn = new Sugar(milkAddOn); System.out.println(sugarAddOn.getDescription() + “: “ + sugarAddOn.cost()); } }

The final result is:

It works! We were able to add more than one add-on to a coffee blend and successfully update its final cost and description, without the need to make infinite subclasses for each add-on combination for all coffee blends.

Finally, to the last category.

Type 3: Behavioral - The Command Design Pattern

A behavioral design pattern focuses on how classes and objects communicate with each other. The main focus of the command pattern is to inculcate a higher degree of loose coupling between involved parties (read: classes).

Uhhhh… What’s that?

Coupling is the way that two (or more) classes that interact with each other, well, interact. The ideal scenario when these classes interact is that they do not depend heavily on each other. That’s loose coupling. So, a better definition for loose coupling would be, classes that are interconnected, making the least use of each other.

The need for this pattern arose when requests needed to be sent without consciously knowing what you are asking for or who the receiver is.

In this pattern, the invoking class is decoupled from the class that actually performs an action. The invoker class only has the callable method execute, which runs the necessary command, when the client requests it.

Let’s take a basic real-world example, ordering a meal at a fancy restaurant. As the flow goes, you give your order (command) to the waiter (invoker), who then hands it over to the chef(receiver), so you can get food. Might sound simple… but a bit meh to code.

The idea is pretty simple, but the coding goes around the nose.

The flow of operation on the technical side is, you make a concrete command, which implements the Command interface, asking the receiver to complete an action, and send the command to the invoker. The invoker is the person that knows when to give this command. The chef is the only one who knows what to do when given the specific command/order. So, when the execute method of the invoker is run, it, in turn, causes the command objects’ execute method to run on the receiver, thus completing necessary actions.

What we need to implement is;

  1. An interface Command
  2. A class Order that implements Command interface
  3. A class Waiter (invoker)
  4. A class Chef (receiver)

So, the coding goes like this:

Chef, the receiver

public class Chef { public void cookPasta() { System.out.println(“Chef is cooking Chicken Alfredo…”); } public void bakeCake() { System.out.println(“Chef is baking Chocolate Fudge Cake…”); } }

Command, the interface

public interface Command { public abstract void execute(); }

Order, the concrete command

public class Order implements Command { private Chef chef; private String food; public Order(Chef chef, String food) { this.chef = chef; this.food = food; } @Override public void execute() { if (this.food.equals(“Pasta”)) { this.chef.cookPasta(); } else { this.chef.bakeCake(); } } }

Waiter, the invoker

public class Waiter { private Order order; public Waiter(Order ord) { this.order = ord; } public void execute() { this.order.execute(); } }

You, the client

public class Client { public static void main(String[] args) { Chef chef = new Chef(); Order order = new Order(chef, “Pasta”); Waiter waiter = new Waiter(order); waiter.execute(); order = new Order(chef, “Cake”); waiter = new Waiter(order); waiter.execute(); } }

As you can see above, the Client makes an Order and sets the Receiver as the Chef. The Order is sent to the Waiter, who will know when to execute the Order (i.e. when to give the chef the order to cook). When the invoker is executed, the Orders’ execute method is run on the receiver (i.e. the chef is given the command to either cook pasta ? or bake cake?).

Quick recap

In this post we went through:

  1. What a design pattern really is,
  2. The different types of design patterns and why they are different
  3. One basic or common design pattern for each type

I hope this was helpful.  

Find the code repo for the post, here.