DESIGN PATTERNS
DESIGN PATTERNS
I. Factory method
1. Intent
- Define an interface for creating an object, but let subclasses decide
which class to instantiate
- Factory Method lets a class defer instantiation to subclasses
2. Usecase
- A class cannot anticipate (foresee) the class of objects it must create
- A class wants its subclasses to specify the objects it create (A class
wants to delegate the task of object creation to its subclasses so that
they can specify the exact object types)
- There are multiple helper subclasses, and the design requires the
ability to localize which subclass is responsible for creating a specific
type of object.
3. Examples
- Document Creation in Office Applications
Scenario: An office application (like Microsoft Office) needs to
handle different types of documents, such as Word documents,
Excel spreadsheets, and PowerPoint presentations.
Factory Method Use: A Document Creator class can define a
method, createDocument(), but the specific document type (Word,
Excel, PowerPoint) is determined by subclasses like
WordDocumentCreator, ExcelDocumentCreator, and
PowerPointDocumentCreator.
Benefit: When a new document type is added, a new subclass can
be created without altering the existing DocumentCreator class.
@Override
public void save() {
System.out.println("Saving an Excel spreadsheet.");
}
}
@Override
public void save() {
System.out.println("Saving a PowerPoint presentation.");
}
}
Client code:
public class Main {
public static void main(String[] args) {
DocumentCreator wordCreator = new WordDocumentCreator();
Document wordDocument = wordCreator.createDocument();
wordDocument.open();
wordDocument.save();
// Step 3: Provide a public static method to get the instance of the class
public static SingletonObject getInstance() {
if (instance == null) {
instance = new SingletonObject(); // Create a new instance if it
doesn’t exist
}
return instance;
}
- Database Connection
Scenario: Many applications need to connect to a database, and
creating multiple database connections can be resource-intensive
and lead to conflicts.
Singleton Use: A DatabaseConnection class can be designed as a
Singleton, ensuring that only one instance of the database
connection exists throughout the application.
- Logger
Scenario: In many applications, there is a single logger that
handles all logging throughout the app. Multiple logger instances
could create inconsistent logs.
Singleton Use: A Logger class can be made as a Singleton,
allowing a single logging instance that is accessible from anywhere
in the application.
- Configuration Settings Manager
Scenario: Many applications need to read configuration settings
(like API keys, file path, cand constants) that should remain
consistent across the app.
Singleton Use: A ConfigurationManager Singleton ensures that
settings are loaded once and are accessible globally.
- Cache Manager
Scenario: In an application that uses caching, you may want a
global, single instance to manage cache data.
Singleton Use: A CacheManager class can act as a Singleton to
store and manage cache data centrally
- Print Spooler
Scenario: A print spooler queues print jobs and manages printing
resources. Multiple instances of a print spooler could lead to
resource conflict.
Singleton Use: A PrintSpooler class can be implemented as a
Singleton, so only one instance controls the print queue.
- Thread Pool Manager
Scenario: Many applications use thread pools to manage a limited
number of threads for handling tasks. Having multiple instances of
thread pools can lead to inefficient resource usage.
Singleton Use: A ThreadPoolManager can be implemented as a
Singleton to ensure only one pool manages threads, controlling the
number of active threads.
4. Advantages
- Controlled Access to a Single Instance: Singleton ensures only one
instance of a class exists throughout the application's lifecycle. This is
useful for managing resources that are costly or logically only need
one instance, like configuration settings, logging, or connection pools.
- Reduced Memory Usage: By limiting the class to one instance, the
Singleton pattern minimizes memory overhead. Rather than creating
multiple instances of the same class, it reuses a single object, which
can be particularly valuable in resource-constrained environments.
- Consistency Across the System: Since the same instance is shared,
any changes made in one part of the application are reflected across
all other parts. This makes Singleton ideal for scenarios where a
consistent state or behavior is required, like in caching, logging, or
application configurations.
- Lazy Initialization: Many Singleton implementations use lazy
initialization, where the instance is only created when it’s needed.
This can save resources by avoiding unnecessary instantiation,
especially if the Singleton is not used immediately or in every
execution.
- Thread Safety (with Proper Implementation): With appropriate
synchronization or a thread-safe implementation, the Singleton pattern
ensures that only one instance is created in a multi-threaded
environment. This is particularly useful for managing shared resources
like databases, ensuring consistency and avoiding conflicts.
- Easy to Access: The Singleton provides a global point of access to its
instance, which simplifies retrieving and sharing the same instance
across various parts of the application without needing to pass it
around manually.
- Encapsulation of Instance Control: By controlling the instance
creation within the Singleton class itself, the pattern encapsulates the
instantiation logic. This makes it easy to change the instantiation
process without impacting the rest of the code.
5. Disadvantages
- Global State and Hidden Dependencies: Since Singleton provides a
global instance, it can create hidden dependencies within the
codebase. This global state can make code less modular, more
interdependent, and harder to understand or debug.
- Difficulties with Unit Testing: Singletons can be challenging to test
because their global state is shared across tests, which can cause side
effects. Mocking or isolating the Singleton in tests is difficult,
especially if the Singleton has complex dependencies or mutable state.
- Risk of Resource Bottlenecks: If multiple components or threads rely
heavily on the Singleton, it can become a bottleneck, as they’re all
waiting for access to the single instance. This can lead to performance
issues, particularly if the Singleton method is synchronized.
- Inflexibility and Reduced Scalability: The Singleton pattern restricts
instantiation, making it difficult to modify or extend the Singleton
behavior. It’s also challenging to transition from a Singleton to a more
scalable solution (e.g., when moving from a single-instance to a multi-
instance configuration for distributed systems).
- Hard to Implement in Multi-threaded Environments: Ensuring that a
Singleton is thread-safe across different platforms and environments
can be challenging. Without proper synchronization, multiple
instances might be created in a multi-threaded application, defeating
the purpose of the Singleton pattern.
- Dependency Injection and Testing Issues: Many modern frameworks
and applications prefer dependency injection for managing
dependencies, as it promotes loose coupling and easier testing.
Singletons, by contrast, are often harder to integrate with dependency
injection, which can make them less suitable for large-scale
applications or frameworks that prioritize DI.
- Risk of Memory Leaks: Singletons can contribute to memory leaks if
they hold onto resources or are not properly garbage-collected. Since
a Singleton exists for the application's lifetime, any resources it holds
may also persist, potentially wasting memory or other resources.
- Encourages Anti-patterns in Design: Since Singletons are globally
accessible, they can encourage anti-patterns by allowing objects to
access the Singleton from anywhere. This can lead to unclear
dependencies and make the code more procedural than object-
oriented.
III. Observer
1. Intent: defines one-to-many dependency between objects so that when
one object changes state, all its dependents (observers) are automatically
notified and updated.
2. Use Cases:
- An abstraction has two interdependent aspects. Separating these
aspects into distinct objects allows them to vary independently.
- Many Dependent Objects: A change to one object requires changing
others, but the exact number or type of dependents isn’t known.
- Loose Coupling: An object needs to notify others of changes without
knowing who these dependents are, enabling a loosely coupled
design.
3. Examples
- Stock Market
Scenario: We are building a simple stock market application. We
want multiple clients (observers) to get notified whenever a stock
price changes.
Subject (Interface or abstract class defining the methods to attach,
detach, and notify observers), ConcreteSubject (The subject that
stores state and sends updates to observers), Observer (Interface or
abstract class defining the update method), ConcreteObserver
(Classes that implement the observer interface and react to updates
from the subjects).
Observer Interface:
interface Observer {
void update(float price);
}
Subject Interface:
interface Subject {
void addObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
@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(price);
}
}
}
@Override
public void update(float price) {
System.out.println("Broker " + name + " notified. New stock
price: $" + price);
}
}
@Override
public void update(float price) {
System.out.println("Investor " + name + " notified. New stock
price: $" + price);
}
}
Client Code:
public class StockMarket {
public static void main(String[] args) {
// Create the subject
Stock stock = new Stock(100);
// Create observers
Broker broker1 = new Broker("Alice");
Investor investor1 = new Investor("Bob");
interface Observer {
void update(float temperature);
}
class WeatherStation {
private List<Observer> observers = new ArrayList<>();
private float temperature;
station.addObserver(display);
station.addObserver(app);
IV. State
1. Intent: allows an object to alter its behavior when its internal state
changes. This pattern provides an efficient way to handle complex
conditional logic based on the state of an object by encapsulating
behavior within different state classes.
2. Use Cases
- An object goes through different states, and its behavior is highly
dependent on its current state.
- The object can transition from one state to another, with each state
having it own behavior. The program’s behavior changes depending
on the state.
3. Examples
- Media Player (Play, Pause, Stop)
Scenario: A media player can be in one of several states, such as
“Play”, “Pause”, or “Stop”. Each state has different behavior.
State Interface:
interface DocumentState {
void publish(DocumentContext context);
void moderate(DocumentContext context);
void archive(DocumentContext context);
}
Concrete States:
class DraftState implements DocumentState {
@Override
public void publish(DocumentContext context) {
System.out.println("Moving from Draft to Moderation.");
context.setState(new ModerationState());
}
@Override
public void moderate(DocumentContext context) {
System.out.println("Cannot moderate a draft. Needs to be
published first.");
}
@Override
public void archive(DocumentContext context) {
System.out.println("Archiving draft.");
context.setState(new ArchivedState());
}
}
@Override
public void moderate(DocumentContext context) {
System.out.println("Document is already in Moderation.");
}
@Override
public void archive(DocumentContext context) {
System.out.println("Archiving document in Moderation.");
context.setState(new ArchivedState());
}
}
@Override
public void moderate(DocumentContext context) {
System.out.println("Cannot moderate a published document.");
}
@Override
public void archive(DocumentContext context) {
System.out.println("Archiving published document.");
context.setState(new ArchivedState());
}
}
@Override
public void moderate(DocumentContext context) {
System.out.println("Cannot moderate an archived document.");
}
@Override
public void archive(DocumentContext context) {
System.out.println("Document is already archived.");
}
}
public DocumentContext() {
state = new DraftState(); // Initial state
}
Client Code:
public class StatePatternDemo {
public static void main(String[] args) {
DocumentContext document = new DocumentContext();
class TrafficLight {
private TrafficLightState state;
public TrafficLight() {
state = new RedState();
}
V. Template Method
1. Intent: defines the skeleton of an algorithm in a base class, allowing
subclasses to redefine certain steps of the algorithm without altering the
overall structure.
2. Use Cases
- You want to let subclasses extend only specific steps of an
algorithm/procedure, without modifying the algorithm’s structure.
- Code reuse is essential, and the algorithm’s structure should remain
consistent across subclasses, with variations only in specific steps.
- You need inversion of control, where the base class dictates the
algorithm’s flow, but subclasses control certain details.
3. Examples
Template (Abstract Class):
abstract class ReportTemplate {
// Template method - defines the skeleton of the algorithm
public final void generateReport() {
fetchData();
processData();
formatReport();
exportReport();
}
Concrete Implementations:
class PDFReport extends ReportTemplate {
@Override
protected void formatReport() {
System.out.println("Formatting report as PDF.");
}
@Override
protected void exportReport() {
System.out.println("Exporting report as PDF.");
}
}
@Override
protected void exportReport() {
System.out.println("Exporting report as Excel file.");
}
}
Client Code:
public class TemplateMethodExample {
public static void main(String[] args) {
ReportTemplate pdfReport = new PDFReport();
pdfReport.generateReport();
System.out.println();
VI. Iterator
1. Intent: provides a way to access the elements of a collection object
sequentially without exposing its underlying representation (like whether
it’s a list, tree, stack, or graph). It separates the traversal logic from the
collection, keeping the traversal code loosely coupled to the collection’s
internal structure.
2. Use Cases
- You want to traverse elements in a collection in a specific order, such
as front-to-back, back-to-front, or using custom traversal (DFS or BFS
for trees).
- You need to access elements of a complex collection (like a tree or
graph) without exposing its underlying structure.
3. Examples
Iterator Interface:
interface Iterator<T> {
boolean hasNext();
T next();
}
Collection Interface:
interface IterableCollection<T> {
Iterator<T> createIterator();
}
@Override
public Iterator<Book> createIterator() {
return new BookIterator();
}
@Override
public boolean hasNext() {
return currentIndex < index;
}
@Override
public Book next() {
return books[currentIndex++];
}
}
}
Client Code:
public class IteratorPatternExample {
public static void main(String[] args) {
BookCollection bookCollection = new BookCollection(3);
bookCollection.addBook(new Book("Design Patterns"));
bookCollection.addBook(new Book("Clean Code"));
bookCollection.addBook(new Book("Refactoring"));
VII. DAO
1. Intent: is used to decouple (tach roi) domain logic from persistence
mechanisms. It provides an interface for accessing data from different
sources, such as databases or flat files, without exposing the details of
these sources to the rest of the application.
2. Use Cases
- The application may need to retrieve data from different types of
sources (SQL databases, NoSQL databases, files), and the DAO
pattern allows you to switch sources without changing business logic.
- When business components need to interact with a data source, the
DAO provides a standard interface, allowing the application to use a
suitable API for connecting and manipulating the data source.
3. Examples
User Entity:
public class User {
private int id;
private String name;
private String email;
DAO Interface:
public interface UserDAO {
void addUser(User user);
User getUser(int id);
void updateUser(User user);
void deleteUser(int id);
List<User> getAllUsers();
}
@Override
public void addUser(User user) {
try {
PreparedStatement stmt = connection.prepareStatement("INSERT
INTO users (id, name, email) VALUES (?, ?, ?)");
stmt.setInt(1, user.getId());
stmt.setString(2, user.getName());
stmt.setString(3, user.getEmail());
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public User getUser(int id) {
User user = null;
try {
PreparedStatement stmt =
connection.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
user = new User(rs.getInt("id"), rs.getString("name"),
rs.getString("email"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return user;
}
@Override
public void updateUser(User user) {
try {
PreparedStatement stmt =
connection.prepareStatement("UPDATE users SET name = ?, email = ?
WHERE id = ?");
stmt.setString(1, user.getName());
stmt.setString(2, user.getEmail());
stmt.setInt(3, user.getId());
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void deleteUser(int id) {
try {
PreparedStatement stmt =
connection.prepareStatement("DELETE FROM users WHERE id = ?");
stmt.setInt(1, id);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public List<User> getAllUsers() {
List<User> users = new ArrayList<>();
try {
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
User user = new User(rs.getInt("id"), rs.getString("name"),
rs.getString("email"));
users.add(user);
}
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}
}
Usage Example:
import java.sql.Connection;
import java.sql.DriverManager;
VIII. Façade
1. Intent: provides a unified interface to a set of interfaces in a subsystem.
It defines a higher-level interface that makes the subsystem easier to use.
Instead of accessing the components of the subsystem directly, clients
interact with the Façade, which simplifies the interactions.
2. Use Cases
- You want to define a single point of access for clients, hiding the
complexity of a subsystem with a single, unified class.
- You want to provide an abstraction that simplifies client code, making
it easier to interact with a complex set of classes.
- You want to reduce coupling (khop noi), so that clients interact with a
simplified interface instead of the details of the subsystem, which
shields them from changes in the subsystem.
3. Examples
- Home Theater System
Scenario: A home theater setup may include multiple components
like a projector, DVD player, speakers, lights, and more.
Controlling each component individually can be complex and
tedious for users.
Façade Use: A HomeTheaterFacade class provides a simplified
interface with methods like watchMovie and endMovie, hiding the
complexity of controlling each component individually.
Subsystem Classes:
class Amplifier {
public void on() { System.out.println("Amplifier on"); }
public void setVolume(int level) { System.out.println("Setting
volume to " + level); }
public void off() { System.out.println("Amplifier off"); }
}
class DvdPlayer {
public void on() { System.out.println("DVD Player on"); }
public void play(String movie) { System.out.println("Playing
movie: " + movie); }
public void stop() { System.out.println("Stopping movie"); }
public void off() { System.out.println("DVD Player off"); }
}
class Projector {
public void on() { System.out.println("Projector on"); }
public void wideScreenMode() { System.out.println("Projector in
widescreen mode"); }
public void off() { System.out.println("Projector off"); }
}
class TheaterLights {
public void dim(int level) { System.out.println("Dimming lights to
" + level + "%"); }
public void on() { System.out.println("Lights on"); }
}
Façade Class:
class HomeTheaterFacade {
private Amplifier amp;
private DvdPlayer dvd;
private Projector projector;
private TheaterLights lights;
Client Code:
public class FacadePatternExample {
public static void main(String[] args) {
Amplifier amp = new Amplifier();
DvdPlayer dvd = new DvdPlayer();
Projector projector = new Projector();
TheaterLights lights = new TheaterLights();
- Banking System
Scenario: A banking system has multiple subsystems for handling
account creation, loans, customer service, and notification. Each
subsystem has its own set of complex operations.
Façade Use: A BankingFacade class provides a simplified interface
for common operations, such as OpenAccount, applyForLoan, or
notifyCustomer.
4. Advantages
- Simplifies Interface for Complex Subsystems: The Facade pattern
provides a straightforward, unified interface to a set of complex
subsystems, making it easier for clients to interact with them. By
hiding complex processes, the Facade reduces the cognitive load on
developers, especially when dealing with intricate systems.
- Promotes Loose Coupling: Facade decouples clients from the internal
details of subsystems, leading to a more modular and maintainable
codebase. This loose coupling allows subsystems to change
independently of the clients, as the Facade shields clients from
internal changes.
- Improves Code Readability and Usability: With a simplified, high-
level interface, the Facade pattern makes code more readable and
usable. It reduces the need for clients to understand intricate
subsystem details and provides a clearer way to perform complex
operations, improving the overall developer experience.
- Reduces Dependency on Subsystems: By centralizing access through
a Facade, the pattern minimizes the number of classes that need to be
directly referenced by the client. This can reduce the risk of
dependency-related issues and simplify dependency management
within the system.
- Eases System Upgrades and Maintenance: Since the Facade shields
clients from the specifics of subsystems, upgrading or modifying
subsystems becomes easier. Developers can update internal
components without impacting the clients, provided the Facade
interface remains consistent.
- Supports Layered Architecture: The Facade pattern is useful in layered
architectures, where it can act as an entry point to different layers of
the application. For instance, in a three-tier architecture, a Facade can
provide a simple interface to business logic, helping to maintain clean
separation between layers.
- Enhances Security by Limiting Access: By exposing only necessary
functions, the Facade pattern can serve as a gatekeeper, allowing
controlled access to subsystems. This helps enforce security by
limiting direct access to sensitive components or complex logic.
- Provides a Centralized Access Point: The Facade pattern provides a
single access point to the subsystems, which can be particularly useful
in applications requiring coordinated access to several components, as
it makes handling dependencies and managing initialization easier.
- Encourages Code Reusability: By centralizing common functions in a
Facade, code reuse is encouraged. The Facade can bundle frequently-
used functionality, which can then be reused across different parts of
the application, reducing duplication.
- Improves Testing and Debugging: Since the Facade offers a simplified
interface, testing and debugging become easier. Developers can test
interactions with the Facade without needing to test each individual
subsystem directly, simplifying test cases and troubleshooting.
5. Disadvantages
- Reduced Flexibility: By providing a simplified interface, the Facade
may hide important functionalities of the subsystems. If clients need
more specific or advanced features, the Facade may not be sufficient,
requiring direct access to the subsystems and potentially undermining
the purpose of the Facade.
- Potential for Over-Simplification: In trying to provide a unified,
simplified interface, the Facade can sometimes over-simplify
interactions with the subsystems, restricting access to useful
capabilities or making it harder to perform complex operations that
don't fit the Facade's design.
- Increased Maintenance with Changes in Subsystems: If subsystems
change frequently, the Facade may require continuous updates to
accommodate these changes. While it decouples clients from the
subsystem details, maintaining this abstraction layer can introduce
additional maintenance overhead, especially in rapidly evolving
systems.
- Risk of Becoming a “God Object”: When the Facade tries to offer
extensive functionality to cater to diverse client needs, it risks
becoming overly complex itself. This can lead to a “God Object” that
becomes a bottleneck, handling too much responsibility and making
the code harder to maintain and modify.
- Performance Overhead: The Facade adds an additional layer between
the client and the subsystem, which can introduce performance
overhead, especially if it consolidates multiple subsystem calls. In
performance-critical applications, this additional layer may affect
system responsiveness.
- Encourages Over-Reliance on the Facade: Since the Facade is meant
to simplify access, developers may over-rely on it, even for tasks
better suited to direct interaction with subsystems. This can lead to
inefficient code if the Facade handles tasks that would be more
effective through direct access to the subsystem.
- Potential for Tight Coupling with Subsystems: The Facade may
unintentionally become tightly coupled with subsystems if it relies
heavily on their specific behaviors. This can make the Facade more
challenging to refactor or reuse if the underlying subsystems need to
be replaced or modified significantly.
- Limited Scalability in Large Systems: For complex systems with
numerous subsystems, a single Facade might struggle to offer
sufficient abstraction and may require multiple facades or nested
facades, increasing the design complexity and making it harder to
scale.
- Difficulties in Unit Testing: While the Facade simplifies the interface,
it can sometimes make unit testing more complex if it heavily depends
on the functionality of underlying subsystems. Testing the Facade’s
functionality might require initializing or mocking various
subsystems, making the test setup more cumbersome.
- Can Mask Inefficiencies or Bugs in Subsystems: By hiding subsystem
details, the Facade can inadvertently mask inefficiencies, bugs, or
poor design within subsystems. This can make debugging more
difficult, as issues within the subsystems may not become apparent
until they cause broader problems in the application.