4 kujundusmustrit, mida peaksite veebiarenduse jaoks teadma: vaatleja, Singleton, strateegia ja kujundaja

Kas olete kunagi olnud meeskonnas, kus peate projekti nullist alustama? Nii juhtub tavaliselt paljudes idufirmades ja teistes väikeettevõtetes.

Erinevaid programmeerimiskeeli, arhitektuure ja muid probleeme on nii palju, et võib olla raske välja mõelda, millest alustada. Seal tulevad sisse kujundusmustrid.

Kujundusmuster on nagu teie projekti mall. See kasutab teatud kokkuleppeid ja võite eeldada, et see käitub konkreetselt. Need mustrid koosnesid paljude arendajate kogemustest, nii et need on tõesti nagu erinevad heade tavade komplektid.

Teie ja teie meeskond saavad otsustada, milline parimate tavade kogum on teie projekti jaoks kõige kasulikum. Teie valitud kujundusmustri põhjal hakkavad teil kõigil tekkima ootused, mida kood peaks tegema ja millist sõnavara te kõik kasutate.

Programmeerimise kujundusmustreid saab kasutada kõigis programmeerimiskeeltes ja neid saab kasutada iga projekti jaoks, sest need annavad teile ainult lahenduse üldise ülevaate.

Raamatust Design Patterns - Elements of Reusable Object-Oriented Software on 23 ametlikku mustrit , mida peetakse üheks kõige mõjukamaks raamatuks objektorienteeritud teooria ja tarkvaraarenduse kohta.

Selles artiklis käsitlen nelja neist kujundusmustritest, et anda teile ülevaade sellest, millised on mõned mustrid ja millal te neid kasutaksite.

Singletoni kujundusmuster

Üksikmuster võimaldab klassil või objektil olla ainult üks eksemplar ja selle eksemplari salvestamiseks kasutatakse globaalset muutujat. Võite kasutada laiskat laadimist veendumaks, et klassis on ainult üks eksemplar, sest see loob klassi ainult siis, kui seda vajate.

See takistab mitme eksemplari samaaegset aktiivsust, mis võib põhjustada imelikke vigu. Enamasti rakendatakse seda konstruktoris. Üksiku mustri eesmärk on tavaliselt reguleerida rakenduse globaalset olekut.

Teie loger on näiteks üksikisik, mida te tõenäoliselt kogu aeg kasutate.

Kui töötate mõne esiotsa raamistikuga, näiteks React või Angular, teate kõike, kui keeruline võib olla mitmest komponendist pärinevate logide käsitsemine. See on suurepärane näide üksikutest toimingutest, kuna te ei soovi kunagi rohkem kui ühte logijaobjekti eksemplari, eriti kui kasutate mingisugust veajälgimistööriista.

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

Nüüd ei pea te muretsema mitme eksemplari logide kaotamise pärast, kuna teie projektis on ainult üks. Nii et kui soovite tellitud toidu logida, saate kasutada sama FoodLoggeri eksemplari mitme faili või komponendi vahel.

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

Kui see üksikmuster on paigas, ei pea te muretsema logide hankimise pärast ainult peamisest rakendusfailist. Saate need kõikjalt oma koodibaasist ja nad lähevad täpselt samasse logija eksemplari, mis tähendab, et ükski teie logidest ei tohiks uute eksemplaride tõttu kaduma minna.

Strateegia kujundamise muster

Strateegia on muster nagu IF-i avalduse täiustatud versioon. Põhimõtteliselt tehakse siin liides meetodile, mis teil on oma baasklassis. Seejärel kasutatakse seda liidest selle meetodi õige rakenduse leidmiseks, mida tuleks tuletatud klassis kasutada. Rakendamine otsustatakse sel juhul käitamise ajal kliendi põhjal.

See muster on uskumatult kasulik olukordades, kus teil on klassi jaoks nõutavad ja valikulised meetodid. Mõni selle klassi eksemplar ei vaja valikulisi meetodeid ja see tekitab probleeme pärandlahenduste korral. Valikuliste meetodite jaoks võiksite kasutada liideseid, kuid siis peate rakenduse kirjutama iga kord, kui kasutate seda klassi, kuna vaikimisi rakendamist ei toimu.

Seal säästab strateegia muster meid. Selle asemel, et klient otsiks rakendust, delegeerib ta strateegia liidese ja strateegia leiab õige rakenduse. Üks levinumaid kasutusviise on maksete töötlemise süsteemides.

Teil võib olla ostukorv, mis võimaldab klientidel ainult krediitkaartidega tasuda, kuid kaotate kliendid, kes soovivad kasutada muid makseviise.

Strateegia kujundus võimaldab meil makseviisid lahutada kassast, mis tähendab, et saame strateegiaid lisada või uuendada, muutmata ühtegi koodi ostukorvis või kassas.

Siin on näide strateegia mustri rakendamisest makseviisi näite abil.

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

Makseviisi strateegia rakendamiseks tegime ühe klassi mitme staatilise meetodiga. Iga meetod võtab sama parameetri customerInfo ja sellel parameetril on määratletud tüüp clientInfoType . (Hei kõik, mida masinakirjas devs! ??) Võtke teadmiseks, et iga meetod on oma rakendamise ja kasutab erinevaid väärtusi customerInfo .

Strateegiamustri abil saate ka käitamise ajal kasutatavat strateegiat dünaamiliselt muuta. See tähendab, et saate muuta kasutatava strateegia või meetodi rakendamist vastavalt kasutaja sisendile või keskkonnale, milles rakendus töötab.

Vaikimisi juurutamise saate määrata ka sellises lihtsas config.jsoni failis:

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

Alati, kui klient hakkab teie veebisaidil makseprotsessi läbima, on vaikimisi makseviisiks PayPali juurutamine, mis pärineb config.jsonilt . Seda saab hõlpsasti uuendada, kui klient valib mõne muu makseviisi.

Nüüd loome meie kassasse faili.

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)

Selles Checkouti klassis saab strateegia muster näidata. Impordime paar faili, nii et meil on saadaval makseviisi strateegiad ja vaikestrateegia konfiguratsioonist .

Seejärel loome klassi konstruktori ja vaikestrateegia varuväärtuse juhul, kui konfiguratsioonis pole ühtegi komplekti määratud . Järgmisena määrame strateegia väärtuse kohaliku oleku muutujale.

Oluline meetod, mille peame oma Checkouti klassis kasutusele võtma, on võimalus muuta maksestrateegiat. Klient võib muuta soovitud makseviisi ja peate saama sellega hakkama. Selleks on meetod ChangeStrategy .

Kui olete kena kodeerimise teinud ja kõik kliendi sisendid hankinud, saate maksestrateegiat kohe tema sisendi põhjal värskendada ja see määrab strateegia dünaamiliselt enne makse töötlemiseks saatmist.

Mingil hetkel peate võib-olla oma ostukorvi lisama rohkem makseviise ja peate vaid lisama selle klassi PaymentMethodStrategy . See on koheselt saadaval kõikjal, kus seda klassi kasutatakse.

Strateegia kujundusmuster on võimas, kui tegelete meetoditega, millel on mitu rakendust. Võib tunduda, et kasutate liidest, kuid te ei pea kirjutama meetodi rakendust iga kord, kui helistate sellele erinevasse klassi. See annab teile rohkem paindlikkust kui liidesed.

Vaatleja kujundusmuster

Kui olete kunagi kasutanud MVC mustrit, olete juba kasutanud vaatleja kujundusmustrit. Mudeli osa on nagu subjekt ja vaade on nagu selle teema vaatleja. Teie subjekt omab kõiki andmeid ja nende olekut. Siis on teil vaatlejad, nagu erinevad komponendid, kes saavad need andmed subjektilt pärast andmete värskendamist.

Vaatleja kujundusmustri eesmärk on luua see üks-paljud-seos uuritava ja kõigi andmeid ootavate vaatlejate vahel, et neid saaks ajakohastada. Nii et kui vaatlusaluse olek muutub, teavitatakse neid kõiki vaatlejaid ja neid värskendatakse koheselt.

Mõned näited selle mustri kasutamisest: kasutajate märguannete saatmine, värskendamine, filtrid ja tellijatega tegelemine.

Oletame, et teil on üheleheline rakendus, millel on kolm funktsioonide rippmenüü loendit, mis sõltuvad kategooria valimisest kõrgema taseme rippmenüüst. See on levinud paljudel ostusaitidel, näiteks Home Depot. Teil on lehel hulk filtreid, mis sõltuvad tipptasemel filtri väärtusest.

Ülemise taseme rippmenüü kood võib välja näha umbes selline:

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

See CategoryDropdown- fail on lihtne konstruktoriga klass, mis initsialiseerib rippmenüüs saadaval olevad kategooriavalikud. See on fail, mida käsitsete loendi toomisel taustaprogrammist või mis tahes sorteerimist, mida soovite teha enne, kui kasutaja suvandeid näeb.

Tellida meetod on, kuidas iga filter loodud selle klassi saada värskendusi riigi vaatleja.

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.

Kas olete oma projektide jaoks kasutanud mõnda muud kujundusmustrit? Enamik kohti valib tavaliselt oma projektide kujundusmustri ja jääb sellest kinni, nii et tahaksin kõigilt kuulda, mida te kasutate.

Täname lugemast. Peaksite mind Twitteris jälgima, sest tavaliselt postitan kasulikke / meelelahutuslikke asju: @FlippedCoding