Design Patterns
Design Patterns
● Design patterns are typical solutions to commonly occurring problems in software design. They are like pre-made
blueprints that you can customize to solve a recurring design problem in your code.
● You can’t just find a pattern and copy it into your program, the way you can with off-the-shelf functions or libraries. The
pattern is not a specific piece of code, but a general concept for solving a particular problem. You can follow the pattern
details and implement a solution that suits the realities of your own program.
Pattern vs algorithm
● Patterns are often confused with algorithms, because both concepts describe typical solutions to some known problems.
While an algorithm always defines a clear set of actions that can achieve some goal, a pattern is a more high-level
description of a solution. The code of the same pattern applied to two different programs may be different.
● An analogy to an algorithm is a cooking recipe: both have clear steps to achieve a goal. On the other hand, a pattern is
more like a blueprint: you can see what the result and its features are, but the exact order of implementation is up to
you.
Classification of Patterns
● Creational patterns provide object creation mechanisms that increase flexibility and reuse of existing code.
● Structural patterns explain how to assemble objects and classes into larger structures, while keeping these structures
flexible and efficient.
● Behavioral patterns take care of effective communication and the assignment of responsibilities between objects
The 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. It encapsulates the object creation logic and allows the client code
to use the same interface to create different types of objects without specifying their concrete classes.
+-------------------------------------+
| Task |
+-------------------------------------+
| - taskId: int |
| - taskName: String |
| - taskDescription: String |
+-------------------------------------+
| + Task(taskId: int, taskName: String, taskDescription: String) |
| + getTaskId(): int |
| + getTaskName(): String |
| + getTaskDescription(): String |
+-------------------------------------+
|
|
|
v
+-------------------------------------+
| TaskManager |
+-------------------------------------+
| + addTask(task: Task): void |
| + deleteTask(task: Task): void |
| + displayTaskDetails(task: Task): void |
+-------------------------------------+
^
|
|
+-------------------------------------+
| FileHandler |
+-------------------------------------+
| + readFile(filePath: String): void |
| + writeFile(filePath: String, task: Task): void |
+-------------------------------------+
^ ^
| |
+------------------+ +-----------------+
| TxtFileHandler | | JsonFileHandler |
+------------------+ +-----------------+
| | | |
+------------------+ +-----------------+
// Task.java
public class Task {
private int taskId;
private String taskName;
private String taskDescription;
// TaskManager.java
public interface TaskManager {
void addTask(Task task);
void deleteTask(Task task);
void displayTaskDetails(Task task);
}
// TxtFileHandler.java
public class TxtFileHandler implements FileHandler {
@Override
public void readFile(String filePath) {
// Implementation for reading from a .txt file
}
@Override
public void writeFile(String filePath, Task task) {
// Implementation for writing to a .txt file
}
}
// JsonFileHandler.java
public class JsonFileHandler implements FileHandler {
@Override
public void readFile(String filePath) {
// Implementation for reading from a .json file
}
@Override
public void writeFile(String filePath, Task task) {
// Implementation for writing to a .json file
}
}
// TaskManagerFactory.java
public class TaskManagerFactory {
public static TaskManager createTaskManager(String fileType) {
if ("txt".equalsIgnoreCase(fileType)) {
return new TaskManagerImpl(new TxtFileHandler());
} else if ("json".equalsIgnoreCase(fileType)) {
return new TaskManagerImpl(new JsonFileHandler());
} else {
throw new IllegalArgumentException("Invalid file type");
}
}
}
// TaskManagerImpl.java
public class TaskManagerImpl implements TaskManager {
private FileHandler fileHandler;
@Override
public void addTask(Task task) {
// Implementation for adding a task
fileHandler.writeFile("tasks.txt", task);
}
@Override
public void deleteTask(Task task) {
// Implementation for deleting a task
}
@Override
public void displayTaskDetails(Task task) {
// Implementation for displaying task details
System.out.println("Task ID: " + task.getTaskId());
System.out.println("Task Name: " + task.getTaskName());
System.out.println("Task Description: " + task.getTaskDescription());
}
}
Now, let's implement the Adapter Design Pattern in the context of the Task Manager example. Suppose we have an existing
FileReader class that reads tasks from a file, but its interface is different from what our TaskManager expects. We can
create an adapter class FileReaderAdapter that adapts the FileReader interface to the TaskManager interface.
@Override
public void addTask(Task task) {
// Implementation for adding task using FileReader
List<Task> tasks = fileReader.readTasksFromFile("tasks.txt");
tasks.add(task);
// Save modified tasks back to the file (if necessary)
}
@Override
public void deleteTask(Task task) {
// Implementation for deleting task using FileReader
List<Task> tasks = fileReader.readTasksFromFile("tasks.txt");
tasks.remove(task);
// Save modified tasks back to the file (if necessary)
}
@Override
public void displayTaskDetails(Task task) {
// Implementation for displaying task details using FileReader
List<Task> tasks = fileReader.readTasksFromFile("tasks.txt");
// Display details of the specified task (if it exists in the file)
}
}
We'll have a TaskManager interface representing the task management functionality, and a RealTaskManager class as
the real subject that performs actual task management operations. The TaskManagerProxy class will act as a proxy to
control access to the real task manager.
// TaskManager.java
public interface TaskManager {
void addTask(Task task);
void deleteTask(Task task);
void displayTaskDetails(Task task);
}
// RealTaskManager.java
public class RealTaskManager implements TaskManager {
@Override
public void addTask(Task task) {
System.out.println("Task added: " + task.getTaskName());
}
@Override
public void deleteTask(Task task) {
System.out.println("Task deleted: " + task.getTaskName());
}
@Override
public void displayTaskDetails(Task task) {
System.out.println("Task details: " + task.getTaskName());
}
}
// TaskManagerProxy.java
public class TaskManagerProxy implements TaskManager {
private RealTaskManager realTaskManager = new RealTaskManager();
@Override
public void addTask(Task task) {
// Additional logic can be added here (e.g., access control)
realTaskManager.addTask(task);
}
@Override
public void deleteTask(Task task) {
// Additional logic can be added here (e.g., logging)
realTaskManager.deleteTask(task);
}
@Override
public void displayTaskDetails(Task task) {
// Additional logic can be added here (e.g., caching)
realTaskManager.displayTaskDetails(task);
}
}
// Task.java
public class Task {
private int taskId;
private String taskName;
private String taskDescription;
// Main.java
public class Main {
public static void main(String[] args) {
TaskManager taskManager = new TaskManagerProxy();
Let's apply the Bridge Design Pattern to our Task Manager example. We'll have an abstraction TaskManager interface and
its implementation classes (PersonalTaskManager and WorkTaskManager). Additionally, we'll have a separate
implementation interface StorageProvider and its implementation classes (DatabaseStorageProvider and
FileStorageProvider). This way, the task management functionality (TaskManager) can be developed independently
from the storage mechanism (StorageProvider), and they can be combined flexibly.
// TaskManager.java (Abstraction interface)
public interface TaskManager {
void addTask(Task task);
void deleteTask(Task task);
void displayTaskDetails(Task task);
}
@Override
public void addTask(Task task) {
// Implementation specific to PersonalTaskManager
storageProvider.storeTask(task);
System.out.println("Personal task added: " + task.getTaskName());
}
@Override
public void addTask(Task task) {
// Implementation specific to WorkTaskManager
storageProvider.storeTask(task);
System.out.println("Work task added: " + task.getTaskName());
}
The Chain of Responsibility pattern is a behavioral design pattern that allows multiple objects to handle a request without the
sender needing to know which object will handle it. It creates a chain of objects, each of which has the ability to either handle
the request or pass it on to the next object in the chain.
In simpler terms, the Chain of Responsibility pattern decouples the sender of a request from its receiver, allowing multiple
objects to handle the request in a flexible and organized manner.
Let's apply the Chain of Responsibility pattern to our Task Manager example. We'll create a chain of handlers to process task-
related requests, such as adding, deleting, or displaying task details.
// Task.java
public class Task {
private int taskId;
private String taskName;
private String taskDescription;
@Override
public void handleTask(Task task) {
// Logic to handle adding task
System.out.println("Task added: " + task.getTaskName());
@Override
public void handleTask(Task task) {
// Logic to handle deleting task
System.out.println("Task deleted: " + task.getTaskName());
public TaskManager() {
// Build the chain of responsibility
taskHandlerChain = new AddTaskHandler(new DeleteTaskHandler(new DisplayTaskDet
}
// Main.java
public class Main {
public static void main(String[] args) {
TaskManager taskManager = new TaskManager();
In simpler terms, the Decorator pattern allows us to add new functionality to an object without changing its structure. It
achieves this by creating a set of decorator classes that are used to wrap concrete components and add new behaviors.
Let's apply the Decorator Design Pattern to our Task Manager example. We'll have a base TaskManager interface
representing the core functionality, and we'll create concrete implementations of this interface. Then, we'll create decorator
classes that add additional functionalities, such as encryption or logging, to the task manager.
@Override
public void deleteTask(Task task) {
System.out.println("Task deleted: " + task.getTaskName());
}
@Override
public void displayTaskDetails(Task task) {
System.out.println("Task details: " + task.getTaskName());
}
}
// TaskManagerDecorator.java (Decorator)
public abstract class TaskManagerDecorator implements TaskManager {
protected TaskManager taskManager;
@Override
public void addTask(Task task) {
taskManager.addTask(task);
}
@Override
public void deleteTask(Task task) {
taskManager.deleteTask(task);
}
@Override
public void displayTaskDetails(Task task) {
taskManager.displayTaskDetails(task);
}
}
@Override
public void addTask(Task task) {
// Additional encryption logic
System.out.println("Encrypting task data...");
super.addTask(task);
}
}
@Override
public void addTask(Task task) {
// Additional logging logic
System.out.println("Logging task addition...");
super.addTask(task);
}
}
The Command Design Pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing
parameterization of clients with queues, requests, and operations. It allows the requester to be decoupled from the executor,
enabling parameterization of clients with queues, requests, and operations.
Imagine you have a remote control with different buttons, each representing a specific action. When you press a button, the
remote control sends a command to execute that action. The remote control doesn't need to know how the action is
performed; it only knows the command to execute.
Let's apply the Command Design Pattern to our Task Manager example. We'll define a Command interface representing a
task-related operation, and concrete implementations of this interface for different actions such as adding, deleting, or
displaying task details.
// Command.java
public interface Command {
void execute();
}
@Override
public void execute() {
taskManager.addTask(task);
}
}
@Override
public void execute() {
taskManager.deleteTask(task);
}
}
@Override
public void execute() {
taskManager.displayTaskDetails(task);
}
}
// TaskManagerInvoker.java (Invoker)
public class TaskManagerInvoker {
private Command command;
// Create commands
Command addCommand = new AddTaskCommand(taskManager, task1);
Command deleteCommand = new DeleteTaskCommand(taskManager, task1);
Command displayCommand = new DisplayTaskDetailsCommand(taskManager, task1);
// Invoker
TaskManagerInvoker invoker = new TaskManagerInvoker();
invoker.setCommand(deleteCommand);
invoker.executeCommand();
invoker.setCommand(displayCommand);
invoker.executeCommand();
}
}
In simpler terms, the Observer pattern allows multiple objects to observe and react to changes in another object's state without
needing to be tightly coupled to it. It promotes loose coupling and flexibility in object interactions.
Let's apply the Observer Design Pattern to our Task Manager example. We'll have a TaskManager class as the subject that
maintains a list of observers (e.g., TaskObserver) and notifies them when a task is added, deleted, or modified.
import java.util.ArrayList;
import java.util.List;
// Task.java (Subject)
public class Task {
private int taskId;
private String taskName;
private String taskDescription;
private List<TaskObserver> observers = new ArrayList<>();
public Task(int taskId, String taskName, String taskDescription) {
this.taskId = taskId;
this.taskName = taskName;
this.taskDescription = taskDescription;
}
// TaskObserver.java (Observer)
public interface TaskObserver {
void update(Task task);
}
// TaskManager.java (Subject)
public class TaskManager {
private List<TaskObserver> observers = new ArrayList<>();
// Register observer
taskManager.addObserver(taskLogger);
// Perform actions
Task task1 = new Task(1, "Task 1", "Description 1");
taskManager.addTask(task1);
In simpler terms, the Builder pattern allows you to construct complex objects step by step. It's like customizing a pizza where
you can choose the crust, toppings, and sauce separately to create different combinations.
Let's apply the Builder Design Pattern to our Task Manager example. We'll create a Task class with a nested TaskBuilder
class that allows us to construct tasks with different configurations step by step.
// Task.java
public class Task {
private int taskId;
private String taskName;
private String taskDescription;
// Main.java
public class Main {
public static void main(String[] args) {
// Using the TaskBuilder to create a Task object
Task task = new Task.TaskBuilder()
.taskId(1)
.taskName("Task 1")
.taskDescription("Description 1")
.build();
The Model-View-Controller (MVC) pattern is a design pattern used to structure applications by separating their concerns into
three main components: Model, View, and Controller.
1. Model: Represents the application's data and business logic. It encapsulates the data and behavior of the application,
responding to requests for information and changing its state as necessary.
2. View: Represents the presentation layer of the application, responsible for displaying the data to the user. It receives
input from the user and forwards it to the controller for processing.
3. Controller: Acts as an intermediary between the model and view, handling user input and updating the model
accordingly. It receives input from the view, processes it (usually by invoking methods on the model), and updates the
view accordingly.
In simpler terms, the MVC pattern separates the concerns of an application into three distinct components: data management
(Model), user interface (View), and user interaction (Controller).
// Model: Task.java
public class Task {
private int taskId;
private String taskName;
private String taskDescription;
// View: TaskView.java
public class TaskView {
public void displayTaskDetails(Task task) {
System.out.println("Task ID: " + task.getTaskId());
System.out.println("Task Name: " + task.getTaskName());
System.out.println("Task Description: " + task.getTaskDescription());
}
}
// Controller: TaskController.java
public class TaskController {
private Task model;
private TaskView view;
// Main.java
public class Main {
public static void main(String[] args) {
// Create a Task object
Task task = new Task();
task.setTaskId(1);
task.setTaskName("Task 1");
task.setTaskDescription("Description 1");