4 أنماط تصميم يجب أن تعرفها لتطوير الويب: المراقب ، الفردي ، الإستراتيجية ، والديكور

هل سبق لك أن انضممت إلى فريق حيث تحتاج إلى بدء مشروع من الصفر؟ هذا هو الحال عادةً في العديد من الشركات الناشئة والشركات الصغيرة الأخرى.

هناك العديد من لغات البرمجة المختلفة ، والبنى ، والمخاوف الأخرى التي قد يكون من الصعب معرفة من أين تبدأ. هذا هو المكان الذي تأتي فيه أنماط التصميم.

يشبه نمط التصميم نموذجًا لمشروعك. إنه يستخدم اصطلاحات معينة ويمكنك توقع نوع معين من السلوك منه. تتكون هذه الأنماط من تجارب العديد من المطورين ، لذا فهي تشبه مجموعات مختلفة من أفضل الممارسات.

وعليك أنت وفريقك تحديد مجموعة أفضل الممارسات الأكثر فائدة لمشروعك. بناءً على نمط التصميم الذي تختاره ، ستبدأ جميعًا في توقع ما يجب أن تفعله الشفرة والمفردات التي ستستخدمها جميعًا.

يمكن استخدام أنماط تصميم البرمجة في جميع لغات البرمجة ويمكن استخدامها لتناسب أي مشروع لأنها تمنحك فقط مخططًا عامًا للحل.

هناك 23 نمطًا رسميًا من كتاب Design Patterns - Elements of Reusable Object-Oriented Software ، والذي يُعد أحد أكثر الكتب تأثيرًا في نظرية الكائنات الموجهة وتطوير البرمجيات.

في هذه المقالة ، سأقوم بتغطية أربعة من أنماط التصميم هذه فقط لأعطيك فكرة عن ماهية بعض الأنماط ومتى ستستخدمها.

نمط تصميم Singleton

يسمح النمط الفردي فقط للفئة أو الكائن أن يكون له مثيل واحد ويستخدم متغيرًا شاملاً لتخزين هذا المثال. يمكنك استخدام التحميل البطيء للتأكد من وجود مثيل واحد فقط للفصل لأنه سينشئ الفصل عند الحاجة إليه فقط.

يمنع ذلك مثيلات متعددة من أن تكون نشطة في نفس الوقت مما قد يتسبب في أخطاء غريبة. في معظم الأحيان يتم تنفيذ هذا في المنشئ. الهدف من النمط الفردي هو عادةً تنظيم الحالة العالمية للتطبيق.

مثال على المفرد الذي ربما تستخدمه طوال الوقت هو المسجل الخاص بك.

إذا كنت تعمل مع بعض الأطر الأمامية مثل React أو Angular ، فأنت تعرف كل شيء عن مدى صعوبة التعامل مع السجلات القادمة من مكونات متعددة. هذا مثال رائع على العمل الفردي لأنك لا تريد أبدًا أكثر من مثيل واحد لكائن المسجل ، خاصة إذا كنت تستخدم نوعًا من أدوات تتبع الأخطاء.

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

الآن لا داعي للقلق بشأن فقد السجلات من حالات متعددة لأن لديك واحدًا فقط في مشروعك. لذلك عندما تريد تسجيل الطعام الذي تم طلبه ، يمكنك استخدام نفس مثيل FoodLogger عبر ملفات أو مكونات متعددة.

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

مع وجود هذا النمط الفردي في مكانه ، لا داعي للقلق بشأن مجرد الحصول على السجلات من ملف التطبيق الرئيسي. يمكنك الحصول عليها من أي مكان في قاعدة الشفرة الخاصة بك وسوف ينتقلون جميعًا إلى نفس مثيل المسجل بالضبط ، مما يعني أنه لن يتم فقد أي من سجلاتك بسبب الحالات الجديدة.

نمط تصميم الإستراتيجية

الاستراتيجية هي النمط مثل نسخة متقدمة من بيان if else. إنه في الأساس المكان الذي تنشئ فيه واجهة لطريقة لديك في صنفك الأساسي. يتم استخدام هذه الواجهة بعد ذلك للعثور على التطبيق الصحيح لتلك الطريقة التي يجب استخدامها في فئة مشتقة. سيتم تحديد التنفيذ ، في هذه الحالة ، في وقت التشغيل بناءً على العميل.

هذا النمط مفيد بشكل لا يصدق في المواقف التي تطلب فيها طرقًا اختيارية للفصل. لن تحتاج بعض أمثلة هذه الفئة إلى الأساليب الاختيارية ، وهذا يسبب مشكلة في حلول الوراثة. يمكنك استخدام واجهات للطرق الاختيارية ، ولكن بعد ذلك سيتعين عليك كتابة التنفيذ في كل مرة تستخدم فيها هذه الفئة نظرًا لعدم وجود تطبيق افتراضي.

هذا هو المكان الذي ينقذنا فيه نمط الإستراتيجية. بدلاً من أن يبحث العميل عن تطبيق ، يقوم بتفويض واجهة إستراتيجية وتجد الإستراتيجية التنفيذ الصحيح. أحد الاستخدامات الشائعة لهذا هو مع أنظمة معالجة الدفع.

هل يمكن أن يكون لديك عربة التسوق التي تسمح فقط للعملاء سحب بطاقات الائتمان الخاصة بهم، ولكنك لن تخسر العملاء الذين يرغبون في استخدام وسائل الدفع الأخرى.

يتيح لنا نمط تصميم الإستراتيجية فصل طرق الدفع عن عملية السداد مما يعني أنه يمكننا إضافة أو تحديث الاستراتيجيات دون تغيير أي رمز في عربة التسوق أو عملية الدفع.

فيما يلي مثال على تنفيذ نمط الإستراتيجية باستخدام مثال طريقة الدفع.

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

لتنفيذ إستراتيجية طريقة الدفع الخاصة بنا ، قمنا بإنشاء فئة واحدة بطرق ثابتة متعددة. تأخذ كل طريقة المعلمة نفسها ، customerInfo ، وهذه المعلمة لها نوع محدد من customerInfoType . (مرحبًا بكم جميع مطوري TypeScript! ؟؟) لاحظ أن كل طريقة لها تنفيذها الخاص وتستخدم قيمًا مختلفة من معلومات العميل .

باستخدام نمط الإستراتيجية ، يمكنك أيضًا تغيير الاستراتيجية المستخدمة في وقت التشغيل ديناميكيًا. هذا يعني أنك ستتمكن من تغيير الاستراتيجية ، أو تنفيذ الطريقة ، التي يتم استخدامها بناءً على إدخال المستخدم أو البيئة التي يعمل بها التطبيق.

يمكنك أيضًا تعيين تنفيذ افتراضي في ملف config.json بسيط مثل هذا:

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

عندما يبدأ العميل في إجراء عملية الدفع على موقع الويب الخاص بك ، فإن طريقة الدفع الافتراضية التي يواجهها ستكون تطبيق PayPal الذي يأتي من config.json . يمكن تحديث هذا بسهولة إذا اختار العميل طريقة دفع مختلفة.

الآن سنقوم بإنشاء ملف لعملية الخروج الخاصة بنا.

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)

فئة Checkout هذه هي المكان الذي يظهر فيه نمط الإستراتيجية. نقوم باستيراد ملفين حتى تتوفر لدينا استراتيجيات طريقة الدفع والاستراتيجية الافتراضية من ملف التكوين .

ثم نقوم بإنشاء الفئة باستخدام المُنشئ وقيمة احتياطية للاستراتيجية الافتراضية في حالة عدم وجود مجموعة واحدة في config . بعد ذلك نقوم بتعيين قيمة الإستراتيجية لمتغير دولة محلي.

إحدى الطرق المهمة التي نحتاج إلى تنفيذها في صفنا في Checkout هي القدرة على تغيير استراتيجية الدفع. قد يغير العميل طريقة الدفع التي يريد استخدامها وستحتاج إلى أن تكون قادرًا على التعامل مع ذلك. هذا هو سبب تغيير طريقة الإستراتيجية .

بعد الانتهاء من بعض الترميز الرائع والحصول على جميع المدخلات من العميل ، يمكنك عندئذٍ تحديث استراتيجية الدفع فورًا بناءً على مدخلاتهم وتعيين الإستراتيجية ديناميكيًا قبل إرسال الدفعة للمعالجة.

في مرحلة ما ، قد تحتاج إلى إضافة المزيد من طرق الدفع إلى عربة التسوق الخاصة بك وكل ما عليك فعله هو إضافتها إلى فئة PaymentMethodStrategy . سيكون متاحًا على الفور في أي مكان يتم فيه استخدام هذا الفصل.

يعد نمط تصميم الإستراتيجية نموذجًا قويًا عندما تتعامل مع طرق لها تطبيقات متعددة. قد تشعر أنك تستخدم واجهة ، لكن ليس عليك كتابة تنفيذ للطريقة في كل مرة تسميها في فصل دراسي مختلف. يمنحك مرونة أكثر من الواجهات.

نمط تصميم المراقب

إذا سبق لك استخدام نمط MVC ، فقد استخدمت بالفعل نمط تصميم المراقب. جزء النموذج يشبه الموضوع وجزء العرض يشبه مراقب هذا الموضوع. موضوعك يحمل جميع البيانات وحالة تلك البيانات. ثم لديك مراقبون ، مثل المكونات المختلفة ، سيحصلون على تلك البيانات من الموضوع عند تحديث البيانات.

الهدف من نمط تصميم المراقب هو إنشاء علاقة رأس بأطراف بين الموضوع وجميع المراقبين الذين ينتظرون البيانات حتى يمكن تحديثها. لذلك في أي وقت تتغير حالة الموضوع ، سيتم إخطار جميع المراقبين وتحديثهم على الفور.

تتضمن بعض الأمثلة على وقت استخدام هذا النمط: إرسال إشعارات المستخدم والتحديث والفلاتر والتعامل مع المشتركين.

لنفترض أن لديك تطبيق صفحة واحدة يحتوي على ثلاث قوائم منسدلة للميزات تعتمد على اختيار فئة من قائمة منسدلة ذات مستوى أعلى. هذا شائع في العديد من مواقع التسوق ، مثل Home Depot. لديك مجموعة من المرشحات على الصفحة تعتمد على قيمة مرشح المستوى الأعلى.

قد يبدو رمز القائمة المنسدلة ذات المستوى الأعلى كما يلي:

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

إن ملف CategoryDropdown هذا عبارة عن فئة بسيطة مع مُنشئ يقوم بتهيئة خيارات الفئة المتوفرة لدينا في القائمة المنسدلة. هذا هو الملف الذي ستتعامل معه لاسترداد قائمة من النهاية الخلفية أو أي نوع من الفرز تريد القيام به قبل أن يرى المستخدم الخيارات.

و الاشتراك الأسلوب هو كيف أن كل مرشح تم إنشاؤها باستخدام هذه الفئة سوف تلقي التحديثات عن حالة المراقب.

و عند_التغيير الأسلوب هو كيف وجهنا إخطار لجميع المشتركين أن تغيير الدولة قد حدث في المراقب أنهم يشاهدون. نحن فقط نمرّر عبر جميع المشتركين ونستدعي طريقة التحديث الخاصة بهم بالفئة المحددة .

قد يبدو رمز المرشحات الأخرى كما يلي:

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

يعد ملف FilterDropdown هذا فئة بسيطة أخرى تمثل جميع القوائم المنسدلة المحتملة التي قد نستخدمها في الصفحة. عندما يتم إنشاء مثيل جديد من هذه الفئة ، فإنه يحتاج إلى تمرير نوع filterType. يمكن استخدام هذا لإجراء مكالمات API محددة للحصول على قائمة العناصر.

ل تحديث طريقة هو تنفيذ ما يمكنك القيام به مع الفئة الجديدة مرة واحدة تم إرسالها من المراقب.

الآن سنلقي نظرة على معنى استخدام هذه الملفات بنمط المراقب:

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)

ما يوضحه لنا هذا الملف هو أن لدينا 3 قوائم منسدلة للمشتركين في القائمة المنسدلة للفئة يمكن ملاحظتها. ثم نقوم بالاشتراك في كل من هذه القوائم المنسدلة للمراقب. عندما يتم تحديث فئة المراقب ، سيرسل القيمة إلى كل مشترك والذي سيقوم بتحديث القوائم المنسدلة الفردية على الفور.

نمط تصميم الديكور

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.

هل استخدمت أيًا من أنماط التصميم الأخرى لمشاريعك؟ عادةً ما تختار معظم الأماكن نمط تصميم لمشاريعها وتلتزم به ، لذا أود أن أسمع منك جميعًا حول ما تستخدمه.

شكرا للقراءة. يجب أن تتابعني على Twitter لأنني عادةً ما أنشر أشياء مفيدة / مسلية:FlippedCoding