4 suunnittelumallia, jotka sinun tulisi tietää verkkokehitykselle: tarkkailija, Singleton, strategia ja sisustaja

Oletko koskaan ollut tiimissä, jossa sinun on aloitettava projekti alusta alkaen? Näin on yleensä monissa aloittelevissa yrityksissä ja muissa pienissä yrityksissä.

Ohjelmointikieliä, -arkkitehtuureja ja muita huolenaiheita on niin paljon, että voi olla vaikea selvittää mistä aloittaa. Siellä tulevat suunnittelumallit.

Suunnittelumalli on kuin malli projektillesi. Se käyttää tiettyjä käytäntöjä ja voit odottaa tietynlaista käyttäytymistä siitä. Nämä mallit koostuivat monien kehittäjien kokemuksista, joten ne ovat kuin erilaisia ​​parhaita käytäntöjä.

Ja sinä ja tiimisi saatte päättää, mitkä parhaista käytännöistä ovat hyödyllisimmät projektissanne. Valitsemasi suunnittelumallin perusteella teillä kaikilla on odotuksia siitä, mitä koodin pitäisi tehdä ja mitä sanastoa te kaikki käytätte.

Ohjelmointisuunnittelumalleja voidaan käyttää kaikilla ohjelmointikielillä, ja niitä voidaan käyttää mihin tahansa projektiin, koska ne antavat sinulle vain yleiskuvan ratkaisusta.

Kirjassa Design Patterns - Elements of Reusable Object-Oriented Software on 23 virallista mallia , jota pidetään yhtenä vaikuttavimmista objekeskeistä teoriaa ja ohjelmistokehitystä käsittelevistä kirjoista.

Tässä artikkelissa aion käsitellä neljä näistä suunnittelumalleista vain antaa sinulle käsityksen siitä, mitä muutamat mallit ovat ja milloin käytät niitä.

Singletonin suunnittelukuvio

Yksittäiskuvio sallii luokan tai objektin olla vain yksi esiintymä ja se käyttää globaalia muuttujaa kyseisen ilmentymän tallentamiseen. Voit käyttää laiskaa latausta varmistaaksesi, että luokassa on vain yksi esiintymä, koska se luo luokan vain, kun sitä tarvitset.

Tämä estää useita esiintymiä olemasta aktiivisia samanaikaisesti, mikä voi aiheuttaa outoja vikoja. Suurimman osan ajasta tämä toteutetaan konstruktorissa. Yksittäisen mallin tavoite on tyypillisesti säännellä sovelluksen globaalia tilaa.

Loggerisi on esimerkki singletistä, jota todennäköisesti käytät koko ajan.

Jos työskentelet joidenkin käyttöliittymäkehysten, kuten React tai Angular, kanssa, tiedät kaiken siitä, kuinka hankalaa voi olla useista komponenteista peräisin olevien lokien käsittely. Tämä on hieno esimerkki yksittäisistä toiminnoista, koska et koskaan halua useampaa kuin yhtä kirjausobjektin esiintymää, varsinkin jos käytät jonkinlaista virheenseurantatyökalua.

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

Nyt sinun ei tarvitse huolehtia useiden instanssien lokien menettämisestä, koska projektissasi on vain yksi. Joten kun haluat kirjata tilaamasi ruoan, voit käyttää samaa FoodLogger- esiintymää useissa tiedostoissa tai komponenteissa.

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

Kun tämä yksittäinen malli on paikallaan, sinun ei tarvitse huolehtia vain lokien hankkimisesta sovelluksen päätiedostosta. Voit saada ne mistä tahansa koodikannastasi, ja ne kaikki menevät täsmälleen samaan loggerin esiintymään, mikä tarkoittaa, että yksikään lokistasi ei saa kadota uusien ilmentymien vuoksi.

Strategian suunnittelumalli

Strategia on malli on kuin edistynyt versio if else -lausekkeesta. Siellä teet periaatteessa käyttöliittymän menetelmälle, joka sinulla on perusluokassasi. Tätä rajapintaa käytetään sitten löytämään menetelmän oikea toteutus, jota tulisi käyttää johdetussa luokassa. Toteutus, tässä tapauksessa, päätetään suorituksen aikana asiakkaan perusteella.

Tämä malli on uskomattoman hyödyllinen tilanteissa, joissa olet tarvinnut ja valinnaisia ​​menetelmiä luokalle. Jotkut kyseisen luokan esiintymät eivät tarvitse valinnaisia ​​menetelmiä, ja se aiheuttaa ongelmia perintöratkaisuille. Voisit käyttää rajapintoja valinnaisiin menetelmiin, mutta sitten sinun on kirjoitettava toteutus joka kerta, kun käytit kyseistä luokkaa, koska oletusarvoista toteutusta ei olisi.

Siellä strategiamalli säästää meitä. Sen sijaan, että asiakas etsii toteutusta, se delegoi strategiarajapinnan ja strategia löytää oikean toteutuksen. Yksi yleinen käyttö tähän on maksujen käsittelyjärjestelmissä.

Sinulla voi olla ostoskori, jonka avulla asiakkaat voivat tarkistaa vain luottokortillaan, mutta menetät asiakkaita, jotka haluavat käyttää muita maksutapoja.

Strategian suunnittelumallin avulla voimme erottaa maksutavat kassaprosessista, mikä tarkoittaa, että voimme lisätä tai päivittää strategioita muuttamatta koodia ostoskorissa tai kassalla.

Tässä on esimerkki strategiatavan toteutuksesta maksutavan esimerkin avulla.

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

Maksutapastrategiamme toteuttamiseksi teimme yhden luokan useilla staattisilla menetelmillä. Kukin menetelmä ottaa saman parametrin, customerInfo , ja kyseisellä parametrilla on määritelty tyyppi clientInfoType . (Hei kaikki te kirjoituskoneella devs! ??) toteavat, että jokainen menetelmä on omat toteuttamista ja käyttää eri arvoja customerInfo .

Strategiamallin avulla voit myös muuttaa dynaamisesti ajoaikana käytettävää strategiaa. Tämä tarkoittaa, että pystyt muuttamaan strategiaa tai menetelmän käyttöönottoa käyttäjän syötteen tai sovelluksen käyttämän ympäristön perusteella.

Voit myös asettaa oletustoteutuksen yksinkertaisessa config.json- tiedostossa näin:

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

Aina kun asiakas alkaa käydä läpi maksuprosessin verkkosivustollasi, oletusmaksutapa, jonka he kohtaavat, on PayPal-toteutus, joka tulee config.jsonista . Tämä voidaan helposti päivittää, jos asiakas valitsee toisen maksutavan.

Nyt luomme tiedoston kassaprosessiamme varten.

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)

Tämä Checkout- luokka on se, jossa strategiamalli saa näyttää. Tuomme pari tiedostoa, joten meillä on käytettävissä maksutapastrategiat ja oletusstrategia konfiguraatiosta .

Sitten luomme luokan rakentaja ja täytetakaus arvo oletuksena strategiaa siltä varalta ei ole ollut yhdet vuonna config . Seuraavaksi määritämme strategian arvon paikallisen tilan muuttujalle.

Tärkeä tapa, joka meidän on otettava käyttöön Checkout- luokassa, on kyky muuttaa maksustrategiaa. Asiakas saattaa muuttaa haluamansa maksutavan, ja sinun on pystyttävä käsittelemään se. Tätä varten changeStrategy- menetelmä on tarkoitettu.

Kun olet tehnyt hienon koodauksen ja saanut kaikki syötteet asiakkaalta, voit päivittää maksustrategian välittömästi heidän panoksensa perusteella ja se asettaa strategian dynaamisesti ennen kuin maksu lähetetään käsittelyyn.

Jossain vaiheessa sinun on ehkä lisättävä lisää maksutapoja ostoskoriin, ja sinun tarvitsee vain lisätä se PaymentMethodStrategy- luokkaan. Se on heti saatavilla missä tahansa luokan käytössä.

Strategian suunnittelumalli on tehokas, kun olet tekemisissä menetelmien kanssa, joissa on useita toteutuksia. Saattaa tuntua siltä, ​​että käytät käyttöliittymää, mutta sinun ei tarvitse kirjoittaa menetelmän toteutusta joka kerta, kun soitat sitä eri luokalle. Se antaa sinulle enemmän joustavuutta kuin rajapinnat.

Tarkkailijan suunnittelukuvio

Jos olet joskus käyttänyt MVC-mallia, olet jo käyttänyt tarkkailijan mallikuviota. Malli-osa on kuin aihe ja Näkymä-osa kuin kyseisen kohteen tarkkailija. Kohteellasi on kaikki tiedot ja niiden tila. Sitten sinulla on tarkkailijoita, kuten erilaisia ​​komponentteja, jotka saavat nämä tiedot kohteesta, kun tiedot on päivitetty.

Tarkkailijan suunnittelumallin tavoitteena on luoda tämä suhde moneen -suhde kohteen ja kaikkien tietoja odottavien tarkkailijoiden välille, jotta ne voidaan päivittää. Joten milloin tahansa kohteen tila muuttuu, kaikille tarkkailijoille ilmoitetaan ja päivitetään välittömästi.

Joitakin esimerkkejä siitä, milloin käytät tätä mallia, ovat: käyttäjäilmoitusten lähettäminen, päivittäminen, suodattimet ja tilaajien käsittely.

Oletetaan, että sinulla on yhden sivun sovellus, jolla on kolme ominaisuusluetteloa, jotka riippuvat luokan valinnasta ylemmän tason pudotusvalikosta. Tämä on yleistä monilla ostospaikoilla, kuten Home Depot. Sivulla on joukko suodattimia, jotka riippuvat ylätason suodattimen arvosta.

Ylätason avattavan valikon koodi voi näyttää tältä:

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

Tämä CategoryDropdown- tiedosto on yksinkertainen luokka konstruktorilla, joka alustaa avattavassa valikossa käytettävissä olevat luokkavaihtoehdot. Tämä on tiedosto, jonka käsittelet noudettaessa luetteloa taustasta tai minkä tahansa lajittelun, jonka haluat tehdä, ennen kuin käyttäjä näkee vaihtoehdot.

Tilata menetelmä on se, miten kukin suodatin luotu tähän luokkaan saavat päivityksiä tilasta tarkkailija.

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.

Oletko käyttänyt muita suunnittelumalleja projekteihisi? Useimmat paikat valitsevat yleensä suunnittelukuvion projekteilleen ja pitävät kiinni siitä, joten haluaisin kuulla teiltä kaikilta käyttämistänne.

Kiitos lukemisesta. Sinun tulisi seurata minua Twitterissä, koska lähetän yleensä hyödyllisiä / viihdyttäviä juttuja: @FlippedCoding