4 σχέδια σχεδίασης που πρέπει να γνωρίζετε για την ανάπτυξη Ιστού: Παρατηρητής, Singleton, Στρατηγική και Διακοσμητής

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

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

Ένα μοτίβο σχεδίασης είναι σαν ένα πρότυπο για το έργο σας. Χρησιμοποιεί ορισμένες συμβάσεις και μπορείτε να περιμένετε ένα συγκεκριμένο είδος συμπεριφοράς από αυτό. Αυτά τα μοτίβα αποτελούνταν από πολλές εμπειρίες προγραμματιστών, έτσι είναι πραγματικά σαν διαφορετικά σύνολα βέλτιστων πρακτικών.

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

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

Υπάρχουν 23 επίσημα μοτίβα από το βιβλίο Design Patterns - Elements of Reusable Object-Oriented Software , το οποίο θεωρείται ένα από τα πιο σημαντικά βιβλία για την αντικειμενοστρεφή θεωρία και ανάπτυξη λογισμικού.

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

Το μοτίβο σχεδίασης Singleton

Το μοτίβο singleton επιτρέπει μόνο σε μια κλάση ή αντικείμενο να έχει μία παρουσία και χρησιμοποιεί μια καθολική μεταβλητή για την αποθήκευση αυτής της παρουσίας. Μπορείτε να χρησιμοποιήσετε τεμπέλης φόρτωση για να βεβαιωθείτε ότι υπάρχει μόνο μία παρουσία της τάξης, επειδή θα δημιουργήσει την τάξη μόνο όταν τη χρειάζεστε.

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

Ένα παράδειγμα ενός singleton που πιθανότατα χρησιμοποιείτε συνεχώς είναι ο καταγραφέας σας.

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

class FoodLogger { constructor() { this.foodLog = [] } log(order) { this.foodLog.push(order.foodItem) // do fancy code to send this log somewhere } } // this is the singleton class FoodLoggerSingleton { constructor() { if (!FoodLoggerSingleton.instance) { FoodLoggerSingleton.instance = new FoodLogger() } } getFoodLoggerInstance() { return FoodLoggerSingleton.instance } } module.exports = FoodLoggerSingleton

Τώρα δεν χρειάζεται να ανησυχείτε για την απώλεια αρχείων καταγραφής από πολλές περιπτώσεις, επειδή έχετε μόνο ένα στο έργο σας. Έτσι, όταν θέλετε να καταγράψετε το φαγητό που έχει παραγγελθεί, μπορείτε να χρησιμοποιήσετε την ίδια παρουσία FoodLogger σε πολλά αρχεία ή στοιχεία.

const FoodLogger = require('./FoodLogger') const foodLogger = new FoodLogger().getFoodLoggerInstance() class Customer { constructor(order) { this.price = order.price this.food = order.foodItem foodLogger.log(order) } // other cool stuff happening for the customer } module.exports = Customer
const FoodLogger = require('./FoodLogger') const foodLogger = new FoodLogger().getFoodLoggerInstance() class Restaurant { constructor(inventory) { this.quantity = inventory.count this.food = inventory.foodItem foodLogger.log(inventory) } // other cool stuff happening at the restaurant } module.exports = Restaurant

Με αυτό το μοτίβο singleton στη θέση του, δεν χρειάζεται να ανησυχείτε για τη λήψη των αρχείων καταγραφής από το κύριο αρχείο της εφαρμογής. Μπορείτε να τα πάρετε από οπουδήποτε στη βάση κώδικα και όλα θα πάνε στην ίδια ακριβώς εμφάνιση του καταγραφικού, πράγμα που σημαίνει ότι κανένα από τα αρχεία καταγραφής σας δεν θα πρέπει να χαθεί λόγω νέων περιπτώσεων.

Το σχέδιο σχεδιασμού στρατηγικής

Η στρατηγική είναι μοτίβο είναι σαν μια προηγμένη έκδοση μιας δήλωσης if else. Είναι βασικά όπου δημιουργείτε μια διεπαφή για μια μέθοδο που έχετε στην βασική σας τάξη. Αυτή η διεπαφή χρησιμοποιείται στη συνέχεια για να βρει τη σωστή εφαρμογή αυτής της μεθόδου που πρέπει να χρησιμοποιηθεί σε μια παράγωγη κλάση. Η εφαρμογή, σε αυτήν την περίπτωση, θα αποφασιστεί κατά το χρόνο εκτέλεσης με βάση τον πελάτη.

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

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

Θα μπορούσατε να έχετε ένα καλάθι αγορών που επιτρέπει στους πελάτες να κάνουν check out μόνο με τις πιστωτικές τους κάρτες, αλλά θα χάσετε πελάτες που θέλουν να χρησιμοποιήσουν άλλους τρόπους πληρωμής.

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

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

class PaymentMethodStrategy { const customerInfoType = { country: string emailAddress: string name: string accountNumber?: number address?: string cardNumber?: number city?: string routingNumber?: number state?: string } static BankAccount(customerInfo: customerInfoType) { const { name, accountNumber, routingNumber } = customerInfo // do stuff to get payment } static BitCoin(customerInfo: customerInfoType) { const { emailAddress, accountNumber } = customerInfo // do stuff to get payment } static CreditCard(customerInfo: customerInfoType) { const { name, cardNumber, emailAddress } = customerInfo // do stuff to get payment } static MailIn(customerInfo: customerInfoType) { const { name, address, city, state, country } = customerInfo // do stuff to get payment } static PayPal(customerInfo: customerInfoType) { const { emailAddress } = customerInfo // do stuff to get payment } }

Για να εφαρμόσουμε τη στρατηγική μεθόδου πληρωμής, κάναμε μια τάξη με πολλές στατικές μεθόδους. Κάθε μέθοδος παίρνει την ίδια παράμετρο, customerInfo και αυτή η παράμετρος έχει έναν καθορισμένο τύπο customerInfoType . (Γεια σε όλους εσείς TypeScript devs! ??) Λάβετε υπόψη ότι κάθε μέθοδος έχει τη δική της εφαρμογή και χρησιμοποιεί διαφορετικές τιμές από το customerInfo .

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

Μπορείτε επίσης να ορίσετε μια προεπιλεγμένη εφαρμογή σε ένα απλό αρχείο config.json όπως αυτό:

{ "paymentMethod": { "strategy": "PayPal" } }

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

Τώρα θα δημιουργήσουμε ένα αρχείο για τη διαδικασία ολοκλήρωσης αγοράς.

const PaymentMethodStrategy = require('./PaymentMethodStrategy') const config = require('./config') class Checkout { constructor(strategy='CreditCard') { this.strategy = PaymentMethodStrategy[strategy] } // do some fancy code here and get user input and payment method changeStrategy(newStrategy) { this.strategy = PaymentMethodStrategy[newStrategy] } const userInput = { name: 'Malcolm', cardNumber: 3910000034581941, emailAddress: '[email protected]', country: 'US' } const selectedStrategy = 'Bitcoin' changeStrategy(selectedStrategy) postPayment(userInput) { this.strategy(userInput) } } module.exports = new Checkout(config.paymentMethod.strategy)

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

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

Μια σημαντική μέθοδος που πρέπει να εφαρμόσουμε στην τάξη του Checkout είναι η δυνατότητα αλλαγής της στρατηγικής πληρωμής. Ένας πελάτης μπορεί να αλλάξει τον τρόπο πληρωμής που θέλει να χρησιμοποιήσει και θα πρέπει να είστε σε θέση να το χειριστείτε. Για αυτό είναι η μέθοδος changeStrategy .

After you've done some fancy coding and gotten all of the inputs from a customer, then you can update the payment strategy immediately based on their input and it dynamically sets the strategy before the payment is sent for processing.

At some point you might need to add more payment methods to your shopping cart and all you'll have to do is add it to the PaymentMethodStrategy class. It'll instantly be available anywhere that class is used.

The strategy design pattern is a powerful one when you are dealing with methods that have multiple implementations. It might feel like you're using an interface, but you don't have to write an implementation for the method every time you call it in a different class. It gives you more flexibility than interfaces.

The Observer Design Pattern

If you've ever used the MVC pattern, you've already used the observer design pattern. The Model part is like a subject and the View part is like an observer of that subject. Your subject holds all of the data and the state of that data. Then you have observers, like different components, that will get that data from the subject when the data has been updated.

The goal of the observer design pattern is to create this one-to-many relationship between the subject and all of the observers waiting for data so they can be updated. So anytime the state of the subject changes, all of the observers will be notified and updated instantly.

Some examples of when you would use this pattern include: sending user notifications, updating, filters, and handling subscribers.

Say you have a single page application that has three feature dropdown lists that are dependent on the selection of a category from a higher level dropdown. This is common on many shopping sites, like Home Depot. You have a bunch of filters on the page that are dependent on the value of a top-level filter.

The code for the top-level dropdown might look something like this:

class CategoryDropdown { constructor() { this.categories = ['appliances', 'doors', 'tools'] this.subscriber = [] } // pretend there's some fancy code here subscribe(observer) { this.subscriber.push(observer) } onChange(selectedCategory) { this.subscriber.forEach(observer => observer.update(selectedCategory)) } }

This CategoryDropdown file is a simple class with a constructor that initializes the category options we have available for in the dropdown. This is the file you would handle retrieving a list from the back-end or any kind of sorting you want to do before the user sees the options.

The subscribe method is how each filter created with this class will receive updates about the state of the observer.

The onChange method is how we send out notification to all of the subscribers that a state change has happened in the observer they're watching. We just loop through all of the subscribers and call their update method with the selectedCategory.

The code for the other filters might look something like this:

class FilterDropdown { constructor(filterType) { this.filterType = filterType this.items = [] } // more fancy code here; maybe make that API call to get items list based on filterType update(category) { fetch('//example.com') .then(res => this.items(res)) } }

This FilterDropdown file is another simple class that represents all of the potential dropdowns we might use on a page. When a new instance of this class is created, it needs to be passed a filterType. This could be used to make specific API calls to get the list of items.

The update method is an implementation of what you can do with the new category once it has been sent from the observer.

Now we'll take a look at what it means to use these files with the observer pattern:

const CategoryDropdown = require('./CategoryDropdown') const FilterDropdown = require('./FilterDropdown') const categoryDropdown = new CategoryDropdown() const colorsDropdown = new FilterDropdown('colors') const priceDropdown = new FilterDropdown('price') const brandDropdown = new FilterDropdown('brand') categoryDropdown.subscribe(colorsDropdown) categoryDropdown.subscribe(priceDropdown) categoryDropdown.subscribe(brandDropdown)

What this file shows us is that we have 3 drop-downs that are subscribers to the category drop-down observable. Then we subscribe each of those drop-downs to the observer. Whenever the category of the observer is updated, it will send out the value to every subscriber which will update the individual drop-down lists instantly.

The Decorator Design Pattern

Using the decorator design pattern is fairly simple. You can have a base class with methods and properties that are present when you make a new object with the class. Now say you have some instances of the class that need methods or properties that didn't come from the base class.

You can add those extra methods and properties to the base class, but that could mess up your other instances. You could even make sub-classes to hold specific methods and properties you need that you can't put in your base class.

Either of those approaches will solve your problem, but they are clunky and inefficient. That's where the decorator pattern steps in. Instead of making your code base ugly just to add a few things to an object instance, you can tack on those specific things directly to the instance.

So if you need to add a new property that holds the price for an object, you can use the decorator pattern to add it directly to that particular object instance and it won't affect any other instances of that class object.

Have you ever ordered food online? Then you've probably encountered the decorator pattern. If you're getting a sandwich and you want to add special toppings, the website isn't adding those toppings to every instance of sandwich current users are trying to order.

Here's an example of a customer class:

class Customer { constructor(balance=20) { this.balance = balance this.foodItems = [] } buy(food) { if (food.price) < this.balance { console.log('you should get it') this.balance -= food.price this.foodItems.push(food) } else { console.log('maybe you should get something else') } } } module.exports = Customer

And here's an example of a sandwich class:

class Sandwich { constructor(type, price) { this.type = type this.price = price } order() { console.log(`You ordered a ${this.type} sandwich for $ ${this.price}.`) } } class DeluxeSandwich { constructor(baseSandwich) { this.type = `Deluxe ${baseSandwich.type}` this.price = baseSandwich.price + 1.75 } } class ExquisiteSandwich { constructor(baseSandwich) { this.type = `Exquisite ${baseSandwich.type}` this.price = baseSandwich.price + 10.75 } order() { console.log(`You ordered an ${this.type} sandwich. It's got everything you need to be happy for days.`) } } module.exports = { Sandwich, DeluxeSandwich, ExquisiteSandwich }

This sandwich class is where the decorator pattern is used. We have a Sandwich base class that sets the rules for what happens when a regular sandwich is ordered. Customers might want to upgrade sandwiches and that just means an ingredient and price change.

You just wanted to add the functionality to increase the price and update the type of sandwich for the DeluxeSandwich without changing how it's ordered. Although you might need a different order method for an ExquisiteSandwich because there is a drastic change in the quality of ingredients.

The decorator pattern lets you dynamically change the base class without affecting it or any other classes. You don't have to worry about implementing functions you don't know, like with interfaces, and you don't have to include properties you won't use in every class.

Now if we'll go over an example where this class is instantiated as if a customer was placing a sandwich order.

const { Sandwich, DeluxeSandwich, ExquisiteSandwich } = require('./Sandwich') const Customer = require('./Customer') const cust1 = new Customer(57) const turkeySandwich = new Sandwich('Turkey', 6.49) const bltSandwich = new Sandwich('BLT', 7.55) const deluxeBltSandwich = new DeluxeSandwich(bltSandwich) const exquisiteTurkeySandwich = new ExquisiteSandwich(turkeySandwich) cust1.buy(turkeySandwich) cust1.buy(bltSandwich)

Final Thoughts

I used to think that design patterns were these crazy, far-out software development guidelines. Then I found out I use them all the time!

A few of the patterns I covered are used in so many applications that it would blow your mind. They are just theory at the end of the day. It's up to us as developers to use that theory in ways that make our applications easy to implement and maintain.

Έχετε χρησιμοποιήσει κάποιο από τα άλλα σχέδια σχεδίασης για τα έργα σας; Τα περισσότερα μέρη συνήθως επιλέγουν ένα μοτίβο σχεδίασης για τα έργα τους και κολλάνε με αυτό, γι 'αυτό θα ήθελα να ακούσω από όλους σας για το τι χρησιμοποιείτε.

Ευχαριστώ για την ανάγνωση. Θα πρέπει να με ακολουθήσετε στο Twitter γιατί συνήθως δημοσιεύω χρήσιμα / διασκεδαστικά πράγματα: @FlippedCoding