4 Design Patterns You Should Know for Web Development: Observer, Singleton, Strategy, and Decorator

4 Design Patterns You Should Know for Web Development: Observer, Singleton, Strategy, and Decorator

When working on a team, have you ever had to begin something from scratch? That’s the case with many new businesses and smaller organisations.

The sheer variety of programming languages, computer architectures, and other factors can make it hard to know where to begin. Design patterns are used for this purpose.

A design pattern serves as a starting point for new designs. It follows established norms, and its behaviour is predictable. Essentially collections of best practises, these patterns are the result of the combined wisdom of many developers.

You and your team have the freedom to choose the best practises that will work best for your endeavour. As a team, you can begin to form shared expectations for the behaviour of the code and the language used going forward based on the design pattern you settle on.

Because they provide only a high-level framework, programming design patterns are portable across languages and can be adapted to any kind of project.

The book Design Patterns – Elements of Reusable Object-Oriented Software contains 23 official patterns and is widely regarded as a seminal work in the field of object-oriented theory and software design.

Four of these design patterns will be discussed in this article to give you an idea of what they are and when you might want to use them.

The Singleton Design Pattern

The singleton pattern restricts classes and objects to having exactly one instance, which is stored in a single global variable. Lazy loading allows you to guarantee that there is only one instance of the class by delaying the creation of the class until it is actually needed.

Having that safeguard in place eliminates the possibility of strange bugs being caused by multiple instances running at once. It is common practise to add this functionality in the function Object() { [native code] }. The singleton pattern is most often used to control the application’s global state.

Your logger is a singleton that is used frequently by your application.

You’ll know how difficult it is to manage logs from various components if you’ve ever worked with a front-end framework like React or Angular. If you’re using an error-tracking tool, you should only ever have a single instance of the logger object, so this is a perfect use case for singletons.

Since there is only one instance in your project, you no longer have to worry about losing logs from other instances. That being said, if you need to keep track of the meals that have been ordered, you can do so by sharing a single instance of the FoodLogger across various files and/or modules.

If you follow this singleton pattern in your app’s main file, you won’t have to worry about where the logs are being stored. They can be retrieved from anywhere in your codebase and will always be sent to the same logger instance, ensuring that none of your logs will be lost as new instances are created.

The Strategy Design Pattern

The pattern strategy is similar to a more complex “if else” statement. Essentially, it’s a way to create an interface for a method in your base class. The appropriate implementation of a given method in a derived class can then be located by using this interface. In this case, the implementation will be chosen dynamically based on the client.

When a class has both mandatory and discretionary methods, this pattern comes in very handy. The problem with inheritance solutions is that some of the class’s instances won’t need the extra features. Interfaces could be used for the ad hoc methods, but then the implementation would need to be written each time the class was used.

The strategy pattern comes in handy here. The client does not actively seek out an implementation, but instead defers that responsibility to a strategy interface, which is responsible for tracking down the appropriate implementation. This is frequently employed in online banking and other forms of electronic payment.

If your shopping cart only accepts credit card payments, you risk losing customers who prefer to use alternative payment methods.

We can add or update strategies without modifying any code in the shopping cart or checkout process because the strategy design pattern allows us to decouple the payment methods from the checkout process.

Take the example of a payment method to see how a strategy pattern can be put into practise.

We created a single class with multiple static methods to put into action our strategy for handling monetary transactions. All of these procedures share a single common parameter called customerInfo, which is of type customerInfoType. (Say what, all you TypeScript programmers!) It’s important to remember that each method has its own implementation, and that it draws on the customerInfo object in slightly different ways.

The strategy pattern also allows you to dynamically switch between different strategies while the application is running. That means you can adjust the tactic, or method implementation, being used in response to data from the app’s users or its surrounding environment.

The config.json file specifies that PayPal should be used as the default payment method whenever a customer initiates a purchase on your site. If the buyer decides to use a different payment option, this can be changed easily.

The strategy pattern is given its chance to shine in the Checkout class. We bring in a couple of files to get the config’s default strategy and the payment method strategies.

Then, if no default strategy has been specified in the configuration, we create the class with the function Object() { [native code] } and a default value. We then store the value of the strategy in a global state variable.

Adjusting the payment method is a crucial feature that needs to be added to our Checkout class. You must be flexible in case a customer decides to switch the payment method they originally selected. The changeStrategy procedure is intended for such purposes.

Once you’ve done some fancy coding and gathered all the information you need from a customer, you can immediately adjust the payment strategy to reflect their preferences, and it will do so before sending the payment to be processed.

The PaymentMethodStrategy class is where you should add any new payment options for your store’s shopping cart. Anywhere that class is used, it will be accessible immediately.

When working with methods that can take different forms, the strategy design pattern becomes particularly useful. You can use the method in multiple classes without having to reimplement it each time, making it feel more like an interface. It offers greater customization than traditional interfaces.

The Observer Design Pattern

You have already used the observer design pattern if you have implemented the Model-View-Controller pattern. The Model component acts as a subject, while the View component takes on the role of an observer. Both the data and its current status are under your subject’s control. Afterward, there are observers, which can be thought of as separate parts, that will collect the data from the subject once it has been updated.

One of the main tenets of the observer design pattern is to establish a one-to-many connection between the thing being observed and all the observers who are keeping an eye out for new information. That means that all of the observers will be immediately informed of any changes to the subject’s status.

Notifications to users, updates, filters, and managing subscribers are all scenarios where this pattern would be useful.

Let’s pretend you’re working with a single-page application that relies on the user’s choice of a category from a drop-down menu to determine which of three feature dropdown lists appears. The Home Depot and other shopping websites are typical of this. A number of the filters on the page are reliant on the setting of a single global filter.

This file, called CategoryDropdown, is a simple class with a function Object() { [native code] } that sets the initial values for the dropdown’s categories. Using this file, you can manage the process of fetching a list from the backend or performing any sorting you’d like before the options are presented to the user.

Each filter that is built with this class will subscribe to updates on the observer’s state.

When an observer’s state changes, we can let all of its subscribers know using the onChange method. Simply iterating over all of the subscribers, we update their method passing in the selectedCategory.

Every possible dropdown we could use on a page is represented in this FilterDropdown file, which is just another simple class. A filterType must be provided whenever a new instance of this class is created. This could be used to retrieve the list of items via targeted API calls.

What can be done with the new category after it has been sent from the observer is implemented in the update method.

This document reveals that there are three select boxes which subscribe to the category select box observable. Then, we subscribe the observer to each of the menu items that collapsed. Each subscriber’s drop-down menus will be instantly refreshed whenever the observer’s category is changed because that value will be broadcast to all of them.

The Decorator Design Pattern

It’s not hard to put the decorator design pattern to use. The objects you create from a base class will already have access to the class’s predefined set of methods and properties. Let’s assume, however, that there are some class instances that call for special functionality that isn’t provided by the base class.

You can modify the base class to include these new methods and properties, but doing so may cause problems for your other instances. Subclasses can be created to store any data or functionality that is too specialised to be included in the base class.

While both of these methods will ultimately be successful, they are both cumbersome and time-consuming. The decorator pattern is used for this purpose. To avoid cluttering your code with unnecessary features for enhancing a single instance of an object, you can append them directly to the instance itself.

Applying the decorator pattern, for example, allows you to add a new property to a single object instance (such as a price property) without affecting any other instances of the class object.

Does anyone here ever order food from a restaurant online? Then the decorator pattern is something you’ve seen before. The website isn’t always accommodating users’ requests for customizations, such as extra toppings on sandwiches.

The decorator pattern is implemented in this sandwich class. The rules for making a standard sandwich are defined in the Sandwich base class. If customers want to splurge on fancier fillings for their sandwiches, all you have to do is adjust the price and the ingredients.

You only wanted to modify the DeluxeSandwich so that the price could be raised and the sandwich type updated without affecting the ordering process. Since the quality of the ingredients in an ExquisiteSandwich is so much higher, you may need to use a different ordering method.

Using the decorator pattern, you can make changes to the base class on the fly without affecting any other classes. Unlike with interfaces, you are not responsible for implementing any unknown functions, and you are not required to include any unused properties in your classes.

We’ll now look at a sample order for a sandwich where this class is instantiated.

Final Thoughts

Until recently, my impression of design patterns was that they were some sort of outlandishly abstract set of rules for creating software. When I really started paying attention, I realised how often I actually make use of them.

Some of the patterns I discussed appear in so many places it’s incredible. Theory is all they are in the end. As developers, it is our responsibility to apply this theory in ways that reduce the complexity of our applications.

When working on projects, have you ever employed the use of any of the alternative design patterns? Organizations typically settle on a single design pattern for all future projects.

More To Explore