0% found this document useful (0 votes)
13 views48 pages

DESIGN PATTERNS

The document discusses design patterns, focusing on the Factory Method, Singleton, and Observer patterns. It outlines the intent, use cases, advantages, and disadvantages of each pattern, providing examples such as document creation in office applications for the Factory Method and database connections for the Singleton. The document emphasizes the importance of these patterns in promoting clean, maintainable, and scalable code while also addressing potential complexities and challenges in implementation.

Uploaded by

Giang
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
13 views48 pages

DESIGN PATTERNS

The document discusses design patterns, focusing on the Factory Method, Singleton, and Observer patterns. It outlines the intent, use cases, advantages, and disadvantages of each pattern, providing examples such as document creation in office applications for the Factory Method and database connections for the Singleton. The document emphasizes the importance of these patterns in promoting clean, maintainable, and scalable code while also addressing potential complexities and challenges in implementation.

Uploaded by

Giang
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 48

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.

(Abstract class/ Interface)


interface Document {
void open();
void save();
}

Implement Concrete Document Classes:


class WordDocument implements Document {
@Override
public void open() {
System.out.println("Opening a Word document.");
}
@Override
public void save() {
System.out.println("Saving a Word document.");
}
}

class ExcelDocument implements Document {


@Override
public void open() {
System.out.println("Opening an Excel spreadsheet.");
}

@Override
public void save() {
System.out.println("Saving an Excel spreadsheet.");
}
}

class PowerPointDocument implements Document {


@Override
public void open() {
System.out.println("Opening a PowerPoint presentation.");
}

@Override
public void save() {
System.out.println("Saving a PowerPoint presentation.");
}
}

Define the DocumentCreator abstract class:


abstract class DocumentCreator {
public abstract Document createDocument();
}

Implement Concrete Creator Classes:


class WordDocumentCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new WordDocument();
}
}

class ExcelDocumentCreator extends DocumentCreator {


@Override
public Document createDocument() {
return new ExcelDocument();
}
}

class PowerPointDocumentCreator extends DocumentCreator {


@Override
public Document createDocument() {
return new PowerPointDocument();
}
}

Client code:
public class Main {
public static void main(String[] args) {
DocumentCreator wordCreator = new WordDocumentCreator();
Document wordDocument = wordCreator.createDocument();
wordDocument.open();
wordDocument.save();

DocumentCreator excelCreator = new ExcelDocumentCreator();


Document excelDocument = excelCreator.createDocument();
excelDocument.open();
excelDocument.save();

DocumentCreator pptCreator = new


PowerPointDocumentCreator();
Document pptDocument = pptCreator.createDocument();
pptDocument.open();
pptDocument.save();
}
}

- GUI Frameworks for Cross-Platform Applications


 Scenario: A cross-platform GUI application might need different
UI components (buttons, text fields) for different operating
systems (Windows, Mac, …)
 Factory Method Use: An abstract ButtonFactory defines a
createButton() method. Each platform-specific subclass
(WindowButtonFactory, MacButtonFactory) implements
createButton() to produce the appropriate button for that operating
system.
 Benefit: The main application can create buttons without worrying
about the specific OS.
- Logistics and Transportation
 Scenario: A logistics application deals with various type of
transportation like trucks, ships, and planes.
 Factory Method Use: A Logistics class might have an abstract
method, createTransport(). Subclasses like TruckLogistics,
ShipLogistics implement createTransport() to return an appropriate
transport type.
- Notification System
 Scenario: A messaging system needs to send different types of
notifications-such as SMS, email, and push notifications.
 Factory Method Use: A NotificationFactory defines a
createNotification() method. Subclasses like
SMSNotificationFactory, EmailNotificationFactory, and
PushNotificationFactory each implement this method to create the
correct type of notification.
- Game Character Creation
 In a game, there are different types of characters with unique
abilities and attributes.
 Factory Method Use: A CharacterFactory has an abstract
createCharacter() method. Each subclass, such as WarriorFactory,
MageFactory and ArcherFactory implements this method to create
the appropriate character.
4. Advantages
- Encapsulation of Object Creation: The Factory Method centralizes the
process of object creation, so if the instantiation process changes, only
the factory method needs updating, not the code throughout the
application. This promotes clean, organized, and maintainable code.
- Enhanced Flexibility and Scalability: It allows subclasses to choose
the type of objects to create. This is especially useful in applications
where new types or variations of objects might be added in the future,
as you can extend the factory without modifying existing code.
- Improved Code Reusability: The factory method can be reused across
different parts of the application, reducing code duplication. Instead of
repeatedly writing object creation logic, you can call the factory
method, simplifying the codebase.
- Supports Open/Closed Principle: The Factory Method pattern aligns
well with the open/closed principle (one of SOLID principles), as the
code can be extended to create new types of objects without altering
the existing code, making it ideal for scenarios where classes or object
types frequently change.
- Simplifies Testing: Since the factory method abstracts the instantiation
process, it makes the code easier to test. For instance, you can use
mocks or stubs within the factory, allowing you to control and isolate
specific test scenarios without affecting other code areas.
- Promotes Loose Coupling: By depending on an interface or abstract
class, rather than a concrete class, the Factory Method pattern reduces
the dependency between code that requires objects and the specific
classes of objects being created, enabling better modularity.
5. Disadvantages
- Increased Complexity: Using the Factory Method pattern introduces
additional classes and methods into the codebase, which can make it
more complex and harder to understand. This can be a downside in
smaller applications where the benefits of a factory might not justify
the added complexity.
- Reduced Readability: The abstraction layer added by the Factory
Method can obscure the logic of which concrete classes are being
instantiated, making the code less intuitive for developers unfamiliar
with the pattern.
- Maintenance Overhead: Because the Factory Method relies on
inheritance and often involves multiple classes, it can be more
challenging to maintain. Any change in the instantiation logic may
require modifications in the factory class and potentially its
subclasses, adding to maintenance work.
- Can Lead to Class Proliferation: The Factory Method pattern can
result in a large number of factory classes or subclasses, especially if
there are many different types of objects to be created. This can clutter
the codebase, making it harder to manage and navigate.
- Difficulties with Testing: If not designed carefully, Factory Methods
can make testing more challenging. Since the factory pattern often
relies on interfaces or abstract classes, mocking or stubbing these
dependencies can add complexity to unit testing.
- Less Flexibility for New Parameters: If additional parameters are
needed in object creation, each factory may need to be modified to
accept and handle those new parameters, which can complicate the
codebase and reduce flexibility over time.
- Inappropriate Use in Simple Applications: In simpler applications or
when the object creation logic isn’t complex, the Factory Method
pattern can be overkill. It may introduce unnecessary complexity
when simple instantiation would suffice.
- Dependency on Subclasses: The Factory Method pattern relies on
subclasses to define which objects to create, which can introduce tight
coupling to specific subclasses. This coupling can be problematic if
the class hierarchy changes or if new object types need to be
supported.

II. Singleton (GoF)


1. Intent
- A class has only one instance
- It provides a global access point to that instance.
2. Usecase
- There must be exactly one instance of a class, and it must be
accessible to clients from a well-known access point.
- The single instance should be extensible by subclassing, allowing
clients to use an extended version without modifying their code.
3. Examples
Singleton Class
class SingletonObject {
// Step 1: Create a private static instance of the Singleton class
private static SingletonObject instance;

// Step 2: Make the constructor private to prevent instantiation from


other classes
private SingletonObject() {
// Initialize resources or configuration settings here if needed
}

// 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;
}

// Example method to demonstrate the functionality of the Singleton


class
public void showMessage() {
System.out.println("Hello from the Singleton instance!");
}
}

Client Code (Main Method)


public class SingletonPatternDemo {
public static void main(String[] args) {
// Step 4: Get the Singleton instance and call a method on it
SingletonObject singletonInstance = SingletonObject.getInstance();
singletonInstance.showMessage();

// Demonstrating that the same instance is returned each time


SingletonObject anotherInstance = SingletonObject.getInstance();
System.out.println("Are both instances the same? " +
(singletonInstance == anotherInstance));
}
}

- 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();
}

Concrete Subject (Stock):


class Stock implements Subject {
private List<Observer> observers;
private float price;

public Stock(float price) {


observers = new ArrayList<>();
this.price = price;
}

public void setPrice(float price) {


this.price = price;
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);
}
}
}

Concrete Observers (e.g., Broker and Investor):


class Broker implements Observer {
private String name;

public Broker(String name) {


this.name = name;
}

@Override
public void update(float price) {
System.out.println("Broker " + name + " notified. New stock
price: $" + price);
}
}

class Investor implements Observer {


private String name;

public Investor(String name) {


this.name = name;
}

@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");

// Attach observers to the subject


stock.addObserver(broker1);
stock.addObserver(investor1);

// Change the stock price and notify observers


stock.setPrice(105); // Alice and Bob are notified
stock.setPrice(110); // Alice and Bob are notified again

// Remove an observer and change the stock price


stock.removeObserver(broker1);
stock.setPrice(120); // Only Bob is notified
}
}

- GUI Event Listeners


 In graphical user interfaces, various components (buttons, sliders,
text fields) need to respond to user actions, such as clicks or text
input.
 Observer use: When a user interacts with a GUI component, the
component notifies all listeners (observers) registered for that
event. (Listener in the app)
- News Feed or Social Media Notifications
 Scenario: Social media platforms notify users when someone they
follow posts new content.
 Observer Use: Each user (observer) subscribes to other users’
(subjects’) feeds. When a subject posts an update, all observers are
notified.
 Benefit: Users can follow or unfollow other users independently,
and the platform does not need to know who the followers are; it
simply notifies all registered observers.
- Stock Market Watchers
 Scenario: Stock prices are constantly changing, and various
brokers, investors, and financial applications need to stay updated
on these changes.
 Observer Use: Investors or brokers register to watch a stock. When
the stock price changes, all registered observers are notified of the
new price.
- Weather Station:
import java.util.ArrayList;
import java.util.List;

interface Observer {
void update(float temperature);
}

class TemperatureDisplay implements Observer {


public void update(float temperature) {
System.out.println("Temperature updated on display: " +
temperature);
}
}

class MobileApp implements Observer {


public void update(float temperature) {
System.out.println("Temperature updated on mobile app: " +
temperature);
}
}

class WeatherStation {
private List<Observer> observers = new ArrayList<>();
private float temperature;

public void addObserver(Observer observer) {


observers.add(observer);
}

public void removeObserver(Observer observer) {


observers.remove(observer);
}
public void setTemperature(float temperature) {
this.temperature = temperature;
notifyObservers();
}

private void notifyObservers() {


for (Observer observer : observers) {
observer.update(temperature);
}
}
}

public class ObserverPatternExample {


public static void main(String[] args) {
WeatherStation station = new WeatherStation();
TemperatureDisplay display = new TemperatureDisplay();
MobileApp app = new MobileApp();

station.addObserver(display);
station.addObserver(app);

station.setTemperature(25.0f); // Both display and app are


notified
station.setTemperature(30.0f); // Both display and app are
notified again
}
}
4. Advantages
- Loose Coupling: Observers and subjects are loosely coupled, meaning
the subject doesn't need to know the specific details of the observers
(like their classes or functions). This allows for greater modularity, as
observers can be added, removed, or modified without impacting the
subject.
- Promotes Code Reusability and Flexibility: Observers can be reused
across different subjects and projects, as they’re independent of the
subject's logic. This flexibility makes the Observer pattern useful in
scenarios where multiple components need to react to the same data or
event without requiring specialized modifications.
- Efficient Communication and Real-Time Updates: The Observer
pattern enables real-time communication between the subject and its
observers. When the subject’s state changes, all registered observers
are automatically notified, making it suitable for applications that
require live updates, like user interfaces, event-driven systems, or data
feeds.
- Supports Open/Closed Principle: Observers can be added or removed
without modifying the subject class itself, which aligns well with the
open/closed principle in software design. This extensibility allows
developers to add new observers for new behaviors without changing
existing code, enhancing maintainability.
- Dynamic Relationships: The pattern allows dynamic registration and
deregistration of observers. Observers can choose to start or stop
observing a subject at any time, providing flexibility in how
components interact based on specific needs.
- Improved Scalability for Multi-Component Systems: Observer is
particularly helpful in systems with multiple components that need to
stay synchronized or react to certain changes. Since the pattern allows
multiple observers to subscribe to a subject, it easily supports a many-
to-many relationship between components, which enhances
scalability.
- Simplifies Code: By centralizing update logic within the subject, the
Observer pattern reduces the need for each observer to poll the subject
for changes. This simplifies code and improves efficiency, as
observers are only updated when necessary.
5. Disadvantages
- Potential for Memory Leaks: If observers are not correctly
unregistered from the subject, they can persist in memory, causing
memory leaks. This is especially common in languages without
automatic garbage collection, where developers must manage
observer subscriptions manually.
- Increased Complexity: The Observer pattern can add complexity to
the codebase, particularly in systems with a large number of
observers. Managing subscriptions, notifications, and potential
chaining of observers can complicate debugging and maintenance.
- Unexpected Behavior from Cascading Updates: If observers update
the subject as part of their notification, it can create a cascade of
updates, where one change triggers multiple additional notifications.
This can lead to unpredictable behavior and bugs if not carefully
managed, particularly in systems where multiple observers depend on
each other’s updates.
- Tight Coupling to Notifications: Although the Observer pattern
promotes loose coupling in terms of object dependencies, it creates a
reliance on notification events. Observers depend on the subject’s
notifications to remain up-to-date, which can be problematic if the
notification frequency is too high or if notifications aren’t delivered as
expected.
- Difficulty in Controlling the Notification Order: With multiple
observers, the order in which they receive notifications can sometimes
be unpredictable. This can lead to issues if certain observers need to
be updated before others, requiring additional logic to manage the
notification order.
- Performance Overhead: In applications with many observers or
frequent updates, notifying all observers can create significant
overhead, impacting performance. Each change in the subject could
result in numerous updates, which can slow down the system,
particularly in real-time applications.
- Hidden Dependencies and Indirect Communication: The Observer
pattern can introduce hidden dependencies, as the relationship
between subjects and observers isn’t always obvious in the code. This
indirect communication can make it harder to trace the source of
certain behaviors or debug issues, especially in large applications with
many observers.
- Complexity in Multi-threaded Environments: In multi-threaded
applications, handling observer notifications can become complex, as
simultaneous updates to observers or the subject can result in race
conditions, deadlocks, or inconsistent states.
-

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 Pattern Use: Each state is represented as a separate class


implementing the MediaPlayerState interface. The media player
context changes behavior based on its current state.
- Document Publishing Workflow (Draft, Moderation, Published,
Archived)
 Scenario: A document in a content management system can go
through states like “Draft”,… Each state determines what actions
can be performed on the document.
 State Pattern Use: Each document state is represented as a separate
class implementing the DocumentState interface. The document
context changes behavior based on its current state.

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());
}
}

class ModerationState implements DocumentState {


@Override
public void publish(DocumentContext context) {
System.out.println("Publishing document.");
context.setState(new PublishedState());
}

@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());
}
}

class PublishedState implements DocumentState {


@Override
public void publish(DocumentContext context) {
System.out.println("Document is already published.");
}

@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());
}
}

class ArchivedState implements DocumentState {


@Override
public void publish(DocumentContext context) {
System.out.println("Cannot publish an archived document.");
}

@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.");
}
}

Context Class (DocumentContext):


class DocumentContext {
private DocumentState state;

public DocumentContext() {
state = new DraftState(); // Initial state
}

public void setState(DocumentState state) {


this.state = state;
}

public void publish() {


state.publish(this);
}
public void moderate() {
state.moderate(this);
}

public void archive() {


state.archive(this);
}
}

Client Code:
public class StatePatternDemo {
public static void main(String[] args) {
DocumentContext document = new DocumentContext();

document.publish(); // Moves from Draft to Moderation


document.moderate(); // Cannot moderate in moderation, needs
to publish first
document.publish(); // Moves from Moderation to Published
document.archive(); // Archives published document
document.publish(); // Cannot publish archived document
}
}

- ATM Machine (Card Inserted, No Card, Correct PIN, Incoorect PIN)


 Scencario: And ATM machine can have different states based on
user actions.
 State Pattern Use: Each ATM state is represented as a separate
class implementing the ATMState interface, and the ATM context
changes behavior based on the current state.
- Order Processing System (Oder Placed, Shipped, Delivered,
Cancelled)
- Traffic lights:
interface TrafficLightState {
void next(TrafficLight context);
}

class RedState implements TrafficLightState {


public void next(TrafficLight context) {
System.out.println("Switching to Green.");
context.setState(new GreenState());
}
}

class GreenState implements TrafficLightState {


public void next(TrafficLight context) {
System.out.println("Switching to Yellow.");
context.setState(new YellowState());
}
}

class YellowState implements TrafficLightState {


public void next(TrafficLight context) {
System.out.println("Switching to Red.");
context.setState(new RedState());
}
}

class TrafficLight {
private TrafficLightState state;

public TrafficLight() {
state = new RedState();
}

public void setState(TrafficLightState state) {


this.state = state;
}

public void next() {


state.next(this);
}
}

public class StatePatternExample {


public static void main(String[] args) {
TrafficLight light = new TrafficLight();
light.next(); // Red to Green
light.next(); // Green to Yellow
light.next(); // Yellow to Red
}
}
4. Advantages
- Encapsulates State-Specific Behavior: The State pattern allows each
state to have its own behavior, encapsulated in separate classes. This
keeps state-specific logic organized and isolated, making it easier to
manage and extend.
- Simplifies Complex Conditionals: By using different classes for
different states, the State pattern can eliminate complex conditional
statements (like if-else or switch cases) within the main object.
Instead, state transitions and behaviors are handled by the state
classes, simplifying the main object’s code.
- Promotes the Open/Closed Principle: The State pattern aligns with the
open/closed principle, as new states can be added by creating new
state classes without modifying existing ones. This makes the code
more maintainable and extensible as requirements evolve.
- Improves Code Readability and Maintainability: Each state’s behavior
is contained within its own class, making the code more modular and
easier to read and maintain. This is particularly useful in applications
with multiple, complex states that may otherwise be hard to track
within a single class.
- Enables Reusability: Individual state classes can often be reused
across different parts of an application or in other applications with
similar state requirements, especially if the behaviors are generic.
- Encourages Object-Oriented Design Principles: The State pattern
takes advantage of polymorphism by allowing different state objects
to define their own behaviors. This leverages object-oriented
principles like inheritance and composition, leading to a more flexible
and structured design.
- Supports Dynamic State Transitions: The State pattern allows for
dynamic state changes at runtime. This flexibility is useful in
applications where state transitions aren’t strictly defined and may
vary depending on user actions or other conditions.
- Easier to Test and Debug: Since each state has its own class and
specific behavior, testing and debugging are simpler. Developers can
isolate and test individual states without worrying about the impact on
other states.
5. Disadvantages
- Increased Number of Classes: The State pattern requires creating
separate classes for each state, which can lead to class proliferation,
especially in complex systems with many states. This can make the
codebase harder to navigate and maintain.
- Higher Complexity: While it helps organize state-specific behaviors,
the State pattern can introduce complexity by adding layers of
abstraction. For simple scenarios with only a few states, this added
complexity might be unnecessary and over-engineered.
- Tight Coupling to Concrete State Classes: The context (main object) is
often closely tied to specific state classes, which can make it
challenging to change or replace states dynamically. If the context
heavily relies on the internal workings of each state, modifying states
may require changes in the context as well, reducing flexibility.
- Difficulty in Managing State Transitions: Complex state transitions
can be hard to manage, particularly if there are many possible
transitions or conditions for moving between states. The State pattern
does not inherently solve transition logic, so the developer needs to
handle it carefully to avoid incorrect transitions.
- Increased Memory Usage: Each state is typically represented by a
separate class instance, which may increase memory usage,
particularly if there are many states or if the state instances hold a
significant amount of data. In resource-constrained environments, this
can be an issue.
- Requires Good Understanding of Object-Oriented Principles:
Implementing the State pattern effectively requires a solid
understanding of object-oriented principles, such as polymorphism,
inheritance, and encapsulation. Misusing or misunderstanding these
principles can result in poorly implemented states that don’t fully
leverage the pattern’s benefits.
- Potential for Inconsistent Behavior: If state transitions aren’t carefully
managed, there’s a risk of leaving the object in an invalid or
inconsistent state. This can lead to bugs or unexpected behavior,
particularly in complex systems with many interdependent states.
-
- Not Always Suitable for Simple Systems: In simpler applications
where the behavior change based on state is minimal, using the State
pattern can be overkill, adding unnecessary complexity without much
benefit.

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();
}

// Common steps for all report types


protected void fetchData() {
System.out.println("Fetching data for the report.");
}

protected void processData() {


System.out.println("Processing data for the report.");
}

// Steps that need to be implemented by subclasses


protected abstract void formatReport();
protected abstract void 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.");
}
}

class ExcelReport extends ReportTemplate {


@Override
protected void formatReport() {
System.out.println("Formatting report as Excel.");
}

@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();

ReportTemplate excelReport = new ExcelReport();


excelReport.generateReport();
}
}

- Report Generation (PDF, Excel, HTML)


 Scenario: A reporting system generates different types of reports,
such as PDF, Excel, and HTML. Each report type has a similar
structure: fetching data, processing data, formatting the report, and
exporting it.
 Template Method Use: The base class defines the structure of
report generation, and each concrete class (PDFReport,
ExcelReport,…) customizes the formatting and exporting steps.
- Data Processing Pipeline (ETL – Extract, Transform, Load)
 Scenario: An ETL (extract, transform, load) pipeline in data
processing extracts data from various sources, transforms it, and
then loads it into a database or data warehouse.
 Template Method Use: The DataPipeline abstract class defines the
skeleton with steps for extracting, transforming, and loading. Each
subclass customizes how data is extracted or transformed.
- Game AI (Behavior for Different Enemy Types)
 Scenario: In a game, different enemy types (zombies, ghosts,
robots) follow a similar behavior routine like patrol, chase, attack,
and die. Each enemy type implements specific actions but follows
the same sequence.
 Template Method Use: The base class EnemyAI defines the
structure for enemy actions, and each enemy type customizes the
step based on its unique behavior.
- Cooking Recipe Template (Different Dish Variations)
 Scenario: A recipe application allows chefs to follow a
standardized recipe structure (prepare ingredients, cook, and
serve), but different dishes have unique implementations for each
step.
 Template Method Use: The RecipeTemplate class defines the
general steps of cooking a dish, and subclasses like PastaRecipe or
SaladRecipe implement the steps in their own way.
- Authentication Process (Different Types of Users)
 Scenario: An application has an authentication process with steps
like “validate credentials”, “check permissions”, and “set up
session”. Different types of users (Admin, User, Guest) have
similar authentication steps but different checks and permissions.
 Template Method Use: The AuthTemplate class defines the basic
steps, while subclasses provide specific checks and permissions for
each user type.
4. Advantages
- Code Reusability: By defining the overall structure of an algorithm in
a superclass and allowing subclasses to fill in the details, the Template
Method pattern promotes code reuse. This reduces duplication, as
common code is centralized in the superclass.
- Simplifies Code Maintenance: Since the core logic is in one place, any
changes to the overarching algorithm can be made in the superclass,
reducing the need to modify each subclass individually. This
simplifies code maintenance and reduces the risk of inconsistencies.
- Promotes the Open/Closed Principle: The Template Method pattern
aligns with the open/closed principle by allowing subclasses to extend
or override specific steps without modifying the structure in the
superclass. This promotes extensibility and makes the code easier to
adapt to new requirements.
- Encapsulates Invariant Parts of the Algorithm: The Template Method
pattern allows you to keep the invariant parts of an algorithm in the
superclass while enabling customization of variable parts in
subclasses. This ensures that only the necessary parts of the algorithm
are altered while preserving the overall structure.
- Improves Code Readability and Organization: By using the Template
Method pattern, the high-level structure of an algorithm is easier to
understand, as it’s all defined in the superclass. This makes the code
more organized and readable, especially when dealing with complex
processes that have multiple variations.
- Encourages Consistency Across Implementations: Since subclasses
must follow the template structure, this pattern ensures a consistent
sequence of operations across different implementations. This can be
helpful when you need uniformity, such as in reporting, data
processing, or workflow management.
- Reduces Risk of Errors: By defining the core structure of an algorithm
in a single place, the Template Method pattern reduces the chance of
errors that might arise if each subclass were to implement the full
algorithm independently. Developers only need to focus on
implementing specific parts of the algorithm, which lowers the risk of
mistakes.
- Encourages Use of Hook Methods: Template Methods can use “hook”
methods, which allow subclasses to optionally override specific steps
or add functionality without requiring them to implement every part.
This gives developers flexibility in customizing behavior without fully
overriding the superclass’s logic.
5. Disadvantages
- Increased Complexity: The Template Method pattern can add
complexity to a codebase, especially when overused or applied to
simpler tasks that don’t need such an elaborate structure. This extra
layer of abstraction can make the code harder to understand for
developers unfamiliar with the pattern.
- Difficulty in Maintaining Inheritance Chains: Since the pattern relies
on inheritance, the Template Method pattern can lead to deep
inheritance chains if overused. Deep inheritance chains make the code
more complex and harder to maintain, as changes to the superclass
may unintentionally impact multiple subclasses.
- Limited Flexibility Due to Fixed Structure: The Template Method
enforces a specific algorithm structure in the superclass, which can be
restrictive if subclasses require a different sequence or additional steps
that the template does not anticipate. Adapting the template to fit
varying needs can lead to clunky or forced implementations.
- Risk of Breaking the Algorithm: Subclasses must override certain
methods to implement their behavior, but there’s a risk that they might
override methods incorrectly or alter the algorithm’s structure
unintentionally. This can break the flow of the algorithm, leading to
inconsistent or unexpected behavior.
- Challenging to Understand for New Developers: Since the template’s
structure is defined in one class and implemented in another, it can be
difficult for new developers to follow the logic. Understanding how
the overall algorithm works requires knowledge of both the superclass
and its subclasses, which can make onboarding more challenging.
- Inheritance-Based Limitation: The Template Method pattern relies on
inheritance, which limits flexibility compared to composition-based
approaches. This can make it harder to apply the pattern if a class
already has a superclass, as many languages only allow single
inheritance, potentially leading to code duplication or workarounds.
- Difficulties in Testing: Since the Template Method involves multiple
classes (superclass and subclasses), testing the full functionality may
require setting up various subclasses to verify that the entire algorithm
operates correctly. This can make testing more complex, particularly if
different subclasses behave differently under the template.
- Potential for Overriding Issues: Subclasses might need to override
only certain parts of the template, but the superclass might not
anticipate this need. This can lead to subclasses unnecessarily
implementing methods or potentially causing issues if the template
changes in ways that affect these overrides.

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();
}

Concrete Collection (BookCollection):


class Book {
private String title;

public Book(String title) {


this.title = title;
}

public String getTitle() {


return title;
}
}

class BookCollection implements IterableCollection<Book> {


private Book[] books;
private int index = 0;
public BookCollection(int size) {
books = new Book[size];
}

public void addBook(Book book) {


if (index < books.length) {
books[index++] = book;
}
}

@Override
public Iterator<Book> createIterator() {
return new BookIterator();
}

private class BookIterator implements Iterator<Book> {


private int currentIndex = 0;

@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"));

Iterator<Book> iterator = bookCollection.createIterator();


while (iterator.hasNext()) {
Book book = iterator.next();
System.out.println("Book: " + book.getTitle());
}
}
}

- File System Browsing


 Scenario: A file explorer application needs to traverse files and
directories in a filesystem. The structure might be complex,
involving directories within directories.
 Iterator Use: A FileIterator can traverse through the files and
folders without exposing the internal details of the filesystem.
- Social Media Feed Scrolling
 Scenario: A social media app displays posts as the user scrolls
through their feed. The feed can include different types of content
(photos, videos, links) and posts might be stored in a complex data
structure.
 Iterator Use: A FeefIterator can provide access to posts one at a
time, regardless of how they’re stored in the backend.
- Tree Traversal (Binary Search Tree)
 Scenario: A binary search tree (BST) holds data, and you want to
traverse it in different orders (in-order, pre-order, post-order).
 Iterator Use: Implement an Iterator for each traversal order, like
InOrderIterator, PreOderIterator, and PostOrderIterator.
- Aggregate Collection (Playlist of Songs)
 Scenario: A music player has playlists of songs. Each playlist may
have a different structure (an array for one playlist, a linked list for
another).
 Iterator Use: Implement a SongIterator that iterates over the songs,
hiding the underlying collection structure.
- Graph Traversal (Breadth-First or Depth-First)
 Scenario: In graph processing, you may need to traverse nodes
either in breadth-first or depth-first order.
 Iterator Use: Implement different iterators like BreadthFristIterator
or DepthFirstIterator to handle the traversal order, regardless of
how nodes and edges are stored.
4. Advantages
- Encapsulates Collection Structure: The Iterator pattern hides the
internal representation of a collection, allowing elements to be
accessed sequentially without revealing the collection's underlying
structure. This promotes encapsulation and reduces dependency on
specific data structures.
- Simplifies Code: By providing a standard way to traverse elements,
the Iterator pattern makes it easier to write and read code that works
with collections. This is particularly useful for complex collections
where manual traversal would be error-prone or cumbersome.
- Supports Multiple Traversals: The Iterator pattern allows multiple
iterators to be created and used on the same collection simultaneously.
This enables different parts of the application to traverse a collection
independently without interfering with each other.
- Promotes Single Responsibility Principle: By separating traversal
logic from the collection itself, the Iterator pattern adheres to the
Single Responsibility Principle. The collection class focuses on
managing data, while the iterator handles the traversal, leading to
cleaner, more modular code.
- Flexible and Consistent Access: Iterators provide a uniform interface
for accessing elements across different types of collections, whether
arrays, lists, trees, or custom structures. This consistency allows
algorithms to operate on various collections without needing to know
the specific details of each one.
- Supports Polymorphic Iteration: The Iterator pattern allows different
collections to be iterated in a polymorphic way. For example, a single
algorithm can work with multiple types of collections, as long as they
all provide an iterator interface, increasing code flexibility.
- Facilitates Extension: The Iterator pattern makes it easier to add new
traversal strategies or iterate through new collection types without
altering the collection or existing code. This can be useful for adding
custom iteration orders or filters.
- Enables Safe Access to Collections: Iterators can provide safe access
to collections by restricting direct modification of elements during
traversal. In some implementations, they even support concurrent
modifications, alerting the user if the collection is modified during
iteration, which can prevent bugs.
- Improves Testability: With iteration logic encapsulated in separate
iterator classes, it becomes easier to test traversal independently. You
can validate the iterator’s behavior without needing to test the entire
collection class, improving modularity in testing.
5. Disadvantages
- Increased Complexity: Implementing an iterator can add complexity
to the codebase, especially when dealing with custom or complex
collections. Creating additional iterator classes or methods may feel
like over-engineering for simple collections.
- Performance Overhead: In some languages or frameworks, iterators
may add overhead due to object creation or the need to maintain the
iterator’s state. This can be an issue in performance-sensitive
applications, particularly if there are a large number of elements or
nested iterations.
- Limited to Sequential Access: The Iterator pattern generally supports
sequential access only, which may not be efficient for collections that
allow direct access (e.g., arrays or lists). For collections where random
access is required, iterators might feel restrictive and inefficient.
- Lack of Flexibility in Complex Traversal: Basic iterators often
provide only a single way to traverse a collection (typically forward).
For collections that support complex traversal patterns (like skipping
elements, reverse iteration, or bi-directional traversal), a standard
iterator may be insufficient, requiring additional custom iterators.
- Concurrent Modification Issues: Iterators can be sensitive to
concurrent modifications of the collection. In some implementations,
if the underlying collection is modified while it’s being iterated, it
may throw errors (e.g., ConcurrentModificationException in Java) or
produce unexpected results. Ensuring safe concurrent access can
complicate the iterator’s implementation.
- Tight Coupling in Some Implementations: The Iterator pattern can
sometimes lead to tight coupling between the collection and the
iterator, particularly if the iterator needs access to the collection’s
internal details. This can make it harder to change the collection’s
internal structure without also modifying the iterator.
- Potential for Memory Leaks: In certain cases, especially in languages
without automatic garbage collection, iterators can cause memory
leaks if they hold references to elements in a collection that’s no
longer needed. This can be problematic in long-lived iterators or when
iterators are not cleaned up properly.
- May Encourage Procedural-Style Code: The Iterator pattern can
sometimes lead to a more procedural programming style, as it focuses
on sequential processing of elements. This can be less compatible with
functional or declarative programming paradigms, which provide
different means of accessing and transforming collections.
- Can Make Debugging Harder: Iterators abstract the underlying data
structure, which can make debugging challenging. Identifying the
source of issues during iteration may require understanding both the
iterator’s logic and the collection’s internal implementation,
particularly when custom iterators are used.

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;

public User(int id, String name, String email) {


this.id = id;
this.name = name;
this.email = email;
}

public int getId() {


return id;
}

public String getName() {


return name;
}

public String getEmail() {


return 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();
}

Concrete DAO Implementation (Database):


import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class UserDAOImpl implements UserDAO {


private Connection connection;

public UserDAOImpl(Connection connection) {


this.connection = connection;
}

@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;
}
}

Client Code (Using the DAO):


public class UserService {
private UserDAO userDAO;

public UserService(UserDAO userDAO) {


this.userDAO = userDAO;
}

public void createUser(int id, String name, String email) {


User user = new User(id, name, email);
userDAO.addUser(user);
}

public User getUserById(int id) {


return userDAO.getUser(id);
}

public void updateUser(User user) {


userDAO.updateUser(user);
}

public void deleteUser(int id) {


userDAO.deleteUser(id);
}

public List<User> listAllUsers() {


return userDAO.getAllUsers();
}
}

Usage Example:
import java.sql.Connection;
import java.sql.DriverManager;

public class Main {


public static void main(String[] args) {
try {
Connection connection =
DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase"
, "username", "password");
UserDAO userDAO = new UserDAOImpl(connection);
UserService userService = new UserService(userDAO);

userService.createUser(1, "John Doe",


"[email protected]");
User user = userService.getUserById(1);
System.out.println("User: " + user.getName() + ", Email: " +
user.getEmail());
} catch (Exception e) {
e.printStackTrace();
}
}
}

- User Management System


 Scenario: In a user management system, the application handles
operations like creating, updating, retrieving, and deleting users.
The users could be stored in a database.
 DAO Use: A UserDAO interface defines methods like addUser,
getUser, updateUser, and deleteUser. Different implementations of
this interface can store users in different types of database.
- Product Catalog in an E-commerce System
 Scenario: An e-commerce platform needs to manage a catalog of
products stored in database. Operations include adding, updating,
retrieving, and deleting products.
 DAO Use: A ProductDAO interface with methods such as
addProduct, getProduct, updateProduct, and deleteProduct
decouples the data access from the business logic. Different
implementations can be created for different databases or data
sources.
4. Advantages
- Encapsulation of Data Access Logic: The DAO pattern separates data
access logic from business logic, keeping database operations (such as
queries, updates, inserts, and deletes) encapsulated within DAO
classes. This results in cleaner code and allows business logic to focus
solely on application functionality.
- Improved Code Maintainability: With data access logic centralized in
DAOs, any changes in the database structure or query logic only need
to be updated within the DAO classes. This modularity simplifies
maintenance and reduces the risk of errors across the application.
- Encourages Reusability: DAO classes can be reused across multiple
parts of the application. Since they handle data access in a
standardized way, the same DAO methods can be used wherever data
retrieval or manipulation is required, promoting consistency and
reusability.
- Increased Flexibility for Database Changes: DAOs make it easier to
change the underlying database or switch to another data source (e.g.,
from SQL to NoSQL) with minimal impact on the rest of the
application. Only the DAO classes need to be updated to support the
new data source, without requiring changes to business logic.
- Supports Multiple Data Sources: The DAO pattern allows for
managing multiple data sources within the same application. Different
DAO implementations can be used to interact with different
databases, making it simple to scale or switch between data sources
when needed.
- Promotes Single Responsibility Principle: By focusing solely on data
operations, DAOs follow the Single Responsibility Principle. This
keeps each class focused on one purpose (data access), leading to a
more modular and organized codebase.
- Easier Testing and Mocking: DAOs can be tested independently from
the business logic, which simplifies testing and debugging.
Additionally, DAOs can be mocked in unit tests, allowing developers
to simulate database interactions and test business logic without
needing a live database connection.
- Enhances Security: Encapsulating database operations in DAO classes
provides an additional security layer. It helps restrict database access
to controlled methods within the DAO, reducing the chances of
unauthorized access or SQL injection attacks if DAO methods are
carefully implemented.
- Simplifies Transactions Management: DAOs can be designed to
manage database transactions within their methods, ensuring that
complex operations (involving multiple queries) are executed safely
and consistently. This simplifies transaction handling within business
logic, where only high-level transaction control may be needed.
- Decouples Application Layers: The DAO pattern promotes loose
coupling between the application and the database. This separation
makes it easier to scale, maintain, and modify the system as the
application grows, providing a more adaptable architecture.
5. Disadvantages
- Increased Complexity and Boilerplate Code: Implementing the DAO
pattern can add complexity to the codebase, particularly for simple
applications or projects with minimal database interactions. The
pattern often requires creating multiple classes, interfaces, and
methods, which can result in excessive boilerplate code.
- Difficulty with Dynamic Queries: The DAO pattern can make
handling complex, dynamic queries more challenging. Since DAOs
typically contain predefined methods for specific queries, supporting
complex or highly variable queries can require creating additional
methods or separate handling mechanisms, which can clutter the
DAO.
- Tight Coupling to Database Logic: Although the DAO pattern aims to
separate data access from business logic, it can still lead to tight
coupling to the underlying database. If the DAO is too specific to a
particular database or SQL dialect, migrating to a different database or
ORM (Object-Relational Mapping) tool can require extensive
refactoring.
- Reduced Flexibility for Advanced Queries: In applications with
advanced database requirements (such as custom joins, aggregations,
or stored procedures), DAOs can be restrictive. Implementing these
features within DAOs may require multiple methods, making the
DAOs harder to maintain and less flexible.
- Potential Performance Overhead: If not carefully designed, DAOs can
lead to performance bottlenecks, especially in applications requiring
frequent data access. The extra layer of abstraction can add method
calls and object creation, impacting performance, particularly in high-
throughput or real-time applications.
- Maintenance Challenges with Large Numbers of DAOs: As the
application grows, so does the number of DAOs, especially if each
entity has its own DAO class. This can lead to a sprawling codebase
with numerous DAOs, making it harder to navigate, manage, and
maintain over time.
- Duplicated Code and Redundancy: Without careful design, DAOs can
lead to duplicated code, particularly if similar database operations are
repeated across multiple DAOs. This redundancy can increase the
effort required for maintenance and updates, as changes in one place
might need to be replicated across several DAOs.
- Limited Support for Non-Relational Databases: The DAO pattern is
traditionally used with relational databases, and adapting it to work
with NoSQL databases can be challenging. Since NoSQL databases
often require different query logic, this adaptation can undermine the
pattern’s intended simplicity and encapsulation.
- Testing Complexity in Large Systems: While DAOs are generally
testable, testing them in larger systems can become complex. DAOs
require database connectivity or mocking during tests, and testing
interactions across multiple DAOs (like in complex transactions) can
complicate unit testing.
- Dependency Management: DAOs can create a large number of
dependencies within the data access layer, especially if they rely on
multiple database libraries or frameworks. Managing these
dependencies, particularly across multiple DAOs, can complicate the
development environment and increase build times.

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;

public HomeTheaterFacade(Amplifier amp, DvdPlayer dvd,


Projector projector, TheaterLights lights) {
this.amp = amp;
this.dvd = dvd;
this.projector = projector;
this.lights = lights;
}

public void watchMovie(String movie) {


System.out.println("Get ready to watch a movie...");
lights.dim(10);
projector.on();
projector.wideScreenMode();
amp.on();
amp.setVolume(5);
dvd.on();
dvd.play(movie);
}

public void endMovie() {


System.out.println("Shutting down the home theater...");
lights.on();
projector.off();
amp.off();
dvd.stop();
dvd.off();
}
}

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();

HomeTheaterFacade homeTheater = new


HomeTheaterFacade(amp, dvd, projector, lights);
homeTheater.watchMovie("Inception");
homeTheater.endMovie();
}
}

- 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.

You might also like