Voltál már olyan csapatban, ahol a nulláról kell kezdened a projektet? Ez általában sok induló és más kisvállalkozás esetében van így.
Olyan sokféle programozási nyelv, architektúra és egyéb aggály létezik, hogy nehéz lehet kitalálni, hogy hol kezdjük. Ott jönnek be a tervezési minták.
A tervezési minta olyan, mint a projekt sablonja. Bizonyos konvenciókat használ, és elvárhat tőle egy bizonyos fajta viselkedést. Ezek a minták sok fejlesztő tapasztalatából tevődtek össze, így valóban hasonlítanak a legjobb gyakorlatok különböző halmazaira.
Önnek és csapatának el kell döntenie, hogy melyik legjobb gyakorlat a leghasznosabb a projektjéhez. A választott tervezési minta alapján mindannyian várakozással tekintenek arra, hogy mit kell tennie a kódnak, és milyen szókincset fog használni mindannyian.
A programozási tervminták az összes programozási nyelven alkalmazhatók, és bármilyen projekthez illeszthetők, mert csak a megoldás általános vázlatát adják meg.
23 hivatalos minta található a Design Patterns - Elements of Reusable Object-Oriented Software című könyvből, amelyet az objektumorientált elmélet és a szoftverfejlesztés egyik legbefolyásosabb könyvének tartanak.
Ebben a cikkben négyet tervezek ezekből a tervezési mintákból, csak hogy betekintést nyújtsak abba, hogy mi a néhány minta, és mikor használná őket.
A Singleton tervezési minta
A szingulett minta csak egy osztálynak vagy objektumnak enged egyetlen példányt, és globális változót használ a példány tárolására. A lusta betöltéssel meggyőződhet arról, hogy az osztálynak csak egy példánya van, mert csak akkor hozza létre az osztályt, amikor szüksége van rá.
Ez megakadályozza, hogy egyszerre több példány aktív legyen, ami furcsa hibákat okozhat. Legtöbbször ez valósul meg a konstruktorban. A szingulett minta célja általában egy alkalmazás globális állapotának szabályozása.
A naplózó példája egy szingulettnek, amelyet valószínűleg állandóan használ.
Ha olyan front-end keretrendszerekkel dolgozik, mint a React vagy az Angular, mindent tud arról, hogy milyen trükkös lehet a több összetevőből származó naplók kezelése. Ez nagyszerű példa a működésben lévő szingulettekre, mert soha nem akar több naplózó objektumot, mint pl. Valamilyen hibakövető eszközt.
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
Most már nem kell aggódnia a több példányból származó naplók elvesztése miatt, mert a projektben csak egy van. Tehát, ha naplózni szeretné a megrendelt ételt, ugyanazt a FoodLogger példányt használhatja több fájlban vagy összetevőben.
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
Ha a helyén van ez az egyedi minta, akkor nem kell aggódnia, hogy csak a naplókat kapja meg a fő alkalmazásfájlból. Bárhonnan megszerezheti őket a kódbázisában, és mindegyik pontosan a naplózó példányába kerül, ami azt jelenti, hogy egyik naplója sem veszhet el az új példányok miatt.
A stratégiai tervezés mintája
A stratégia mintázatú, mint egy if else utasítás haladó változata. Alapvetően itt készít felületet az alaposztályában lévő módszerhez. Ezt az interfészt azután megtalálják a módszer megfelelő megvalósításának, amelyet egy származtatott osztályban kell használni. A megvalósítás ebben az esetben futás közben fog eldőlni az ügyfél alapján.
Ez a minta hihetetlenül hasznos olyan helyzetekben, amikor kötelező és opcionális módszereket igényel egy osztály számára. Az osztály egyes példányainak nem lesz szükségük az opcionális módszerekre, és ez problémát okoz az öröklési megoldásoknál. Használhat interfészeket az opcionális módszerekhez, de akkor minden alkalommal meg kell írnia a megvalósítást, amikor az adott osztályt használja, mivel nem lesz alapértelmezett megvalósítás.
Itt ment meg minket a stratégiai minta. Ahelyett, hogy az ügyfél megvalósítást keresne, egy stratégiai felületre delegál, és a stratégia megtalálja a megfelelő megvalósítást. Ennek egyik általános alkalmazása a fizetésfeldolgozó rendszerek.
Lehet, hogy van egy bevásárlókosara, amely csak az ügyfeleknek teszi lehetővé a hitelkártyájukkal történő fizetést, de elveszíti azokat az ügyfeleket, akik más fizetési módokat szeretnének használni.
A stratégia kialakítása lehetővé teszi a fizetési módok elválasztását a fizetési folyamattól, ami azt jelenti, hogy stratégiákat adhatunk hozzá vagy frissíthetünk anélkül, hogy a bevásárlókosárban vagy a fizetési folyamatban kódot változtatnánk.
Íme egy példa a stratégia mintájának megvalósítására a fizetési mód példáján keresztül.
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 } }
Fizetési mód stratégiánk megvalósításához egyetlen osztályt készítettünk, több statikus módszerrel. Minden metódus ugyanaz a paraméter, customerInfo , és hogy a paraméter egy meghatározott típusú customerInfoType . (Hey all you TypeScript devs! ??) Vegye figyelembe, hogy mindegyik módszer saját megvalósítással rendelkezik, és a customerInfo- tól eltérő értékeket használ .
A stratégiai mintával dinamikusan megváltoztathatja a futás közben használt stratégiát is. Ez azt jelenti, hogy megváltoztathatja a stratégiát vagy a módszer megvalósítását, amelyet a felhasználói bevitel vagy az alkalmazás futó környezete alapján használ.
Alapértelmezett megvalósítást is beállíthat egy ilyen egyszerű config.json fájlban:
{ "paymentMethod": { "strategy": "PayPal" } }
Amikor az ügyfél elkezdi a fizetési folyamatot az Ön webhelyén, az alapértelmezett fizetési mód a PayPal-implementáció lesz, amely a config.json- ból származik . Ez könnyen frissíthető, ha az ügyfél más fizetési módot választ.
Most létrehozunk egy fájlt a fizetési folyamathoz.
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)
Ebben a Checkout osztályban mutatkozhat meg a stratégiai minta. Pár fájlt importálunk, így rendelkezésre állnak a fizetési mód stratégiái és az alapértelmezett stratégia a konfigurációból .
Ezután létrehozzuk az osztályt a konstruktorral és az alapértelmezett stratégia tartalékértékével , ha a konfigurációban még nem lett volna beállítva . Ezután hozzárendeljük a stratégia értékét egy helyi állapot változóhoz.
Fontos módszer, amelyet a Checkout osztályunkban végre kell hajtanunk, a fizetési stratégia megváltoztatásának képessége. Az ügyfél megváltoztathatja a használni kívánt fizetési módot, és ezt Önnek tudnia kell kezelnie. Erre szolgál a changeStrategy módszer.
Miután elvégezte a divatos kódolást és megszerezte az összes inputot az ügyféltől, akkor azonnal frissítheti a fizetési stratégiát az ő bevitele alapján, és ez dinamikusan beállítja a stratégiát, mielőtt a fizetést feldolgozásra küldik.
Valamikor szükség lehet további fizetési módok felvételére a bevásárlókosárba, és csak annyit kell tennie, hogy hozzáadja a PaymentMethodStrategy osztályhoz. Azonnal elérhető lesz bárhol, ahol az osztályt használják.
A stratégia kialakításának mintája hatékony, ha olyan módszerekkel foglalkozik, amelyek többféle megvalósítással rendelkeznek. Úgy tűnhet, mintha egy felületet használna, de nem kell minden alkalommal végrehajtást írni a módszerhez, amikor más osztályba hívja. Nagyobb rugalmasságot biztosít, mint az interfészek.
A megfigyelő tervezési mintája
Ha valaha is használta az MVC mintát, akkor már használta a megfigyelő tervezési mintát. A Modell rész olyan, mint egy alany, a Nézet rész pedig olyan, mint az adott téma megfigyelője. Az alany rendelkezik az összes adattal és az adatok állapotával. Ezután vannak megfigyelői, mint a különböző komponensek, amelyek az adatokat frissítik, majd megkapják az alanytól.
A megfigyelői tervezési minta célja, hogy megteremtse ezt az egy a többihez kapcsolatot az alany és az összes adatra váró megfigyelő között, hogy frissíthessék őket. Tehát bármikor, amikor a téma állapota megváltozik, az összes megfigyelőt azonnal értesítik és frissítik.
Néhány példa a minta használatának idejére: felhasználói értesítések küldése, frissítés, szűrők és az előfizetők kezelése.
Tegyük fel, hogy van egyoldalas alkalmazása, amely három olyan szolgáltatás legördülő listával rendelkezik, amelyek egy magasabb szintű legördülő menüből választanak ki kategóriát. Ez sok bevásárlóhelyen jellemző, például a Home Depot-ban. Van egy csomó szűrő az oldalon, amelyek függenek a legfelső szintű szűrő értékétől.
A legfelső szintű legördülő menü kódja így nézhet ki:
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)) } }
Ez a CategoryDropdown fájl egy egyszerű osztály konstruktorral, amely inicializálja a legördülő menüben rendelkezésre álló kategóriaopciókat. Ez az a fájl, amelyet kezelne, ha lekérne egy listát a háttérből, vagy bármilyen rendezni szeretne, mielőtt a felhasználó meglátná az opciókat.
A feliratkozási módszer az, hogy az ezzel az osztállyal létrehozott minden szűrő hogyan kap frissítéseket a megfigyelő állapotáról.
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