4 dizajnerska uzorka koja biste trebali znati za web razvoj: Promatrač, Singleton, Strategija i Dekorator

Jeste li ikad bili u timu u kojem trebate započeti projekt od nule? To je obično slučaj u mnogim novoosnovanim poduzećima i drugim malim tvrtkama.

Postoji toliko mnogo različitih programskih jezika, arhitektura i drugih problema da može biti teško otkriti odakle početi. Tu dolaze dizajnerski obrasci.

Uzorak dizajna je poput predloška za vaš projekt. Koristi određene konvencije i od njega možete očekivati ​​određenu vrstu ponašanja. Ti su obrasci sačinjeni od iskustava mnogih programera pa su zaista poput različitih skupova najboljih praksi.

A vi i vaš tim odlučite koji je skup najboljih praksi najkorisniji za vaš projekt. Na temelju uzorka dizajna koji odaberete, svi ćete početi očekivati ​​što kôd treba raditi i koji ćete rječnik koristiti.

Uzorci dizajna programiranja mogu se koristiti u svim programskim jezicima i mogu se koristiti za uklapanje u bilo koji projekt jer vam daju samo općenite nacrte rješenja.

Postoje 23 službena uzorka iz knjige Dizajn uzoraka - elementi višekratno korištenog objektno orijentiranog softvera , koja se smatra jednom od najutjecajnijih knjiga o objektno orijentiranoj teoriji i razvoju softvera.

U ovom ću članku obraditi četiri od tih dizajnerskih uzoraka samo kako bih vam pružio uvid u to koji su nekoliko uzoraka i kada biste ih koristili.

Uzorak dizajna Singleton

Uzorak pojedinca omogućuje samo klasi ili objektu da ima jednu instancu i koristi globalnu varijablu za pohranu te instance. Možete koristiti lijeno učitavanje kako biste bili sigurni da postoji samo jedan primjerak klase jer će on stvoriti klasu samo kada vam zatreba.

To sprječava istovremeno aktiviranje više instanci što bi moglo prouzročiti čudne greške. Većinu vremena to se implementira u konstruktor. Cilj jednostrukog uzorka je obično regulirati globalno stanje aplikacije.

Primjer pojedinca koji vjerojatno cijelo vrijeme koristite je vaš zapisnik.

Ako radite s nekim front-end okvirima poput React-a ili Angular-a, znate sve o tome kako nezgodno može biti rukovanje zapisnicima koji dolaze iz više komponenata. Ovo je sjajan primjer jednokratnih postupaka u akciji jer nikada ne želite više od jednog primjerka zapisa dnevnika, pogotovo ako koristite neku vrstu alata za praćenje pogrešaka.

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

Sada ne morate brinuti da ćete izgubiti zapisnike s više instanci, jer u projektu imate samo jedan. Dakle, kada želite prijaviti naručenu hranu, možete koristiti istu instancu FoodLogger u više datoteka ili komponenata.

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

S ovim jedinstvenim uzorkom na mjestu, ne morate brinuti hoćete li samo dobiti zapisnike iz glavne datoteke aplikacije. Možete ih dobiti s bilo kojeg mjesta u vašoj bazi koda i svi će otići na potpuno istu instancu zapisnika, što znači da se niti jedan vaš dnevnik ne smije izgubiti zbog novih instanci.

Uzorak dizajna strategije

Strategija je obrazac poput napredne verzije if else izjave. U osnovi se izrađuje sučelje za metodu koju imate u osnovnoj klasi. To se sučelje zatim koristi za pronalaženje prave implementacije te metode koja bi se trebala koristiti u izvedenoj klasi. U ovom će se slučaju o provedbi odlučivati ​​tijekom izvođenja na temelju klijenta.

Ovaj je obrazac nevjerojatno koristan u situacijama kada imate potrebne i neobavezne metode za predavanje. Neke instance te klase neće trebati neobavezne metode, a to uzrokuje problem za rješenja nasljeđivanja. Možete koristiti sučelja za opcionalne metode, ali tada biste morali pisati implementaciju svaki put kada ste koristili tu klasu, jer ne bi bilo zadane implementacije.

Tu nas spašava obrazac strategije. Umjesto da klijent traži implementaciju, on prelazi na strateško sučelje i strategija pronalazi pravu implementaciju. Jedna uobičajena upotreba za to je sa sustavima za obradu plaćanja.

Mogli biste imati košaricu koja kupcima omogućuje samo odjavu putem njihovih kreditnih kartica, ali izgubit ćete kupce koji žele koristiti druge načine plaćanja.

Uzorak dizajna strategije omogućuje nam razdvajanje načina plaćanja od postupka plaćanja, što znači da možemo dodavati ili ažurirati strategije bez mijenjanja koda u košarici ili procesu plaćanja.

Evo primjera provedbe uzorka strategije pomoću primjera načina plaćanja.

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

Da bismo implementirali strategiju načina plaćanja, napravili smo jednu klasu s više statičkih metoda. Svaka metoda uzima isti parametar, customerInfo , a taj parametar ima definiranu vrstu customerInfoType . (Hej, svi vi TypeScript devs! ??) Imajte na umu da svaka metoda ima vlastitu implementaciju i koristi različite vrijednosti od customerInfo .

Uz obrazac strategije možete i dinamički mijenjati strategiju koja se koristi u vrijeme izvođenja. To znači da ćete moći promijeniti strategiju ili implementaciju metode koja se koristi na temelju korisničkog unosa ili okruženja u kojem se aplikacija izvodi.

Također možete postaviti zadanu implementaciju u jednostavnoj datoteci config.json poput ove:

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

Kad god kupac počne prolaziti kroz postupak plaćanja na vašoj web stranici, zadani način plaćanja s kojim se susretne bit će implementacija PayPala koja dolazi iz config.json . To se lako može ažurirati ako kupac odabere drugi način plaćanja.

Sada ćemo stvoriti datoteku za naš postupak naplate.

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)

Ova klasa Checkout je mjesto gdje se pokazuje strateški obrazac. Uvozimo nekoliko datoteka tako da imamo dostupne strategije načina plaćanja i zadanu strategiju iz konfiguracije .

Zatim kreiramo klasu s konstruktorom i zamjenskom vrijednošću za zadanu strategiju u slučaju da ona nije postavljena u konfiguraciji . Dalje vrijednost strategije dodjeljujemo lokalnoj varijabli stanja.

Važna metoda koju moramo primijeniti u našoj klasi Checkout je sposobnost promjene strategije plaćanja. Kupac može promijeniti način plaćanja koji želi koristiti, a vi ćete to morati podnijeti. Tome služi metoda changeStrategy .

Nakon što napravite neko otmjeno kodiranje i od kupca dobijete sve unose, tada možete odmah ažurirati strategiju plaćanja na temelju njihovog unosa i ona dinamički postavlja strategiju prije nego što se uplata pošalje na obradu.

U nekom ćete trenutku možda trebati dodati više načina plaćanja u košaricu, a sve što trebate učiniti je dodati u klasu PaymentMethodStrategy . Odmah će biti dostupan bilo gdje da se koristi klasa.

Uzorak dizajna strategije moćan je kada se bavite metodama koje imaju više implementacija. Možda se čini da koristite sučelje, ali ne morate pisati implementaciju za metodu svaki put kad je pozovete u drugoj klasi. Omogućuje vam veću fleksibilnost od sučelja.

Uzorak dizajnera promatrača

Ako ste ikada koristili MVC obrazac, već ste koristili obrazac dizajna promatrača. Model dio je poput subjekta, a View dio je poput promatrača te teme. Vaš subjekt čuva sve podatke i stanje tih podataka. Tada imate promatrače, poput različitih komponenata, koji će te podatke dobiti od subjekta kad se podaci ažuriraju.

Cilj obrasca dizajna promatrača je stvoriti ovaj odnos jedan-prema-više između subjekta i svih promatrača koji čekaju podatke kako bi se mogli ažurirati. Dakle, kad god se stanje subjekta promijeni, svi će promatrači biti odmah obaviješteni i ažurirani.

Neki primjeri kada biste koristili ovaj obrazac uključuju: slanje korisničkih obavijesti, ažuriranje, filtriranje i rukovanje pretplatnicima.

Recimo da imate aplikaciju s jednom stranicom koja ima tri padajuća popisa značajki koji ovise o odabiru kategorije s padajućeg izbornika više razine. To je uobičajeno na mnogim web lokacijama za kupnju, poput Home Depota. Na stranici imate hrpu filtara koji ovise o vrijednosti filtra najviše razine.

Kôd padajućeg izbornika najviše razine mogao bi izgledati otprilike ovako:

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

Ovaj izbornik Kategorija datoteka je jednostavna klasa pomoću konstruktora koji inicijalizira opcija kategorija imamo na raspolaganju za u padajućem izborniku. To je datoteka s kojom biste rukovali dohvaćanjem popisa iz pozadine ili bilo kojom vrstom sortiranja koju želite obaviti prije nego što korisnik vidi opcije.

Način pretplate je kako će svaki filtar stvoren s ovom klasom primati ažuriranja o stanju promatrača.

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.

Have you used any of the other design patterns for your projects? Most places usually pick a design pattern for their projects and stick with it so I'd like to hear from you all about what you use.

Thanks for reading. You should follow me on Twitter because I usually post useful/entertaining stuff: @FlippedCoding