Software Design Patterns — Pt 1
A Tutoring Titan Once Said ….
“Software design patterns are like Easter eggs hidden in code — you have to be a savvy code hunter to discover them, and once you do, it’s like finding a sweet surprise that makes your programming skills egg-ceptional!”
Have you ever found yourself creating code that just seems to flow naturally, almost as if it’s writing itself? You might be surprised to learn that you’re probably already using software design patterns without even realizing it. These patterns are the unsung heroes of programming that quietly guide us towards best practices and efficient solutions. So, let’s take a closer look at these hidden gems and uncover the mystery behind how they’re already working behind the scenes to make your code better.
To put in simpler terms
“Software design patterns are reusable solutions to common programming problems. They’re like blueprints for writing code that can be used across multiple projects. By following design patterns, programmers can create code that is more efficient, easier to maintain, and less prone to bugs. Think of it like a recipe for baking a cake. Just as a recipe provides step-by-step instructions for creating a delicious dessert, a design pattern provides a framework for writing code that is both effective and efficient. By using software design patterns, programmers can save time and produce better quality code.”
There are numerous software design patterns in the “Realm of Software Engineering.” , lets conquer them one by one.
Strategy Design Pattern:
“Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.”
Lets take a real world example to explain it.
Conside you own a restaurant and you have recently started out.
Your Menu contains a fixed set of Items , you have hired a chef who specialises in cooking those Items in the Menu
Few weeks later , your restaurant gains popularity , and you want to expand and offer more items. This creates an overhead on the existing chef due to the following reasons .
- He will be cooking items he is not really a master of which might cause the items served to be not up to the mark
- Each time a new item is added to the Menu , the existing chef needs to learn that and cook it at the same grade he cooks his best meals.
- Certain customers will have certain dietary restrictions and they will require a completely different way of cooking , making the existing cook lose valuable time and effort of learning something from scratch and delivering.
You know that this is not going to scale and the Chef will get frustrated and might leave the job.
As a business owner what will you do ? What I would do is this .
Look at my current Menu, Categorize them into categories such as .
1. Italian Items
2. Chinese Items
3. Bakery Items
4. Custom Made Food Items
Hire a chef specialised for each category.
What this does is offloads the tasks from existing chef and actually delivering quality items.
Lets take this example and see how this is a weirdly simple example and implementation of “Strategy Design Pattern”.
Lets say a customer places an order for Dish-1 belonging to the “Italian Items “ Category, the restaurant will dynamically assign it to the Italian Chef.
Similarly for Chinese Food Items the Restaurant will dynamically assign it to the Chinese Chef.
Lets Go a bit more technically and break down the entities of a Strategy Design Pattern , and how they make sense in our real world example.
Lets Go Back to the original definition of Strategy Design Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.”
Algorithms here are cooking methods and steps
Strategy : Chefs
Clients : Customers
Context : Restaurant
Translation to Code (After Using Strategy Design Pattern)
// Define the Chef interface
class Chef {
prepareOrder(order) {
throw new Error('prepareOrder() must be implemented by subclass')
}
}
// Define the ItalianChef class
class ItalianChef extends Chef {
prepareOrder(order) {
console.log(`Preparing ${order.dishName} using Italian cuisine`)
// ...
}
}
// Define the ChineseChef class
class ChineseChef extends Chef {
prepareOrder(order) {
console.log(`Preparing ${order.dishName} using Chinese cuisine`)
// ...
}
}
// Define the Dish class
class Dish {
constructor(name, cuisine) {
this.name = name
this.cuisine = cuisine
}
}
// Define the Restaurant class
class Restaurant {
constructor() {
this.chefs = {
'Italian': new ItalianChef(),
'Chinese': new ChineseChef()
}
}
prepareOrder(order) {
const { dishName, cuisine } = order
const chef = this.chefs[cuisine]
if (!chef) {
throw new Error(`Chef not found for cuisine: ${cuisine}`)
}
const dish = new Dish(dishName, cuisine)
chef.prepareOrder(dish)
}
}
// Usage example
const restaurant = new Restaurant()
restaurant.prepareOrder({ dishName: 'Spaghetti', cuisine: 'Italian' })
restaurant.prepareOrder({ dishName: 'Kung Pao Chicken', cuisine: 'Chinese' })
This code defines the Strategy design pattern by separating the behavior of the
Chef
class into separate subclasses that implement the same method,prepareOrder()
, in different ways based on the cuisine.The
Chef
class defines the interface for theprepareOrder()
method, which is then implemented by its subclasses,ItalianChef
andChineseChef
, in different ways based on their respective cuisines. TheRestaurant
class uses a dictionary ofChef
instances to determine which chef to use based on the cuisine of the order, and then delegates the preparation of the order to that chef's implementation of theprepareOrder()
method.This design pattern allows for the behavior of the
Chef
class to be easily extended in the future by adding new subclasses that implement theprepareOrder()
method in new ways. It also allows for theRestaurant
class to be decoupled from the specific implementations of theprepareOrder()
method, which makes it more flexible and easier to maintain.
Translation to Code (Before Using Strategy Design Pattern)
Translation to Code (Before Using Strategy Design Pattern)
class Chef {
prepareOrder(order) {
// Chef performs a complex series of operations to prepare the dish
let dish = null;
if (order.cuisine === 'Italian') {
if (order.dishName === 'Spaghetti') {
dish = new Dish(order.dishName, 'Italian');
console.log(`Preparing ${dish.name} using Italian cuisine`);
// Perform Italian cooking steps
} else if (order.dishName === 'Pizza') {
dish = new Dish(order.dishName, 'Italian');
console.log(`Preparing ${dish.name} using Italian cuisine`);
// Perform Italian cooking steps
}
} else if (order.cuisine === 'Chinese') {
if (order.dishName === 'Kung Pao Chicken') {
dish = new Dish(order.dishName, 'Chinese');
console.log(`Preparing ${dish.name} using Chinese cuisine`);
// Perform Chinese cooking steps
} else if (order.dishName === 'Fried Rice') {
dish = new Dish(order.dishName, 'Chinese');
console.log(`Preparing ${dish.name} using Chinese cuisine`);
// Perform Chinese cooking steps
}
}
if (!dish) {
throw new Error(`Dish not found: ${order.dishName} (${order.cuisine})`);
}
// Chef returns the prepared dish
return dish;
}
}
class Dish {
constructor(name, cuisine) {
this.name = name
this.cuisine = cuisine
}
}
class Restaurant {
constructor() {
this.chefs = {
'Italian': new Chef(),
'Chinese': new Chef()
}
}
prepareOrder(order) {
const chef = this.chefs[order.cuisine]
if (!chef) {
throw new Error(`Chef not found for cuisine: ${order.cuisine}`)
}
const dish = chef.prepareOrder(order)
// Restaurant returns the prepared dish
return dish;
}
}
// Usage example
const restaurant = new Restaurant()
const spaghetti = restaurant.prepareOrder({ dishName: 'Spaghetti', cuisine: 'Italian' })
const kungPaoChicken = restaurant.prepareOrder({ dishName: 'Kung Pao Chicken', cuisine: 'Chinese' })
As you can see, this implementation is much more complex and tightly coupled than the Strategy pattern version. Each chef now has to handle all possible dishes for their cuisine, which can quickly become unmanageable as the number of dishes grows. This approach also requires the restaurant to keep track of multiple instances of the Chef class, instead of using a single instance per cuisine like in the Strategy pattern implementation. Overall, this implementation is less modular, less scalable, and harder to maintain than the Strategy pattern version.
As you can see, this implementation is much more complex and tightly coupled than the Strategy pattern version. Each chef now has to handle all possible dishes for their cuisine, which can quickly become unmanageable as the number of dishes grows. This approach also requires the restaurant to keep track of multiple instances of the Chef class, instead of using a single instance per cuisine like in the Strategy pattern implementation. Overall, this implementation is less modular, less scalable, and harder to maintain than the Strategy pattern version.
So, that’s the beauty of the Strategy design pattern: it’s like having a toolbox full of specialized tools, each designed to tackle a specific job. You can pick and choose the right tool for the task at hand, without having to worry about how it works under the hood. It’s like magic, but for code! With the Strategy pattern, you can easily swap out one algorithm for another, without having to rewrite your entire program. It’s the ultimate in flexibility and modularity, and a great way to future-proof your code. So, the next time you’re facing a complex problem, remember the Strategy pattern: your secret weapon in the battle against code complexity
With that lets take a “strategic” timeout, till we go on another adventure into the realm of Software Design Patterns