To advertise with us contact on Whatsapp: +923041280395 For guest post email at: [email protected]

Design Patterns in Software Engineering

Design Patterns in Software Engineering

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.

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.

 

Leave a Reply

Your email address will not be published. Required fields are marked *