4 Corak Reka Bentuk yang Perlu Anda Ketahui untuk Pembangunan Web: Pemerhati, Singleton, Strategi, dan Penghias

Adakah anda pernah menyertai pasukan di mana anda perlu memulakan projek dari awal? Itu biasanya berlaku di banyak syarikat permulaan dan syarikat kecil lain.

Terdapat begitu banyak bahasa pengaturcaraan, seni bina, dan keprihatinan lain sehingga sukar untuk mengetahui di mana hendak bermula. Di situlah corak reka bentuk masuk.

Corak reka bentuk seperti templat untuk projek anda. Ia menggunakan konvensyen tertentu dan anda boleh mengharapkan tingkah laku tertentu daripadanya. Corak-corak ini terdiri daripada banyak pengalaman pemaju sehingga mereka benar-benar seperti pelbagai amalan terbaik.

Dan anda dan pasukan anda dapat memutuskan kumpulan amalan terbaik mana yang paling berguna untuk projek anda. Berdasarkan corak reka bentuk yang anda pilih, anda semua akan mula mempunyai harapan untuk apa yang harus dilakukan kod dan kosa kata apa yang anda semua akan gunakan.

Corak reka bentuk pengaturcaraan dapat digunakan di semua bahasa pengaturcaraan dan dapat digunakan untuk menyesuaikan proyek apa pun kerana ia hanya memberikan garis besar penyelesaian.

Terdapat 23 corak rasmi dari buku Design Patterns - Elements of Reusable Object-Oriented Software , yang dianggap sebagai salah satu buku yang paling berpengaruh mengenai teori dan pengembangan perisian berorientasikan objek.

Dalam artikel ini, saya akan membahas empat corak reka bentuk tersebut hanya untuk memberi anda gambaran bagaimana beberapa corak dan kapan anda akan menggunakannya.

Corak Reka Bentuk Singleton

Corak singleton hanya membenarkan kelas atau objek untuk memiliki satu contoh dan ia menggunakan pemboleh ubah global untuk menyimpan contoh itu. Anda boleh menggunakan pemuatan malas untuk memastikan bahawa hanya ada satu contoh kelas kerana kelas hanya akan dibuat apabila anda memerlukannya.

Itu menghalang beberapa kejadian daripada aktif pada masa yang sama yang boleh menyebabkan pepijat pelik. Sebilangan besar masa ini dilaksanakan di konstruktor. Matlamat corak singleton biasanya adalah untuk mengatur keadaan global aplikasi.

Contoh single yang mungkin anda gunakan sepanjang masa adalah logger anda.

Sekiranya anda bekerja dengan beberapa kerangka depan seperti React atau Angular, anda tahu betapa sukarnya mengendalikan log yang berasal dari pelbagai komponen. Ini adalah contoh yang sangat baik bagi orang yang beraksi kerana anda tidak pernah menginginkan lebih daripada satu objek objek logger, terutamanya jika anda menggunakan sejenis alat pengesan ralat.

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

Sekarang anda tidak perlu bimbang kehilangan log dari beberapa kejadian kerana anda hanya mempunyai satu dalam projek anda. Oleh itu, apabila anda ingin mencatat makanan yang telah dipesan, anda boleh menggunakan contoh FoodLogger yang sama di pelbagai fail atau komponen.

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

Dengan corak singleton ini, anda tidak perlu risau hanya mendapatkan log dari fail aplikasi utama. Anda boleh mendapatkannya dari mana saja di pangkalan kod anda dan mereka semua akan pergi ke contoh yang sama dengan logger, yang bermaksud tidak ada log anda yang akan hilang kerana kejadian baru.

Corak Reka Bentuk Strategi

Strategi adalah corak seperti versi lanjutan dari pernyataan jika lain Pada dasarnya anda membuat antara muka untuk kaedah yang anda ada di kelas asas anda. Antaramuka ini kemudian digunakan untuk mencari pelaksanaan kaedah yang betul yang harus digunakan di kelas turunan. Pelaksanaan, dalam hal ini, akan diputuskan pada waktu berjalan berdasarkan klien.

Corak ini sangat berguna dalam situasi di mana anda memerlukan kaedah pilihan dan pilihan untuk kelas. Beberapa contoh kelas tersebut tidak memerlukan kaedah pilihan, dan itu menyebabkan masalah untuk penyelesaian warisan. Anda boleh menggunakan antara muka untuk kaedah pilihan, tetapi kemudian anda harus menulis pelaksanaannya setiap kali anda menggunakan kelas itu kerana tidak akan ada implementasi lalai.

Di situlah corak strategi menyelamatkan kita. Alih-alih klien mencari implementasi, ia mendelegasikan ke antara muka strategi dan strategi mencari pelaksanaan yang tepat. Salah satu kegunaan umum untuk ini adalah dengan sistem pemprosesan pembayaran.

Anda mungkin mempunyai keranjang belanja yang hanya memungkinkan pelanggan melapor keluar dengan kad kredit mereka, tetapi anda akan kehilangan pelanggan yang ingin menggunakan kaedah pembayaran lain.

Corak reka bentuk strategi membolehkan kita memisahkan kaedah pembayaran dari proses pembayaran yang bermaksud kita dapat menambah atau mengemas kini strategi tanpa mengubah kod apa pun di keranjang belanja atau proses pembayaran.

Berikut adalah contoh pelaksanaan corak strategi menggunakan contoh kaedah pembayaran.

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

Untuk melaksanakan strategi kaedah pembayaran, kami membuat satu kelas dengan beberapa kaedah statik. Setiap kaedah menggunakan parameter yang sama, customerInfo , dan parameter tersebut memiliki jenis customerInfoType yang ditentukan . (Hey all you dev TypeScript! ??) Perhatikan bahawa setiap kaedah mempunyai pelaksanaannya sendiri dan menggunakan nilai yang berbeza dari customerInfo .

Dengan corak strategi, anda juga dapat mengubah strategi yang digunakan secara dinamis pada waktu berjalan. Itu bermakna anda akan dapat mengubah strategi, atau implementasi metode, yang digunakan berdasarkan input pengguna atau lingkungan tempat aplikasi berjalan.

Anda juga boleh menetapkan pelaksanaan lalai dalam fail config.json sederhana seperti ini:

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

Setiap kali pelanggan mula melalui proses pembayaran di laman web anda, kaedah pembayaran lalai yang mereka hadapi adalah pelaksanaan PayPal yang berasal dari config.json . Ini dapat diperbaharui dengan mudah jika pelanggan memilih kaedah pembayaran yang lain.

Sekarang kami akan membuat fail untuk proses pembayaran kami.

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)

Ini Checkout kelas adalah di mana corak strategi yang mendapat untuk menunjuk-nunjuk. Kami mengimport beberapa fail sehingga kami mempunyai strategi kaedah pembayaran yang tersedia dan strategi lalai dari konfigurasi .

Kemudian kami membuat kelas dengan konstruktor dan nilai penggantian untuk strategi lalai sekiranya belum ada satu set dalam konfigurasi . Seterusnya kami memberikan nilai strategi kepada pemboleh ubah keadaan tempatan.

Kaedah penting yang perlu kita terapkan di kelas Checkout adalah kemampuan mengubah strategi pembayaran. Pelanggan mungkin mengubah kaedah pembayaran yang ingin mereka gunakan dan anda mesti dapat mengatasinya. Itulah kaedah kaedah changeStrategy .

Setelah anda melakukan pengekodan mewah dan mendapatkan semua input dari pelanggan, maka anda dapat mengemas kini strategi pembayaran dengan segera berdasarkan input mereka dan strategi ini secara dinamis menetapkan sebelum pembayaran dikirim untuk diproses.

Pada satu ketika anda mungkin perlu menambahkan lebih banyak kaedah pembayaran ke keranjang belanja anda dan yang perlu anda lakukan hanyalah menambahkannya ke kelas PaymentMethodStrategy . Ia akan tersedia dengan segera di mana sahaja kelas itu digunakan.

Corak reka bentuk strategi sangat berkesan ketika anda berhadapan dengan kaedah yang mempunyai banyak pelaksanaan. Mungkin terasa seperti anda menggunakan antara muka, tetapi anda tidak perlu menulis implementasi untuk kaedah tersebut setiap kali anda memanggilnya di kelas yang berbeza. Ini memberi anda lebih banyak fleksibiliti daripada antara muka.

Corak Reka Bentuk Pemerhati

Sekiranya anda pernah menggunakan corak MVC, anda sudah menggunakan corak rekaan pemerhati. Bahagian Model adalah seperti subjek dan bahagian View adalah seperti pemerhati subjek itu. Subjek anda menyimpan semua data dan keadaan data tersebut. Kemudian anda mempunyai pemerhati, seperti komponen yang berbeza, yang akan memperoleh data dari subjek apabila data telah dikemas kini.

Matlamat corak reka bentuk pemerhati adalah untuk mewujudkan hubungan satu-ke-banyak antara subjek dan semua pemerhati yang menunggu data supaya mereka dapat dikemas kini. Jadi bila-bila masa keadaan subjek berubah, semua pemerhati akan diberitahu dan dikemas kini dengan serta-merta.

Beberapa contoh kapan anda akan menggunakan corak ini termasuk: mengirim pemberitahuan pengguna, mengemas kini, menyaring, dan menangani pelanggan.

Katakan anda mempunyai aplikasi satu halaman yang mempunyai tiga senarai dropdown ciri yang bergantung pada pemilihan kategori dari dropdown level yang lebih tinggi. Perkara ini biasa berlaku di banyak laman membeli-belah, seperti Home Depot. Anda mempunyai sekumpulan penapis di halaman yang bergantung pada nilai penapis tahap atas.

Kod untuk menu lungsur bawah mungkin kelihatan seperti ini:

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

Ini CategoryDropdown fail adalah kelas yang mudah dengan pembina yang initializes pilihan kategori yang kita perlu ada dalam jatuh turun. Ini adalah fail yang anda kendalikan untuk mendapatkan senarai dari bahagian belakang atau jenis penyusunan yang anda mahu lakukan sebelum pengguna melihat pilihannya.

The melanggan kaedah adalah bagaimana setiap penapis dicipta dengan kelas ini akan menerima maklumat terkini mengenai keadaan pemerhati.

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