Design Pattern
Design Pattern
Design patterns are general reusable solutions to common problems that occur during software development. They represent best practices evolved over time by experienced developers. There are several design patterns, categorized into three main types: creational, structural, and behavioral patterns.
Creational Patterns:
Singleton Pattern: Ensures a class has only one instance and provides a global point of access to it.
Factory Method Pattern: Defines an interface for creating an object but leaves the choice of its type to the subclasses, creating an instance of a class.
Abstract Factory Pattern: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Structural Patterns:
Adapter Pattern: Allows the interface of an existing class to be used as another interface.
Decorator Pattern: Attaches additional responsibilities to an object dynamically.
Composite Pattern: Composes objects into tree structures to represent part-whole hierarchies.
Behavioral Patterns:
Observer Pattern: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Strategy Pattern: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Command Pattern: Encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of the parameters.
These patterns help developers create more maintainable and scalable software by promoting modular, reusable, and clean code. It's essential to choose the right pattern based on the specific problem or requirement at hand.
Explain each of them with java code and explain when and how to use them
Certainly! Let's go through each design pattern with a brief explanation and provide a simple Java code example for each.
1. Singleton Pattern:
Explanation: Ensures that a class has only one instance and provides a global point of access to it.
Java Code:
public class Singleton {
private static Singleton instance;
private Singleton() {
// private constructor to prevent instantiation
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
When to use: Use the Singleton pattern when you want to ensure that a class has only one instance, and you want a global point of access to that instance. Commonly used for logging, driver objects, caching, thread pools, and database connections.
2. Factory Method Pattern:
Explanation: Defines an interface for creating an object but leaves the choice of its type to the subclasses, creating an instance of a class.
Java Code:
// Product interface
public interface Product {
void create();
}
// ConcreteProductA class
public class ConcreteProductA implements Product {
@Override
public void create() {
System.out.println("ConcreteProductA created.");
}
}
// ConcreteProductB class
public class ConcreteProductB implements Product {
@Override
public void create() {
System.out.println("ConcreteProductB created.");
}
}
// Creator interface (Factory Method)
public interface Creator {
Product factoryMethod();
}
// ConcreteCreatorA class
public class ConcreteCreatorA implements Creator {
@Override
public Product factoryMethod() {
return new ConcreteProductA();
}
}
// ConcreteCreatorB class
public class ConcreteCreatorB implements Creator {
@Override
public Product factoryMethod() {
return new ConcreteProductB();
}
}
// Client class
public class Client {
public static void main(String[] args) {
// Creating an instance of ConcreteCreatorA
Creator creatorA = new ConcreteCreatorA();
Product productA = creatorA.factoryMethod();
productA.create();
// Creating an instance of ConcreteCreatorB
Creator creatorB = new ConcreteCreatorB();
Product productB = creatorB.factoryMethod();
productB.create();
}
}
In this example, we have a Product
interface representing the products to be created. Concrete classes ConcreteProductA
and ConcreteProductB
implement the Product
interface. The Creator
interface declares the factory method (factoryMethod
) for creating a Product
.
Concrete classes ConcreteCreatorA
and ConcreteCreatorB
implement the Creator
interface and provide their own implementations of the factory method, creating instances of ConcreteProductA
and ConcreteProductB
, respectively.
The Client
class demonstrates how to use the Factory Method Pattern by creating instances of ConcreteCreatorA
and ConcreteCreatorB
and then using their factory methods to create specific products (ConcreteProductA
and ConcreteProductB
).
When to use: Use the Factory Method pattern when a class cannot anticipate the class of objects it must create, or you want to delegate the responsibility of object instantiation to its subclasses.
3. Abstract Factory Pattern:
Explanation: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Java Code:
// Abstract Product A
interface ProductA {
void create();
}
// Concrete Product A1
class ConcreteProductA1 implements ProductA {
@Override
public void create() {
System.out.println("Product A1 created.");
}
}
// Concrete Product A2
class ConcreteProductA2 implements ProductA {
@Override
public void create() {
System.out.println("Product A2 created.");
}
}
// Abstract Product B
interface ProductB {
void create();
}
// Concrete Product B1
class ConcreteProductB1 implements ProductB {
@Override
public void create() {
System.out.println("Product B1 created.");
}
}
// Concrete Product B2
class ConcreteProductB2 implements ProductB {
@Override
public void create() {
System.out.println("Product B2 created.");
}
}
// Abstract Factory
interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}
// Concrete Factory 1
class ConcreteFactory1 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA1();
}
@Override
public ProductB createProductB() {
return new ConcreteProductB1();
}
}
// Concrete Factory 2
class ConcreteFactory2 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA2();
}
@Override
public ProductB createProductB() {
return new ConcreteProductB2();
}
}
// Client
public class Client {
public static void main(String[] args) {
// Using Concrete Factory 1
AbstractFactory factory1 = new ConcreteFactory1();
ProductA productA1 = factory1.createProductA();
ProductB productB1 = factory1.createProductB();
productA1.create();
productB1.create();
// Using Concrete Factory 2
AbstractFactory factory2 = new ConcreteFactory2();
ProductA productA2 = factory2.createProductA();
ProductB productB2 = factory2.createProductB();
productA2.create();
productB2.create();
}
}
In this example:
ProductA
andProductB
are abstract product interfaces.ConcreteProductA1
,ConcreteProductA2
,ConcreteProductB1
, andConcreteProductB2
are concrete product implementations.AbstractFactory
is the abstract factory interface declaring the methods to createProductA
andProductB
.ConcreteFactory1
andConcreteFactory2
are concrete factory implementations that create families of related products.The
Client
class demonstrates how to use the abstract factory and create instances of products from different families.
When to use: Use the Abstract Factory pattern when you want to provide an interface for creating families of related or dependent objects without specifying their concrete classes. It's especially useful when the system needs to be independent of how its objects are created, composed, and represented.
4. Adapter Pattern:
Explanation: Allows the interface of an existing class to be used as another interface.
Java Code:
// Target interface
interface Target {
void request();
}
// Adaptee (existing class)
class Adaptee {
void specificRequest() {
System.out.println("Specific request from Adaptee.");
}
}
// Adapter
class Adapter implements Target {
private Adaptee adaptee;
Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specificRequest();
}
}
// Client
public class Client {
public static void main(String[] args) {
// Using the Target interface directly
Target target1 = new Adapter();
target1.request();
// Using the Adapter to make Adaptee compatible with Target
Adaptee adaptee = new Adaptee();
Target target2 = new Adapter(adaptee);
target2.request();
}
}
In this example:
Target
is the target interface that the client code expects.Adaptee
is an existing class with a methodspecificRequest
that needs to be adapted.Adapter
is the adapter class that implements theTarget
interface and wraps an instance ofAdaptee
.Client
demonstrates how both theTarget
and the adaptedAdaptee
can be used interchangeably through theAdapter
.
When the client code calls the request
method on the Target
, it can be fulfilled either by the ConcreteTarget
(a class implementing the Target
interface directly) or by the Adapter
, which internally delegates the request to the specificRequest
method of Adaptee
.
When to use: Use the Adapter pattern when you want to make an existing class with a compatible interface work with a class that expects a different interface. It is often used to make existing classes work with others without modifying their source code.
5. Decorator Pattern:
Explanation: Attaches additional responsibilities to an object dynamically.
Java Code:
// Component interface
interface Component {
void operation();
}
// Concrete Component
class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("ConcreteComponent operation.");
}
}
// Decorator
abstract class Decorator implements Component {
protected Component component;
Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
// Concrete Decorator A
class ConcreteDecoratorA extends Decorator {
ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
System.out.println("ConcreteDecoratorA operation.");
}
}
// Concrete Decorator B
class ConcreteDecoratorB extends Decorator {
ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
System.out.println("ConcreteDecoratorB operation.");
}
}
// Client
public class Client {
public static void main(String[] args) {
// Using the Concrete Component directly
Component component = new ConcreteComponent();
component.operation();
System.out.println("-----------------------");
// Using the Decorators to add additional behavior
Component decoratedComponentA = new ConcreteDecoratorA(new ConcreteComponent());
decoratedComponentA.operation();
System.out.println("-----------------------");
Component decoratedComponentB = new ConcreteDecoratorB(new ConcreteComponent());
decoratedComponentB.operation();
System.out.println("-----------------------");
// Using multiple decorators
Component fullyDecoratedComponent = new ConcreteDecoratorA(new ConcreteDecoratorB(new ConcreteComponent()));
fullyDecoratedComponent.operation();
}
}
In this example:
Component
is the interface that defines the basic behavior.ConcreteComponent
is the concrete implementation ofComponent
.Decorator
is the abstract decorator class that implementsComponent
and has a reference to aComponent
instance.ConcreteDecoratorA
andConcreteDecoratorB
are concrete decorator classes that add specific behavior to the decorated component.Client
demonstrates how to use the concrete component and decorators to create and compose objects with different combinations of behavior.
When to use: Use the Decorator pattern when you want to add new functionality to an object dynamically without altering its structure. It is an alternative to subclassing for extending behavior.
6. Composite Pattern:
Explanation: Composes objects into tree structures to represent part-whole hierarchies.
Java Code:
import java.util.ArrayList;
import java.util.List;
// Component interface
interface Component {
void operation();
}
// Leaf
class Leaf implements Component {
private String name;
Leaf(String name) {
this.name = name;
}
@Override
public void operation() {
System.out.println("Leaf: " + name);
}
}
// Composite
class Composite implements Component {
private List<Component> children = new ArrayList<>();
void add(Component component) {
children.add(component);
}
void remove(Component component) {
children.remove(component);
}
@Override
public void operation() {
for (Component component : children) {
component.operation();
}
}
}
// Client
public class Client {
public static void main(String[] args) {
// Creating leaf components
Component leaf1 = new Leaf("Leaf 1");
Component leaf2 = new Leaf("Leaf 2");
Component leaf3 = new Leaf("Leaf 3");
// Creating composite components
Composite composite1 = new Composite();
Composite composite2 = new Composite();
// Adding leaf and composite components to the first composite
composite1.add(leaf1);
composite1.add(leaf2);
composite1.add(composite2);
// Adding another leaf to the second composite
composite2.add(leaf3);
// Operation on the first composite triggers operations on its children
composite1.operation();
}
}
In this example:
Component
is the interface that declares the common operation for both leaf and composite objects.Leaf
is a leaf component that implements theComponent
interface.Composite
is a composite component that can have child components, both leaf and other composites.Client
demonstrates how to create and use a composite structure by adding leaf and composite components to the hierarchy and performing operations on the entire structure.
When to use: Use the Composite pattern when you want to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
7. Observer Pattern:
Explanation: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Java Code:
import java.util.ArrayList;
import java.util.List;
// Subject interface
interface Subject {
void addObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
int getState();
void setState(int state);
}
// Concrete Subject
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private int state;
@Override
public void addObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
@Override
public int getState() {
return state;
}
@Override
public void setState(int state) {
this.state = state;
notifyObservers();
}
}
// Observer interface
interface Observer {
void update();
}
// Concrete Observer
class ConcreteObserver implements Observer {
private Subject subject;
ConcreteObserver(Subject subject) {
this.subject = subject;
subject.addObserver(this);
}
@Override
public void update() {
System.out.println("Observer updated with state: " + subject.getState());
}
}
// Client
public class Client {
public static void main(String[] args) {
// Creating a concrete subject
ConcreteSubject subject = new ConcreteSubject();
// Creating concrete observers and attaching them to the subject
Observer observer1 = new ConcreteObserver(subject);
Observer observer2 = new ConcreteObserver(subject);
// Setting the state of the subject will notify all attached observers
subject.setState(5);
}
}
In this example:
Subject
is the interface that declares methods for managing observers and notifying them of changes in state.ConcreteSubject
is the concrete subject that maintains a list of observers and notifies them when its state changes.Observer
is the interface that declares theupdate
method, which is called by the subject to notify observers of changes.ConcreteObserver
is the concrete observer that registers itself with a subject and updates its state when notified.Client
demonstrates how to create a concrete subject, attach observers to it, and trigger notifications by changing the subject's state.
When to use: Use the Observer pattern when a one-to-many dependency exists between objects, and when one object changes state, all its dependents are notified and updated automatically. It is commonly used in implementing distributed event handling systems.
8. Strategy Pattern:
Explanation: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Java Code:
// Strategy interface
interface Strategy {
void execute();
}
// Concrete Strategy A
class ConcreteStrategyA implements Strategy {
@Override
public void execute() {
System.out.println("Executing strategy A.");
}
}
// Concrete Strategy B
class ConcreteStrategyB implements Strategy {
@Override
public void execute() {
System.out.println("Executing strategy B.");
}
}
// Context
class Context {
private Strategy strategy;
void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
void executeStrategy() {
strategy.execute();
}
}
// Client
public class Client {
public static void main(String[] args) {
// Creating strategies
Strategy strategyA = new ConcreteStrategyA();
Strategy strategyB = new ConcreteStrategyB();
// Creating context and setting the strategy
Context context = new Context();
// Using strategy A
context.setStrategy(strategyA);
context.executeStrategy();
// Using strategy B
context.setStrategy(strategyB);
context.executeStrategy();
}
}
In this example:
Strategy
is the interface that declares the algorithm to be executed.ConcreteStrategyA
andConcreteStrategyB
are concrete implementations of theStrategy
interface, each representing a different algorithm.Context
is the class that maintains a reference to the current strategy and delegates the execution to the strategy.
When to use: Use the Strategy pattern when you want to define a family of algorithms, encapsulate each algorithm, and make them interchangeable. It allows the client to choose the appropriate algorithm at runtime.
9. Command Pattern:
Explanation: Encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of the parameters.
Java Code:
// Command interface
interface Command {
void execute();
}
// Concrete Command
class ConcreteCommand implements Command {
private Receiver receiver;
ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.action();
}
}
// Receiver
class Receiver {
void action() {
System.out.println("Receiver is performing an action.");
}
}
// Invoker
class Invoker {
private Command command;
void setCommand(Command command) {
this.command = command;
}
void executeCommand() {
command.execute();
}
}
// Client
public class Client {
public static void main(String[] args) {
// Creating receiver
Receiver receiver = new Receiver();
// Creating command and associating it with the receiver
Command command = new ConcreteCommand(receiver);
// Creating invoker and setting the command
Invoker invoker = new Invoker();
invoker.setCommand(command);
// Invoking the command
invoker.executeCommand();
}
}
In this example:
Command
is the interface that declares theexecute
method.ConcreteCommand
is the concrete implementation of theCommand
interface, encapsulating a specific action to be performed by theReceiver
.Receiver
is the class that performs the actual action.Invoker
is the class that invokes the command, but it doesn't need to know the details of how the command is executed.
The Client
code demonstrates how to create a receiver, associate it with a command, and use an invoker to execute the command. The beauty of the Command Pattern is that it decouples the sender (Invoker) from the receiver and allows easy extension by introducing new command classes.
When to use: Use the Command pattern when you want to parameterize objects with operations, queue requests, and support undoable operations. It separates the sender and receiver of a command, allowing for decoupling and flexibility.