Οι 3 τύποι μοτίβων σχεδίασης που όλοι οι προγραμματιστές πρέπει να γνωρίζουν (με παραδείγματα κώδικα του καθενός)

Τι είναι ένα σχέδιο σχεδίασης;

Τα μοτίβα σχεδιασμού είναι λύσεις επιπέδου σχεδιασμού για επαναλαμβανόμενα προβλήματα που συναντούμε συχνά οι μηχανικοί λογισμικού. Δεν είναι κωδικός - επαναλαμβάνω,ΚΩΔΙΚΟΣ . Είναι σαν μια περιγραφή του τρόπου αντιμετώπισης αυτών των προβλημάτων και του σχεδιασμού μιας λύσης.

Η χρήση αυτών των προτύπων θεωρείται καλή πρακτική, καθώς ο σχεδιασμός της λύσης είναι αρκετά δοκιμασμένος και δοκιμασμένος, με αποτέλεσμα μεγαλύτερη αναγνωσιμότητα του τελικού κώδικα. Τα μοτίβα σχεδιασμού δημιουργούνται συχνά και χρησιμοποιούνται από τις γλώσσες OOP, όπως η Java, στην οποία θα γραφτούν τα περισσότερα από τα παραδείγματα από εδώ και πέρα.

Τύποι σχεδίων σχεδίασης

Υπάρχουν περίπου 26 μοτίβα που ανακαλύφθηκαν αυτήν τη στιγμή (σχεδόν δεν νομίζω ότι θα τα κάνω όλα ...).

Αυτά τα 26 μπορούν να ταξινομηθούν σε 3 τύπους:

1. Δημιουργικότητα: Αυτά τα μοτίβα έχουν σχεδιαστεί για την τάξη. Μπορούν να είναι είτε πρότυπα δημιουργίας τάξεων είτε αντικείμενα δημιουργίας αντικειμένων

2. Δομικά: Αυτά τα σχέδια έχουν σχεδιαστεί σε σχέση με τη δομή και τη σύνθεση μιας τάξης. Ο κύριος στόχος των περισσότερων από αυτά τα μοτίβα είναι να αυξήσει τη λειτουργικότητα των τάξεων που συμμετέχουν, χωρίς να αλλάξει μεγάλο μέρος της σύνθεσής της.

3. Συμπεριφορά: Αυτά τα μοτίβα σχεδιάζονται ανάλογα με τον τρόπο με τον οποίο μια τάξη επικοινωνεί με άλλους.

Σε αυτήν την ανάρτηση, θα εξετάσουμε ένα βασικό σχέδιο σχεδίασης για κάθε διαβαθμισμένο τύπο.

Τύπος 1: Δημιουργική - Το μοτίβο σχεδίασης Singleton

Το Singleton Design Pattern είναι ένα δημιουργικό μοτίβο, του οποίου ο στόχος είναι να δημιουργήσει μόνο μία παρουσία μιας κλάσης και να παρέχει μόνο ένα παγκόσμιο σημείο πρόσβασης σε αυτό το αντικείμενο. Ένα συνήθως χρησιμοποιούμενο παράδειγμα μιας τέτοιας κλάσης στην Java είναι το Ημερολόγιο, όπου δεν μπορείτε να δημιουργήσετε μια παρουσία αυτής της κλάσης. Χρησιμοποιεί επίσης τη δική του getInstance()μέθοδο για να πάρει το αντικείμενο που θα χρησιμοποιηθεί.

Μια τάξη που χρησιμοποιεί το μοτίβο σχεδίασης singleton θα περιλαμβάνει,

  1. Μια ιδιωτική στατική μεταβλητή, που κρατά τη μοναδική παρουσία της τάξης.
  2. Ένας ιδιωτικός κατασκευαστής, οπότε δεν μπορεί να είναι οπουδήποτε αλλού.
  3. Μια δημόσια στατική μέθοδος, για την επιστροφή της μοναδικής παρουσίας της τάξης.

Υπάρχουν πολλές διαφορετικές εφαρμογές του singleton design. Σήμερα, θα περάσω από τις υλοποιήσεις του.

1. Πρόθυμη Instantiation

2. Τεμπέλη Instantiation

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; } }

Αυτός ο τύπος εμφάνισης συμβαίνει κατά τη φόρτωση της τάξης, καθώς η παρουσία της μεταβλητής παρουσίας συμβαίνει εκτός οποιασδήποτε μεθόδου. Αυτό δημιουργεί ένα μεγάλο μειονέκτημα εάν αυτή η τάξη δεν χρησιμοποιείται καθόλου από την εφαρμογή πελάτη. Το σχέδιο έκτακτης ανάγκης, εάν αυτή η τάξη δεν χρησιμοποιείται, είναι το Lazy Instantiation.

Τεμπέλικες μέρες

Δεν υπάρχει μεγάλη διαφορά από την παραπάνω εφαρμογή. Οι κύριες διαφορές είναι ότι η στατική μεταβλητή δηλώνεται αρχικά μηδενική, και τεκμηριώνεται μόνο στη 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; } }

Αυτό διορθώνει ένα πρόβλημα, αλλά ένα άλλο εξακολουθεί να υπάρχει. Τι γίνεται αν δύο διαφορετικοί πελάτες έχουν πρόσβαση στην τάξη Singleton ταυτόχρονα, μέχρι το χιλιοστό του δευτερολέπτου; Λοιπόν, θα ελέγξουν εάν η παρουσία είναι μηδενική ταυτόχρονα, και θα το βρει αλήθεια, και έτσι θα δημιουργήσουν δύο παρουσίες της κλάσης για κάθε αίτημα από τους δύο πελάτες. Για να το διορθώσετε, πρέπει να εφαρμοστεί η εφαρμογή ασφαλούς νήματος

(Νήμα) Η ασφάλεια είναι το κλειδί

Στην Java, η λέξη-κλειδί συγχρονισμένη χρησιμοποιείται σε μεθόδους ή αντικείμενα για την εφαρμογή της ασφάλειας του νήματος, έτσι ώστε μόνο ένα νήμα να έχει πρόσβαση σε έναν συγκεκριμένο πόρο ταυτόχρονα. Το instantiation της τάξης τοποθετείται σε ένα συγχρονισμένο μπλοκ, έτσι ώστε η μέθοδος να είναι προσβάσιμη μόνο από έναν πελάτη σε μια δεδομένη στιγμή.

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μέθοδο, και εάν η τιμή είναι μόνη μηδενική, 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 προσθέτει καφέ γάλακτος με ζάχαρη ως μια άλλη υποκατηγορία σε κάθε κατηγορία μητρικών καφέ. Το υπόλοιπο του μήνα είναι ομαλή ιστιοπλοΐα, οι άνθρωποι παρατάσσονται για να πάρουν τον καφέ σας, πραγματικά κερδίζετε χρήματα. ??

Αλλά περιμένετε, υπάρχουν περισσότερα!

Ο κόσμος είναι εναντίον σας για άλλη μια φορά. Ένας αγωνιζόμενος ανοίγει απέναντι, με όχι μόνο 4 τύπους καφέ, αλλά και περισσότερα από 10 πρόσθετα! ;

Αγοράζετε όλα αυτά και πολλά άλλα, για να πουλήσετε καλύτερα καφέ μόνοι σας, και μόλις θυμηθείτε ότι ξεχάσατε να ενημερώσετε το συγκεκριμένο σύστημα χρέωσης. Πιθανότατα δεν μπορείτε να δημιουργήσετε τον άπειρο αριθμό υποκατηγοριών για οποιονδήποτε και όλους τους συνδυασμούς όλων των πρόσθετων, με τα νέα μείγματα καφέ. Για να μην αναφέρουμε, το μέγεθος του τελικού συστήματος.

Ώρα να επενδύσετε πραγματικά σε ένα σωστό σύστημα χρέωσης. Θα βρείτε νέο προσωπικό πληροφορικής, που ξέρει πραγματικά τι κάνουν και λένε.

“Why, this will be so much easier and smaller if it used the decorator pattern.”

What on earth is that?

The decorator design pattern falls into the structural category, that deals with the actual structure of a class, whether is by inheritance, composition or both. The goal of this design is to modify an objects’ functionality at runtime. This is one of the many other design patterns that utilize abstract classes and interfaces with composition to get its desired result.

Let’s give Math a chance (shudder?) to bring this all into perspective;

Take 4 coffee blends and 10 add-ons. If we stuck to the generation of subclasses for each different combination of all the add-ons for one type of coffee. That’s;

(10–1)² = 9² = 81 subclasses

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(); } }

Όπως μπορείτε να δείτε παραπάνω, ο Πελάτης κάνει μια Παραγγελία και ορίζει τον Παραλήπτη ως Σεφ. Η παραγγελία αποστέλλεται στον σερβιτόρο, ο οποίος θα γνωρίζει πότε θα εκτελέσει την παραγγελία (δηλαδή πότε θα δώσει στον σεφ την παραγγελία να μαγειρέψει) Όταν εκτελείται ο εισβολέας, η μέθοδος εκτέλεσης των παραγγελιών εκτελείται στον παραλήπτη (δηλ. Ο σεφ έχει την εντολή είτε να μαγειρέψει ζυμαρικά είτε να ψήσει κέικ;).

Γρήγορη ανακεφαλαίωση

Σε αυτήν την ανάρτηση περάσαμε:

  1. Τι είναι πραγματικά ένα σχέδιο σχεδίασης,
  2. Οι διαφορετικοί τύποι σχεδίων και γιατί είναι διαφορετικοί
  3. Ένα βασικό ή κοινό σχέδιο σχεδίασης για κάθε τύπο

Ελπίζω ότι αυτό ήταν χρήσιμο.  

Βρείτε τον κωδικό repo για την ανάρτηση, εδώ.