Design patterns are reusable solutions to common software design problems. They provide a structured approach to solving problems and can help developers create more robust and maintainable software systems. Design patterns can be applied at various levels of software development, including architecture, design, and coding. There are many types of design patterns, including creational patterns (such as Singleton, Factory, and Builder), structural patterns (such as Adapter, Facade, and Composite), and behavioral patterns (such as Observer, Strategy, and Command). By using design patterns, developers can write code that is easier to read, understand, and maintain, and that can be reused in future projects.
Table of Contents
List of Few Software Design Patterns
Here is a list of some commonly used software design patterns, along with an example and explanation of each:
Singleton Pattern
The Singleton pattern restricts the instantiation of a class to a single object. This pattern is useful when we want to ensure that only one instance of a class is created and used throughout the application. For example, a logger class can be implemented as a Singleton, to ensure that only one instance is used to log messages throughout the application.
Singleton design pattern is a creational pattern that ensures that only one instance of a class is created and provides a global point of access to that instance.
Example: Suppose we have a logger class and we want only one instance of that class to be created throughout the application. We can use the singleton pattern to ensure that only one instance is created.
Here’s an example implementation of the singleton pattern in Java:
public class Logger { private static Logger instance; private Logger() { // private constructor to prevent instantiation } public static Logger getInstance() { if (instance == null) { instance = new Logger(); } return instance; } public void log(String message) { // log the message } }
In this example, the Logger class has a private constructor so it cannot be instantiated from outside the class. The getInstance() method is used to get the singleton instance of the Logger class. The first time this method is called, it creates a new instance of the Logger class. Subsequent calls to getInstance() will return the existing instance.
Factory Pattern
The Factory pattern provides a way to create objects without exposing the creation logic to the client. It defines an interface for creating objects, but allows subclasses to decide which class to instantiate.
Factory design pattern is a creational pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
Example: Suppose we have a website that sells different types of pizzas, and we want to create a Pizza class that can create different types of pizzas such as Margherita, Pepperoni, and Veggie. We can use the factory pattern to create an interface for creating different types of pizzas.
Here’s an example implementation of the factory pattern in Java:
public abstract class Pizza { protected String name; protected String dough; protected String sauce; protected List<String> toppings; public void prepare() { System.out.println("Preparing " + name); System.out.println("Tossing " + dough + " dough"); System.out.println("Adding " + sauce + " sauce"); System.out.println("Adding toppings:"); for (String topping : toppings) { System.out.println(" " + topping); } } public void bake() { System.out.println("Baking for 25 minutes at 350 degrees"); } public void cut() { System.out.println("Cutting the pizza into diagonal slices"); } public void box() { System.out.println("Placing pizza in official PizzaStore box"); } public String getName() { return name; } } public class MargheritaPizza extends Pizza { public MargheritaPizza() { name = "Margherita Pizza"; dough = "Thin Crust"; sauce = "Tomato Sauce"; toppings = Arrays.asList("Fresh Mozzarella", "Basil"); } } public class PepperoniPizza extends Pizza { public PepperoniPizza() { name = "Pepperoni Pizza"; dough = "Thick Crust"; sauce = "Tomato Sauce"; toppings = Arrays.asList("Pepperoni", "Cheese"); } } public class PizzaFactory { public Pizza createPizza(String type) { if (type.equals("Margherita")) { return new MargheritaPizza(); } else if (type.equals("Pepperoni")) { return new PepperoniPizza(); } else { throw new IllegalArgumentException("Invalid pizza type: " + type); } } }
Decorator Pattern
The decorator design pattern is used to add behavior to an individual object dynamically, without affecting other objects of the same class. One example of this pattern could be a coffee shop where customers can choose to add various toppings or flavors to their coffee, such as whipped cream or caramel syrup. These toppings can be added in any order and combination, without changing the underlying coffee object. In this case, the coffee object is the base component, while the various toppings are decorators that add additional behavior to the coffee object.
# Base component class Coffee: def __init__(self): self.description = "Coffee" def get_cost(self): return 1.00 def get_description(self): return self.description # Decorator component class CoffeeDecorator(Coffee): def __init__(self, coffee): self.coffee = coffee def get_cost(self): return self.coffee.get_cost() def get_description(self): return self.coffee.get_description() # Concrete decorator class Whip(CoffeeDecorator): def __init__(self, coffee): super().__init__(coffee) self.description = "Whipped cream" def get_cost(self): return self.coffee.get_cost() + 0.50 def get_description(self): return self.coffee.get_description() + ", " + self.description # Concrete decorator class Caramel(CoffeeDecorator): def __init__(self, coffee): super().__init__(coffee) self.description = "Caramel syrup" def get_cost(self): return self.coffee.get_cost() + 0.75 def get_description(self): return self.coffee.get_description() + ", " + self.description # Create a base coffee object coffee = Coffee() print(coffee.get_description(), "$" + str(coffee.get_cost())) # Add a whipped cream topping coffee = Whip(coffee) print(coffee.get_description(), "$" + str(coffee.get_cost())) # Add a caramel syrup topping coffee = Caramel(coffee) print(coffee.get_description(), "$" + str(coffee.get_cost()))
In this example, we have a Coffee
class as the base component and a CoffeeDecorator
class as the decorator component. The Whip
and Caramel
classes are concrete decorators that add additional behavior to the coffee object. The get_cost
and get_description
methods are overridden in each decorator to modify the cost and description of the coffee object as toppings are added. Finally, we create a base coffee object and add the Whip
and Caramel
decorators to it to create a final coffee object with multiple toppings.
Observer Pattern
The Observer pattern defines a one-to-many dependency between objects, where when one object changes state, all its dependents are notified and updated automatically. For example, a stock market application can use the Observer pattern to update the prices of stocks as they change throughout the day.
import java.util.ArrayList; import java.util.List; interface Observer { void update(); } class Subject { private List<Observer> observers = new ArrayList<>(); public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void notifyObservers() { for (Observer observer : observers) { observer.update(); } } } class ConcreteSubject extends Subject { private int state; public int getState() { return state; } public void setState(int state) { this.state = state; notifyObservers(); } } class ConcreteObserver implements Observer { private ConcreteSubject subject; public ConcreteObserver(ConcreteSubject subject) { this.subject = subject; this.subject.attach(this); } @Override public void update() { System.out.println("Subject state changed: " + subject.getState()); } } public class ObserverExample { public static void main(String[] args) { ConcreteSubject subject = new ConcreteSubject(); ConcreteObserver observer1 = new ConcreteObserver(subject); ConcreteObserver observer2 = new ConcreteObserver(subject); subject.setState(10); subject.detach(observer1); subject.setState(20); } }
In this example, the Observer pattern is used to notify multiple observers of changes to the state of a subject object. The Subject
class maintains a list of observers and provides methods for attaching, detaching, and notifying observers. The ConcreteSubject
class extends Subject
and defines a state
variable that can be changed. Whenever the state
variable changes, the setState()
method is called, which in turn calls the notifyObservers()
method to notify all observers of the change.
The Observer
interface defines the update()
method that is called by the subject when its state changes. The ConcreteObserver
class implements the Observer
interface and maintains a reference to the subject it is observing. When the update()
method is called, the observer outputs a message to the console indicating the subject’s state has changed.
In the main()
method, a ConcreteSubject
object is created along with two ConcreteObserver
objects. The observers are attached to the subject, and then the subject’s state is changed twice. The output to the console shows that both observers are notified of the changes to the subject’s state.
Adapter Pattern
The Adapter pattern allows objects with incompatible interfaces to work together. It involves creating an adapter that translates the interface of one object into another interface that the client expects. For example, an adapter can be used to connect an old printer with a new computer, by providing a bridge between their different interfaces.
class Target: """ The Target defines a domain-specific interface that Client uses to interact with the Adaptee. """ def request(self) -> str: return "Target: The default target's behavior." class Adaptee: """ The Adaptee contains some useful behavior, but its interface is incompatible with the existing client code. The Adaptee needs some adaptation before the client code can use it. """ def specific_request(self) -> str: return ".eetpadA eht fo roivaheb laicepS" class Adapter(Target): """ The Adapter makes the Adaptee's interface compatible with the Target's interface via inheritance. """ def __init__(self, adaptee: Adaptee): self._adaptee = adaptee def request(self) -> str: return f"Adapter: (TRANSLATED) {self._adaptee.specific_request()[::-1]}" if __name__ == "__main__": adaptee = Adaptee() adapter = Adapter(adaptee) print("Adaptee: ", adaptee.specific_request()) print("Adapter: ", adapter.request())
In this example, we have a Target
class with a request()
method, an Adaptee
class with a specific_request()
method, and an Adapter
class that inherits from Target
and contains an instance of Adaptee
.
The Adapter
class implements the request()
method by calling the specific_request()
method on its _adaptee
instance, and then translating the output to make it compatible with the Target
interface.
In the main
function, we create an instance of Adaptee
and an instance of Adapter
that wraps the Adaptee
instance. We then call the specific_request()
method on the Adaptee
instance and the request()
method on the Adapter
instance, and print out the results. The output should be:
Adaptee: .eetpadA eht fo roivaheb laicepS Adapter: Adapter: (TRANSLATED) Special behavior of the Adaptee.
Proxy Pattern
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. This pattern is useful when we want to control access to a resource, such as a network connection or a file, without exposing the underlying implementation details to the client. For example, a proxy can be used to control access to a remote web service, by providing a local interface that the client can use to interact with it.
from abc import ABC, abstractmethod # The subject interface defines common operations for both the RealSubject and # the Proxy. As long as the client works with RealSubject using this interface, # you'll be able to pass it a proxy instead of a real subject. class Subject(ABC): @abstractmethod def request(self) -> None: pass # The RealSubject contains some core business logic. Usually, RealSubjects are # capable of doing some useful work which may also be very slow or sensitive - # e.g. correcting input data. A Proxy can solve these issues without any changes # to the RealSubject's code. class RealSubject(Subject): def request(self) -> None: print("RealSubject: Handling request.") # The Proxy has an interface identical to the RealSubject. class Proxy(Subject): def __init__(self, real_subject: RealSubject) -> None: self._real_subject = real_subject def request(self) -> None: # The most common applications of the Proxy pattern are lazy loading, # caching, controlling access, logging, etc. A Proxy can perform one of # these things and then, depending on the result, pass the execution to # the same method in a linked RealSubject object. if self.check_access(): self._real_subject.request() self.log_access() def check_access(self) -> bool: print("Proxy: Checking access prior to firing a real request.") return True def log_access(self) -> None: print("Proxy: Logging the time of request.") # The client code is supposed to work with all objects (both subjects and # proxies) via the Subject interface in order to support both real subjects and # proxies. In real life, however, clients mostly work with their real subjects # directly. In this case, to implement the pattern more easily, you can extend # your proxy from the real subject's class. def client_code(subject: Subject) -> None: # ... subject.request() # ... if __name__ == "__main__": print("Client: Executing the client code with a real subject:") real_subject = RealSubject() client_code(real_subject) print("") print("Client: Executing the same client code with a proxy:") proxy = Proxy(real_subject) client_code(proxy)
In this example, we have a Subject
interface with an abstract request()
method, which is implemented by both RealSubject
and Proxy
classes. The RealSubject
class represents an object that performs some complex computation, while the Proxy
class acts as a surrogate for the RealSubject
object, controlling its access and providing additional functionality.
When a client invokes the request()
method on a Proxy
object, the Proxy
first checks for access rights, and then either forwards the request to the RealSubject
object or logs the access. The client interacts with the Subject
interface, and is unaware of whether it’s dealing with a RealSubject
or a Proxy
object.
This pattern is useful when you want to add additional functionality to an object without changing its interface, or when you want to control access to the object from a remote location.
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently from the clients that use it. For example, a sorting algorithm can be implemented using the Strategy pattern, allowing the client to choose between different sorting methods based on its needs.
// Define an interface for the strategy interface SortingStrategy { public void sort(int[] arr); } // Define concrete strategies that implement the interface class BubbleSortStrategy implements SortingStrategy { public void sort(int[] arr) { // implementation of bubble sort } } class QuickSortStrategy implements SortingStrategy { public void sort(int[] arr) { // implementation of quick sort } } // Define a context class that uses the strategy class SortingContext { private SortingStrategy sortingStrategy; public SortingContext(SortingStrategy sortingStrategy) { this.sortingStrategy = sortingStrategy; } public void setSortingStrategy(SortingStrategy sortingStrategy) { this.sortingStrategy = sortingStrategy; } public void sort(int[] arr) { sortingStrategy.sort(arr); } } // Client code that uses the context class to apply the strategy public class Client { public static void main(String[] args) { int[] arr = {5, 2, 7, 1, 9}; SortingContext sortingContext = new SortingContext(new BubbleSortStrategy()); sortingContext.sort(arr); sortingContext.setSortingStrategy(new QuickSortStrategy()); sortingContext.sort(arr); } }
In this example, we have an interface SortingStrategy
that defines a sort
method. Two concrete strategies BubbleSortStrategy
and QuickSortStrategy
implement this interface and provide their own implementation of the sort
method.
The SortingContext
class is the context that uses the selected strategy to perform the sorting operation. The context class is initialized with a default sorting strategy, which can be changed later on by the client code. The sort
method in the context class delegates the sorting operation to the current sorting strategy.
Finally, the client code creates an instance of the SortingContext
class and initializes it with the BubbleSortStrategy
. The sort
method is called on the context object, which delegates the sorting operation to the BubbleSortStrategy
. The sorting strategy is then changed to QuickSortStrategy
, and the sort
method is called again on the same context object, which delegates the sorting operation to the new strategy.
Command Pattern
The Command pattern encapsulates a request as an object, allowing us to parameterize clients with different requests, queue or log requests, and support undoable operations. For example, a remote control for a television can use the Command pattern to store a list of different commands, such as changing channels or adjusting volume, and then execute them when needed.
from abc import ABC, abstractmethod # Receiver class Light: def on(self): print("Light is turned ON") def off(self): print("Light is turned OFF") # Command class Command(ABC): @abstractmethod def execute(self): pass # Concrete Command class LightOnCommand(Command): def __init__(self, light: Light): self.light = light def execute(self): self.light.on() # Concrete Command class LightOffCommand(Command): def __init__(self, light: Light): self.light = light def execute(self): self.light.off() # Invoker class RemoteControl: def __init__(self, command: Command): self.command = command def press_button(self): self.command.execute() if __name__ == "__main__": light = Light() light_on_command = LightOnCommand(light) light_off_command = LightOffCommand(light) remote_control = RemoteControl(light_on_command) remote_control.press_button() remote_control.command = light_off_command remote_control.press_button()
In this example, we have a Light
class that serves as the Receiver, Command
class as an abstract base class for all commands, and LightOnCommand
and LightOffCommand
classes as Concrete Commands. We also have an Invoker
class called RemoteControl
which has a Command
object and can execute the command by calling its execute
method.
Template Method Pattern
The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It allows subclasses to redefine certain steps of an algorithm without changing its structure. For example, a game engine can use the Template Method pattern to define the basic structure of a game loop, while allowing individual games to customize the details of how the loop works.
public abstract class Game { // template method public final void play() { initialize(); startPlay(); endPlay(); } // concrete method private void initialize() { System.out.println("Initializing the game..."); } // abstract methods to be implemented by subclasses protected abstract void startPlay(); protected abstract void endPlay(); } public class Football extends Game { @Override protected void startPlay() { System.out.println("Starting football game..."); } @Override protected void endPlay() { System.out.println("Ending football game..."); } } public class Basketball extends Game { @Override protected void startPlay() { System.out.println("Starting basketball game..."); } @Override protected void endPlay() { System.out.println("Ending basketball game..."); } } public class TemplateMethodExample { public static void main(String[] args) { Game football = new Football(); football.play(); Game basketball = new Basketball(); basketball.play(); } }
In this example, Game
is an abstract class that defines a template method play()
that calls several other methods, including a concrete method initialize()
and two abstract methods startPlay()
and endPlay()
. The Football
and Basketball
classes are concrete subclasses that implement the abstract methods. When the play()
method is called on a Football
or Basketball
object, it follows the template defined in the Game
class, calling the initialize()
method, the appropriate startPlay()
method for the subclass, and the endPlay()
method.
These are just a few examples of the many software design patterns that can be used to improve the quality, maintainability, and extensibility of software systems.