4 designmønstre, du bør kende til webudvikling: Observer, Singleton, strategi og dekoratør

Har du nogensinde været i et team, hvor du har brug for at starte et projekt fra bunden? Det er normalt tilfældet i mange nystartede virksomheder og andre små virksomheder.

Der er så mange forskellige programmeringssprog, arkitekturer og andre bekymringer, at det kan være svært at finde ud af, hvor man skal starte. Det er her designmønstre kommer ind.

Et designmønster er som en skabelon til dit projekt. Det bruger visse konventioner, og du kan forvente en bestemt form for adfærd fra det. Disse mønstre bestod af mange udvikleres oplevelser, så de virkelig ligner forskellige sæt bedste praksis.

Og du og dit team beslutter, hvilket sæt bedste praksis der er mest nyttigt for dit projekt. Baseret på det designmønster, du vælger, vil du alle begynde at have forventninger til, hvad koden skal gøre, og hvilket ordforråd I alle vil bruge.

Programmeringsdesignmønstre kan bruges på alle programmeringssprog og kan bruges til at passe til ethvert projekt, fordi de kun giver dig en generel oversigt over en løsning.

Der er 23 officielle mønstre fra bogen Design Patterns - Elements of Reusable Object-Oriented Software , der betragtes som en af ​​de mest indflydelsesrige bøger om objektorienteret teori og softwareudvikling.

I denne artikel vil jeg dække fire af disse designmønstre bare for at give dig et indblik i, hvad et par af mønstrene er, og hvornår du vil bruge dem.

Singleton-designmønsteret

Singleton-mønsteret tillader kun, at en klasse eller et objekt har en enkelt forekomst, og det bruger en global variabel til at gemme den forekomst. Du kan bruge doven indlæsning for at sikre, at der kun er en forekomst af klassen, fordi den kun opretter klassen, når du har brug for den.

Det forhindrer flere forekomster i at være aktive på samme tid, hvilket kan forårsage underlige bugs. Det meste af tiden bliver dette implementeret i konstruktøren. Målet med singleton-mønsteret er typisk at regulere den globale tilstand for en applikation.

Et eksempel på en singleton, som du sandsynligvis bruger hele tiden, er din logger.

Hvis du arbejder med nogle af front-end-rammerne som React eller Angular, ved du alt om, hvor vanskeligt det kan være at håndtere logfiler, der kommer fra flere komponenter. Dette er et godt eksempel på singletoner i aktion, fordi du aldrig vil have mere end en forekomst af et loggerobjekt, især hvis du bruger en slags fejlsporingsværktøj.

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

Nu behøver du ikke bekymre dig om at miste logfiler fra flere forekomster, fordi du kun har en i dit projekt. Så når du vil logge den mad, der er bestilt, kan du bruge den samme FoodLogger- forekomst på tværs af flere filer eller komponenter.

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

Med dette singleton-mønster på plads behøver du ikke bekymre dig om bare at få logfilerne fra hovedapplikationsfilen. Du kan få dem fra hvor som helst i din kodebase, og de vil alle gå til nøjagtig samme forekomst af loggeren, hvilket betyder, at ingen af ​​dine logfiler skal gå tabt på grund af nye forekomster.

Strategidesignmønsteret

Strategien er mønster er som en avanceret version af en hvis ellers udsagn. Det er dybest set hvor du laver en grænseflade til en metode, du har i din basisklasse. Denne grænseflade bruges derefter til at finde den rigtige implementering af den metode, der skal bruges i en afledt klasse. Implementeringen, i dette tilfælde, vil blive besluttet i løbetid baseret på klienten.

Dette mønster er utroligt nyttigt i situationer, hvor du har brug for og valgfri metoder til en klasse. Nogle forekomster af denne klasse har ikke brug for de valgfrie metoder, og det forårsager et problem for arveløsninger. Du kunne bruge grænseflader til de valgfrie metoder, men så skulle du skrive implementeringen hver gang du brugte den klasse, da der ikke ville være nogen standardimplementering.

Det er her, strategimønsteret redder os. I stedet for at klienten søger en implementering, delegerer den til en strategigrænseflade, og strategien finder den rigtige implementering. En almindelig anvendelse af dette er med betalingsbehandlingssystemer.

Du kan have en indkøbskurv, der kun lader kunder tjekke ud med deres kreditkort, men du mister kunder, der ønsker at bruge andre betalingsmetoder.

Strategidesignmønsteret lader os afkoble betalingsmetoderne fra kassen, hvilket betyder, at vi kan tilføje eller opdatere strategier uden at ændre nogen kode i indkøbskurven eller kassen.

Her er et eksempel på implementering af et strategimønster ved hjælp af eksemplet med betalingsmetode.

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

For at implementere vores betalingsmetodestrategi lavede vi en enkelt klasse med flere statiske metoder. Hver metode tager den samme parameter, kundeInfo , og denne parameter har en defineret type kundeInfoType . (Hej alle jeres TypeScript-devs! ??) Vær opmærksom på, at hver metode har sin egen implementering og bruger forskellige værdier fra kundeinformationen .

Med strategimønsteret kan du også ændre den strategi, der bruges i løbetid, dynamisk. Det betyder, at du vil være i stand til at ændre strategien eller implementeringen af ​​metoden, der bruges baseret på brugerinput eller det miljø, som appen kører i.

Du kan også indstille en standardimplementering i en simpel config.json- fil som denne:

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

Hver gang en kunde begynder at gennemgå kassen på dit websted, er standardbetalingsmetoden, de støder på, PayPal-implementeringen, der kommer fra config.json . Dette kan let opdateres, hvis kunden vælger en anden betalingsmetode.

Nu opretter vi en fil til vores betalingsproces.

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)

Denne Checkout- klasse er hvor strategimønsteret kan vise sig. Vi importerer et par filer, så vi har de tilgængelige betalingsmetodestrategier og standardstrategien fra konfigurationen .

Derefter opretter vi klassen med konstruktøren og en reserveværdi for standardstrategien , hvis der ikke er et sæt i konfigurationen . Dernæst tildeler vi strategiværdien til en lokal statsvariabel.

En vigtig metode, vi skal implementere i vores Checkout- klasse, er evnen til at ændre betalingsstrategien. En kunde kan ændre den betalingsmetode, de vil bruge, og du skal være i stand til at håndtere det. Det er, hvad changeStrategy- metoden er beregnet til.

Når du har foretaget en smuk kodning og fået alle input fra en kunde, kan du straks opdatere betalingsstrategien baseret på deres input, og den indstiller strategien dynamisk , før betalingen sendes til behandling.

På et eller andet tidspunkt er du muligvis nødt til at tilføje flere betalingsmetoder til din indkøbskurv, og alt hvad du skal gøre er at føje det til klassen PaymentMethodStrategy . Det vil straks være tilgængeligt overalt, hvor klassen bruges.

Strategimønsteret er stærkt, når du har at gøre med metoder, der har flere implementeringer. Det føles måske som om du bruger en grænseflade, men du behøver ikke at skrive en implementering til metoden hver gang du kalder den i en anden klasse. Det giver dig mere fleksibilitet end grænseflader.

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.

Har du brugt et af de andre designmønstre til dine projekter? De fleste steder vælger normalt et designmønster til deres projekter og holder fast i det, så jeg vil gerne høre fra dig alt om, hvad du bruger.

Tak for læsningen. Du bør følge mig på Twitter, fordi jeg normalt sender nyttige / underholdende ting: @FlippedCoding