Creating a hierarchical overview of Java design patterns
Creating a hierarchical overview of Java design patterns
using a flowchart along with corresponding Java examples involves organizing
the patterns into categories and illustrating their relationships in a
flowchart format. Below is a conceptual flowchart representation along with
simplified Java code examples for each pattern category.
Hierarchical Overview of Java Design Patterns
Singleton Design
The Singleton Design Pattern is a creational pattern
that ensures a class has only one instance and provides a global point of
access to that instance. This is particularly useful when exactly one object is
needed to coordinate actions across the system, such as in logging, configuration
settings, or connection pooling.
Key Concepts:
- Single
Instance: The class restricts the instantiation to one object.
- Global
Access: The instance is globally accessible, typically via a static
method.
- Thread
Safety (optional): The Singleton can be implemented in a thread-safe
manner to ensure that multiple threads don’t create separate instances in
a multithreaded environment.
Java Implementation: Singleton Pattern
1. Basic Singleton (Lazy Initialization)
This implementation ensures that the instance is created
only when it is needed (lazy initialization).
Here’s how you can implement a Singleton pattern for
managing a database connection using JDBC.
a.
Basic Singleton Database Connection
java
import
java.sql.Connection; import
java.sql.DriverManager; import
java.sql.SQLException; public class
DatabaseSingleton { // Private static instance of the class private static DatabaseSingleton
instance; // JDBC URL, username, and password for
the database private static final String URL =
"jdbc:mysql://localhost:3306/mydatabase"; private static final String USER =
"root"; private static final String PASSWORD =
"password"; // Connection object private Connection connection; // Private constructor to prevent
instantiation from other classes private DatabaseSingleton() throws
SQLException { try { // Initialize the database
connection (load JDBC driver if necessary) this.connection =
DriverManager.getConnection(URL, USER, PASSWORD); } catch (SQLException e) { throw new
SQLException("Error creating database connection: " +
e.getMessage()); } } // Public static method to provide global
access to the instance public static DatabaseSingleton
getInstance() throws SQLException { if (instance == null) { synchronized
(DatabaseSingleton.class) { // Thread-safe initialization if (instance == null) { instance = new
DatabaseSingleton(); } } } return instance; } // Method to retrieve the database
connection public Connection getConnection() { return connection; } } |
Explanation:
- Private
Constructor: Prevents creating new instances from outside the class.
- Static
Method (getInstance()): Provides global access to the Singleton
instance. It also includes thread-safe initialization (double-checked
locking).
- Database
Connection: The Connection object is created only once when the
Singleton is instantiated, ensuring only one connection to the database.
b.
Usage Example:
java
import
java.sql.Connection; import
java.sql.SQLException; import
java.sql.Statement; public class
SingletonDatabaseDemo { public static void main(String[] args) { try { // Get the Singleton instance DatabaseSingleton
databaseSingleton = DatabaseSingleton.getInstance(); // Retrieve the connection Connection connection =
databaseSingleton.getConnection(); // Create a statement and execute
a query Statement statement =
connection.createStatement(); String query = "SELECT *
FROM users"; statement.executeQuery(query); System.out.println("Query
executed successfully!"); } catch (SQLException e) { e.printStackTrace(); } } } |
c.
Thread-Safe Singleton Database Connection
(Double-Checked Locking)
This ensures that multiple threads don’t create multiple
connections simultaneously:
java
public static
DatabaseSingleton getInstance() throws SQLException { if (instance == null) { synchronized
(DatabaseSingleton.class) { if (instance == null) { instance = new
DatabaseSingleton(); } } } return instance; } |
d.
Handling Exceptions:
Make sure you properly handle SQL exceptions during the
connection setup and execution of queries. This design centralizes the
connection management, making it easier to handle connection-related errors
globally.
e.
Closing the Connection (Optional):
You might also want to manage the lifecycle of the database
connection, ensuring the connection is closed when no longer needed.
java
public void
closeConnection() throws SQLException { if (connection != null &&
!connection.isClosed()) { connection.close(); } } |
2. Thread-Safe Singleton
For a multithreaded environment, the basic implementation
might cause issues. You can make the getInstance() method synchronized to
prevent multiple threads from creating multiple instances simultaneously.
java
public class
ThreadSafeSingleton { private static ThreadSafeSingleton
instance; private ThreadSafeSingleton() {} // Synchronized method to ensure only one
thread can access at a time public static synchronized
ThreadSafeSingleton getInstance() { if (instance == null) { instance = new
ThreadSafeSingleton(); } return instance; } public void showMessage() { System.out.println("Thread-safe
Singleton instance invoked!"); } } |
3. Double-Checked Locking Singleton
A more efficient way to implement the Singleton pattern in a
multithreaded environment without paying the cost of synchronization every time
the getInstance() method is called is by using double-checked locking.
java
public class
DoubleCheckedLockingSingleton { private static volatile
DoubleCheckedLockingSingleton instance; private DoubleCheckedLockingSingleton()
{} public static
DoubleCheckedLockingSingleton getInstance() { if (instance == null) { synchronized
(DoubleCheckedLockingSingleton.class) { if (instance == null) { instance = new
DoubleCheckedLockingSingleton(); } } } return instance; } public void showMessage() {
System.out.println("Double-checked locking Singleton instance
invoked!"); } } |
4. Eager Initialization Singleton
In this version, the instance is created at the time of
class loading. This is simpler but does not support lazy initialization.
java
public class
EagerSingleton { // Instance is created at the time of
class loading private static final EagerSingleton
instance = new EagerSingleton(); // Private constructor private EagerSingleton() {} public static EagerSingleton
getInstance() { return instance; } public void showMessage() { System.out.println("Eager
Singleton instance invoked!"); } } |
5. Bill Pugh Singleton (Best Practice)
This approach uses an inner static helper class to achieve
lazy loading and thread safety without the need for synchronization.
java
public class
BillPughSingleton { private BillPughSingleton() {} // Inner static class responsible for
holding the single instance private static class SingletonHelper { private static final
BillPughSingleton INSTANCE = new BillPughSingleton(); } public static BillPughSingleton
getInstance() { return SingletonHelper.INSTANCE; } public void showMessage() { System.out.println("Bill Pugh
Singleton instance invoked!"); } } |
6. Enum Singleton (Best Practice in Modern Java)
This is considered the best approach for Singleton in modern
Java. It’s simple, thread-safe, and protects against serialization and
reflection issues.
java
public enum
EnumSingleton { INSTANCE; public void showMessage() { System.out.println("Enum
Singleton instance invoked!"); } } |
Usage:
java
public class
EnumSingletonDemo { public static void main(String[] args) { EnumSingleton singleton =
EnumSingleton.INSTANCE; singleton.showMessage(); // Output:
Enum Singleton instance invoked! } } |
Key Differences Between Singleton Implementations:
- Lazy
Initialization: Lazy initialization delays the creation of the
instance until it is needed (e.g., getInstance()).
- Thread
Safety: Simple Singleton may not be thread-safe, but synchronized
methods, double-checked locking, and the Bill Pugh method ensure thread
safety.
- Eager
Initialization: The instance is created when the class is loaded,
which may be unnecessary if the instance is not used.
- Bill
Pugh Singleton: Combines lazy initialization with thread safety in a
very efficient manner.
- Enum
Singleton: This is the simplest, thread-safe, and most recommended
solution for modern Java, as it handles serialization and reflection
issues.
Advantages of Singleton Pattern:
- Controlled
Access to a Single Instance: The Singleton pattern ensures that only
one instance of the class is created and provides a global access point.
- Lazy
Initialization (Optional): The instance is created only when it is
needed, which can save resources.
- Thread
Safety (Optional): Thread-safe implementations ensure that only one
instance is created, even in a multithreaded environment.
Disadvantages of Singleton Pattern:
- Global
State: It introduces global state into an application, which can lead
to unintended side effects if not used carefully.
- Difficult
to Test: Singletons can make unit testing harder, especially if the
Singleton holds state or interacts with external systems.
- Overuse:
Overusing Singleton can lead to a violation of the Single Responsibility
Principle (SRP), where a class does more than it should.
When to Use the Singleton Pattern:
Ø When
a class needs to have exactly one instance, and this instance needs to be
accessible to multiple clients.
Ø When
managing resources like database connections, logging services, or
configuration settings.
The Builder Design Pattern
The Builder Design Pattern is a creational design pattern
that is used to construct complex objects step by step. It allows you to create
different representations of the same object while avoiding constructor overloading
issues. The pattern is particularly useful when an object contains multiple
attributes or configurations that are optional or require validation.
When to Use the Builder Pattern
Ø When
a class has many optional parameters.
Ø When
you want to construct an object step-by-step.
Ø When
you want to ensure immutability after the object is built.
Example: Building a Complex Object (House)
Let’s consider an example where we are building a House
object that has several optional and required attributes.
1. House Class (Product)
This class represents the object to be built.
java
class House { // Required attributes private String foundation; private String structure; // Optional attributes private boolean hasGarage; private boolean hasSwimmingPool; private boolean hasGarden; // Private constructor so it can only be
instantiated through the builder private House(HouseBuilder builder) { this.foundation = builder.foundation; this.structure = builder.structure; this.hasGarage = builder.hasGarage; this.hasSwimmingPool =
builder.hasSwimmingPool; this.hasGarden = builder.hasGarden; } @Override public String toString() { return "House{" + "foundation='" +
foundation + '\'' + ", structure='" +
structure + '\'' + ", hasGarage=" +
hasGarage + ",
hasSwimmingPool=" + hasSwimmingPool + ", hasGarden=" +
hasGarden + '}'; } } |
2. HouseBuilder Class
This is the builder class that provides methods to set
different parts of the House object.
java
class
HouseBuilder { // Required attributes String foundation; String structure; // Optional attributes boolean hasGarage; boolean hasSwimmingPool; boolean hasGarden; public HouseBuilder(String foundation,
String structure) { this.foundation = foundation; this.structure = structure; } public HouseBuilder setGarage(boolean
hasGarage) { this.hasGarage = hasGarage; return this; // Returning the builder itself to enable
method chaining } public HouseBuilder
setSwimmingPool(boolean hasSwimmingPool) { this.hasSwimmingPool =
hasSwimmingPool; return this; } public HouseBuilder setGarden(boolean
hasGarden) { this.hasGarden = hasGarden; return this; } // Build the final object public House build() { return new House(this); } } |
3. Using the Builder Pattern
Here’s how to use the builder pattern to create instances of
the House class:
java
public class
BuilderPatternDemo { public static void main(String[] args) { // Creating a House object with a
garage and swimming pool House house1 = new
HouseBuilder("Concrete Foundation", "Wooden Structure") .setGarage(true) .setSwimmingPool(true) .build(); System.out.println(house1); // Creating a House object with a
garden but no garage or pool House house2 = new
HouseBuilder("Steel Foundation", "Brick Structure") .setGarden(true) .build(); System.out.println(house2); } } |
4. Output
plaintext
House{foundation='Concrete
Foundation', structure='Wooden Structure', hasGarage=true,
hasSwimmingPool=true, hasGarden=false} House{foundation='Steel
Foundation', structure='Brick Structure', hasGarage=false,
hasSwimmingPool=false, hasGarden=true} |
Explanation
- House
Class:
Ø
This is the object being built. It has both
required attributes (foundation, structure) and optional attributes (hasGarage,
hasSwimmingPool, hasGarden).
Ø
The constructor is private, so it can only be
accessed through the HouseBuilder.
- HouseBuilder
Class:
Ø
It defines methods to set optional properties
and build the final House object.
Ø
The build() method creates and returns a House
object using the current state of the builder.
Ø
Method chaining is enabled by returning this
from setter methods.
- Fluent
Interface:
Ø
The builder pattern uses a "fluent
interface" design, allowing methods to be chained together, making the
code more readable and allowing flexible configuration.
Advantages of the Builder Pattern
ü Readability:
Clear and readable code when creating objects with many attributes.
ü Immutability:
Once the object is built, it is immutable (no setters in the House class).
ü Flexible
Object Construction: Different configurations of the object can be created
without complex constructors.
ü Avoid
Constructor Overload: Eliminates the need to write multiple constructors
with varying parameters.
Use Cases
ü Configuring
objects with multiple optional parameters, such as creating complex UI
elements, configuring HTTP requests, or constructing domain models with
multiple configurations.
This example shows how the Builder Pattern simplifies object
construction, making the code more readable and maintainable, especially when
dealing with complex objects.
The Factory Method Pattern
The Factory Method Pattern is a creational design
pattern that defines an interface for creating an object, but lets subclasses
alter the type of objects that will be created. This pattern promotes loose
coupling by eliminating the need for client code to specify the exact class of
objects it will create.
Key Concepts
Ø Factory
Method: An abstract method in the base class that is overridden by
subclasses to create specific objects.
Ø Product:
The object created by the factory method.
Ø Concrete
Product: A specific implementation of the product interface.
Ø Client:
Uses the factory method to get an object, without knowing the exact class of
the object it will receive.
Real-World Analogy:
Imagine you are developing a system where users can choose
between different types of documents to create, like Word documents or PDFs.
Instead of instantiating document objects directly, you use a factory method
that decides which type of document to create based on the user’s input.
Java Implementation: Factory Method Pattern
Let’s implement a factory method that creates different
types of Document objects: WordDocument and PDFDocument.
1. Product Interface
This is the common interface for all document types.
java
interface
Document { void open(); } |
2. Concrete Products
These are specific implementations of the Document
interface.
WordDocument
java
class
WordDocument implements Document { @Override public void open() { System.out.println("Opening a
Word document."); } } |
PDFDocument
java
class
PDFDocument implements Document { @Override public void open() { System.out.println("Opening a
PDF document."); } } |
3. Creator (Factory) Class
This class contains the factory method that returns a
Document. Subclasses will implement this method to instantiate specific
document types.
java
interface DocumentFactory
{ Document createDocument(); } |
4. Concrete Factories
These subclasses implement the createDocument() method to
return specific types of documents.
WordDocumentFactory
java
class
WordDocumentFactory implements DocumentFactory { @Override public Document createDocument() { return new WordDocument(); } } |
PDFDocumentFactory
java
class
PDFDocumentFactory implements DocumentFactory { @Override public Document createDocument() { return new PDFDocument(); } } |
5. Client Code
The client interacts with the DocumentFactory, which decides
which concrete product to create.
java
public class
FactoryMethodExample { public static void main(String[] args) { // Create a WordDocument using
WordDocumentFactory DocumentFactory wordFactory = new
WordDocumentFactory(); Document wordDoc =
wordFactory.createDocument(); wordDoc.open(); // Output: Opening a Word document. // Create a PDFDocument using
PDFDocumentFactory DocumentFactory pdfFactory = new
PDFDocumentFactory(); Document pdfDoc =
pdfFactory.createDocument(); pdfDoc.open(); // Output: Opening a PDF document. } } |
6. Output
plaintext
Opening a
Word document. Opening a PDF
document. |
Explanation
Ø Product
Interface (Document): Declares the interface that all document types must
implement.
Ø Concrete
Products (WordDocument, PDFDocument): These classes implement the Document
interface.
Ø Factory
Interface (DocumentFactory): This interface declares the createDocument()
method, which is used by concrete factories to return specific document types.
Ø Concrete
Factories (WordDocumentFactory, PDFDocumentFactory): These classes
implement the factory method to return specific document instances.
Ø Client
Code: The client doesn’t need to know which type of document it is working
with. It simply uses the factory, which decides which concrete product to
instantiate.
Advantages of the Factory Method Pattern
- Loose
Coupling: The client code is decoupled from the specific classes of
objects it creates.
- Single
Responsibility Principle: The object creation code is centralized in
the factory method, making it easier to maintain and extend.
- Flexibility:
New types of products can be added without modifying existing client code,
by adding new factory subclasses.
- Scalability:
You can add more concrete products or factories without changing the
client code.
When to Use the Factory Method Pattern
Ø When
a class doesn’t know which class of objects it will be required to create.
Ø When
a class wants its subclasses to specify the object to be created.
Ø When
the process of object creation needs to be independent of the specific type of
product.
Example Use Cases
Ø UI
Elements: If you have different operating systems (Windows, macOS, Linux),
each may require different types of UI components like buttons and menus.
Ø Document
Processing Systems: As in the example above, where the system can generate
different types of documents (PDFs, Word docs).
Ø Game
Development: Creating various types of enemies, weapons, or power-ups.
The Factory Method Pattern helps manage the
complexity of object creation by delegating the responsibility to subclasses.
It promotes flexibility and extensibility, making your code more maintainable
in the long run.
The Abstract Factory Pattern:
The Abstract Factory Pattern is a creational design
pattern that provides an interface for creating families of related or
dependent objects without specifying their concrete classes. It is useful when
the system needs to be independent of how its objects are created and when the
object families are designed to work together.
Key Concepts
Ø Abstract
Factory: Declares an interface for creating abstract products.
Ø Concrete
Factories: Implement the operations to create concrete products.
Ø Abstract
Products: Declare an interface for a type of product.
Ø Concrete
Products: Implement the abstract product interface.
Ø Client:
Uses only interfaces declared by the abstract factory and abstract products.
Real-World Analogy:
Imagine you are creating a furniture store where the
furniture comes in different styles, such as Victorian and Modern. The furniture
includes objects like chairs and sofas, and you want to build these objects
using an abstract factory so that your client can switch between creating
Victorian or Modern furniture without modifying the code.
Java Implementation: Abstract Factory Pattern
We’ll demonstrate how to implement an Abstract Factory to
create Victorian and Modern style furniture.
1. Define Abstract Products
These interfaces define the product types that can be
created by the factories, e.g., Chair and Sofa.
java
interface
Chair { void sitOn(); } interface
Sofa { void relaxOn(); } |
2. Define Concrete Products
These classes implement the product interfaces for each
family (Victorian and Modern).
Victorian Furniture
java
class
VictorianChair implements Chair { @Override public void sitOn() { System.out.println("Sitting on a
Victorian chair."); } } class
VictorianSofa implements Sofa { @Override public void relaxOn() { System.out.println("Relaxing on
a Victorian sofa."); } } |
Modern Furniture
java
class
ModernChair implements Chair { @Override public void sitOn() { System.out.println("Sitting on a
Modern chair."); } } class
ModernSofa implements Sofa { @Override public void relaxOn() { System.out.println("Relaxing on
a Modern sofa."); } } |
3. Define Abstract Factory
This interface declares methods for creating abstract
products (e.g., createChair() and createSofa()).
java
interface
FurnitureFactory { Chair createChair(); Sofa createSofa(); } |
4. Define Concrete Factories
These classes implement the FurnitureFactory interface to
create the specific concrete products (Victorian or Modern).
Victorian Furniture Factory
java
class
VictorianFurnitureFactory implements FurnitureFactory { @Override public Chair createChair() { return new VictorianChair(); } @Override public Sofa createSofa() { return new VictorianSofa(); } } |
Modern Furniture Factory
java
class ModernFurnitureFactory
implements FurnitureFactory { @Override public Chair createChair() { return new ModernChair(); } @Override public Sofa createSofa() { return new ModernSofa(); } } |
5. Client Code
The client uses the FurnitureFactory interface to create
objects without worrying about their concrete classes.
java
public class
AbstractFactoryPatternDemo { public static void main(String[] args) { // Create a Victorian furniture
factory FurnitureFactory victorianFactory =
new VictorianFurnitureFactory(); Chair victorianChair =
victorianFactory.createChair(); Sofa victorianSofa =
victorianFactory.createSofa(); victorianChair.sitOn(); victorianSofa.relaxOn(); // Create a Modern furniture factory FurnitureFactory modernFactory = new
ModernFurnitureFactory(); Chair modernChair =
modernFactory.createChair(); Sofa modernSofa =
modernFactory.createSofa(); modernChair.sitOn(); modernSofa.relaxOn(); } } |
6. Output
plaintext
Sitting on a
Victorian chair. Relaxing on a
Victorian sofa. Sitting on a
Modern chair. Relaxing on a
Modern sofa. |
Explanation
Ø Abstract
Factory (FurnitureFactory): Declares methods to create abstract products
(createChair(), createSofa()).
Ø Concrete
Factory (VictorianFurnitureFactory, ModernFurnitureFactory): Implements the
methods to create concrete products specific to a family (Victorian or Modern).
Ø Client
(AbstractFactoryPatternDemo): The client uses the factory to create objects
without knowing their specific types.
Advantages of the Abstract Factory Pattern
- Decouples
the Client from Concrete Implementations: The client works with the
abstract interface, so it doesn't know or care which concrete products are
being used.
- Enforces
Object Family Creation: The Abstract Factory pattern ensures that
objects that belong to the same family (like Victorian or Modern) are used
together.
- Easier
to Extend Families of Products: If you need to add a new family of
products (e.g., Art Deco furniture), you can create new concrete factory
and product classes without changing the existing code.
When to Use the Abstract Factory Pattern
Ø When
a system needs to be independent of how its objects are created.
Ø When
you want to create families of related objects that need to work together.
Ø When
you expect to add new product families to the system.
The Abstract Factory Pattern helps you to abstract
away the process of object creation, allowing for easier extension and
management of related object families.
The Chain of Responsibility pattern
The Chain of Responsibility pattern is a behavioral design
pattern that allows an object to pass a request along a chain of handlers until
one of them handles the request. It's useful when multiple objects might handle
a request, and the handler is not determined until runtime.
Java Example: Chain of Responsibility Pattern
Let's say we have a system that processes different levels
of logging messages: DEBUG, INFO, and ERROR. We want to set up a chain of
loggers, where each logger handles a specific level of messages, and forwards
the rest to the next logger in the chain.
1. Logger Interface
This defines the interface for all loggers in the chain.
Each logger will handle a log message or pass it to the next logger in the
chain.
java
interface
Logger { void setNext(Logger nextLogger); // Set the next logger in the chain void logMessage(int level, String
message); // Handle the log message } |
2. Concrete Loggers
These are specific implementations of the Logger interface.
Each logger will handle messages based on its log level.
InfoLogger
java
class
InfoLogger implements Logger { private Logger nextLogger; // Reference
to the next logger in the chain @Override public void setNext(Logger nextLogger) { this.nextLogger = nextLogger; } @Override public void logMessage(int level, String
message) { if (level == 1) { System.out.println("INFO:
" + message); } else if (nextLogger != null) { nextLogger.logMessage(level,
message); } } } |
DebugLogger
java
class
DebugLogger implements Logger { private Logger nextLogger; @Override public void setNext(Logger nextLogger) { this.nextLogger = nextLogger; } @Override public void logMessage(int level, String
message) { if (level == 2) { System.out.println("DEBUG:
" + message); } else if (nextLogger != null) { nextLogger.logMessage(level,
message); } } } |
ErrorLogger
java
class
ErrorLogger implements Logger { private Logger nextLogger; @Override public void setNext(Logger nextLogger) { this.nextLogger = nextLogger; } @Override public void logMessage(int level, String
message) { if (level == 3) { System.out.println("ERROR:
" + message); } else if (nextLogger != null) { nextLogger.logMessage(level,
message); } } } |
3. Client Code
The client sets up the chain of responsibility and sends log
messages through the chain.
java
public class
ChainOfResponsibilityDemo { public static void main(String[] args) { // Create loggers Logger infoLogger = new InfoLogger(); Logger debugLogger = new
DebugLogger(); Logger errorLogger = new ErrorLogger(); // Set up the chain: INFO -> DEBUG -> ERROR infoLogger.setNext(debugLogger); debugLogger.setNext(errorLogger); // Log messages at different levels infoLogger.logMessage(1, "This
is an informational message.");
// Output: INFO: This is an informational message. infoLogger.logMessage(2, "This
is a debug message."); //
Output: DEBUG: This is a debug message. infoLogger.logMessage(3, "This
is an error message."); //
Output: ERROR: This is an error message. infoLogger.logMessage(4, "This
level is not handled."); //
No output, as no logger handles level 4 } } |
4. Output
plaintext
INFO: This is
an informational message. DEBUG: This
is a debug message. ERROR: This
is an error message. |
Explanation
Ø Logger
Interface: Defines the methods setNext() to set the next logger in the
chain and logMessage() to either process the log or pass it to the next logger.
Ø Concrete
Loggers (InfoLogger, DebugLogger, ErrorLogger): Implement the Logger
interface. Each logger checks if the log level matches its responsibility; if
it doesn't, it passes the log message to the next logger.
Ø Client
Code: The client sets up the chain of responsibility by connecting the
loggers and then sends log messages through the chain. Each message is either
handled by the appropriate logger or ignored if no logger can handle it.
Advantages
- Loose
Coupling: The client doesn't need to know which logger handles which
log level; the message simply flows through the chain.
- Extensibility:
New loggers can easily be added to the chain without modifying existing
code.
- Single
Responsibility Principle: Each logger only handles a specific log
level, keeping the responsibility clear.
When to Use
Ø When
you have multiple objects that can handle a request, but only one should
process it.
Ø When
you want to decouple the sender of a request from its receiver.
Ø When
the chain of responsibility should be dynamic and extensible.
This implementation shows how to build the Chain of
Responsibility Pattern using interfaces, avoiding the use of the abstract
keyword while maintaining flexibility and scalability.
The Interpreter pattern:
The Interpreter pattern is a behavioral design pattern that
defines a representation for a language's grammar along with an interpreter
that uses this representation to interpret sentences in the language. It is
useful when you want to evaluate expressions in a language or when the grammar
is simple and well-understood.
Java Example: Interpreter Pattern
Let’s consider an example where we want to evaluate simple
mathematical expressions consisting of numbers, addition, and subtraction.
1. Define the Expression Interface
This interface will declare an interpret method, which each
concrete expression will implement.
java
interface
Expression { int interpret(); } |
2. Create Concrete Expression Classes
We need classes for numbers and the operations (addition and
subtraction).
a. NumberExpression
java
class
NumberExpression implements Expression { private int number; public NumberExpression(int number) { this.number = number; } public NumberExpression(String number) { this.number =
Integer.parseInt(number); } @Override public int interpret() { return this.number; } } |
b. AddExpression
java
class
AddExpression implements Expression { private Expression leftExpression; private Expression rightExpression; public AddExpression(Expression leftExpression,
Expression rightExpression) { this.leftExpression = leftExpression; this.rightExpression =
rightExpression; } @Override public int interpret() { return leftExpression.interpret() +
rightExpression.interpret(); } } |
c. SubtractExpression
java
class
SubtractExpression implements Expression { private Expression leftExpression; private Expression rightExpression; public SubtractExpression(Expression
leftExpression, Expression rightExpression) { this.leftExpression = leftExpression; this.rightExpression =
rightExpression; } @Override public int interpret() { return leftExpression.interpret() -
rightExpression.interpret(); } } |
3. Interpreter Client
The client will parse a simple string expression and create
a corresponding expression tree using the interpreter classes.
java
import
java.util.Stack; class
InterpreterClient { public static Expression parse(String
expression) { Stack<Expression> stack = new
Stack<>(); String[] tokens =
expression.split(" "); for (String token : tokens) { if (isOperator(token)) { Expression rightExpression =
stack.pop(); Expression leftExpression =
stack.pop(); Expression operator =
getOperator(token, leftExpression, rightExpression); int result =
operator.interpret(); stack.push(new
NumberExpression(result)); } else { stack.push(new
NumberExpression(token)); } } return stack.pop(); } private static boolean isOperator(String
token) { return token.equals("+") ||
token.equals("-"); } private static Expression
getOperator(String token, Expression left, Expression right) { switch (token) { case "+": return new
AddExpression(left, right); case "-": return new
SubtractExpression(left, right); default: throw new
UnsupportedOperationException("Unknown operator: " + token); } } } |
4. Using the Interpreter Pattern
Now, let's create a client application that uses the
InterpreterClient to evaluate expressions.
java
public class
InterpreterPatternDemo { public static void main(String[] args) { String expression = "7 3 - 2 1 +
+"; // (7 - 3) + (2 + 1) Expression result =
InterpreterClient.parse(expression); System.out.println(expression +
" = " + result.interpret()); } } |
5. Output
When you run the InterpreterPatternDemo, you should see the
following output:
plaintext
7 3 - 2 1 + +
= 7 |
Explanation
Ø
Expression Interface: Defines a method
interpret() that is implemented by all concrete expressions.
Ø
Concrete Expressions:
ü
NumberExpression handles numbers.
ü
AddExpression and SubtractExpression handle
addition and subtraction, respectively.
Ø
Interpreter Client: The InterpreterClient
class parses a postfix expression (where operators follow their operands) and
uses a stack to evaluate the expression using the interpreter classes.
Ø
Postfix Expression: The expression
"7 3 - 2 1 + +" represents the mathematical expression (7−3)+(2+1)(7
- 3) + (2 + 1)(7−3)+(2+1).
Use Case
Ø The
Interpreter pattern is suitable for scenarios where the grammar is simple and
stable, and the language to be interpreted is small.
Ø Examples
include interpreting mathematical expressions, parsing simple configuration
files, or evaluating conditions in rule engines.
This example demonstrates how the Interpreter pattern can be
implemented in Java to evaluate simple mathematical expressions.
The Prototype Design Pattern
The Prototype Design Pattern is a creational pattern
in which an object is created by copying an existing object (the
"prototype"). Instead of creating a new object from scratch, a
prototype object is cloned to create new objects. This is particularly useful
when object creation is expensive and we want to reuse objects that already
exist.
In Java, the Prototype pattern typically uses the clone()
method from the Cloneable interface to create a copy of the object.
Key Concepts:
- Prototype
Interface: Declares a cloning method (like clone()).
- Concrete
Prototype: A class that implements the prototype interface and defines
how to clone itself.
- Client:
The object that asks for a clone of the prototype.
Java Implementation: Prototype Pattern
1. Prototype Interface
This interface declares a method for cloning objects. In
Java, we typically use the Cloneable interface and override the clone() method
from Object.
java
interface
Prototype extends Cloneable { Prototype clone(); } |
2. Concrete Prototype
This is a class that implements the Prototype interface and
provides its own implementation of the clone() method.
Document Prototype
java
class
Document implements Prototype { private String title; private String content; public Document(String title, String
content) { this.title = title; this.content = content; } // Getter and Setter methods public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public Document clone() { try { // Create a shallow copy using
Object's clone method return (Document) super.clone(); } catch (CloneNotSupportedException
e) { e.printStackTrace(); return null; } } @Override public String toString() { return "Document{" + "title='" + title +
'\'' + ", content='" +
content + '\'' + '}'; } } |
3. Client Code
The client creates new objects by cloning existing ones,
instead of creating new objects from scratch.
java
public class
PrototypePatternDemo { public static void main(String[] args) { // Original document (prototype) Document originalDoc = new
Document("Original Title", "This is the original
content."); // Cloning the original document to
create a new one Document clonedDoc =
originalDoc.clone(); // Changing the cloned document's
content clonedDoc.setTitle("Cloned
Title"); clonedDoc.setContent("This is
the cloned content."); // Output the original and cloned
documents System.out.println("Original
Document: " + originalDoc); System.out.println("Cloned
Document: " + clonedDoc); } } |
4. Output
plaintext
Original
Document: Document{title='Original Title', content='This is the original
content.'} Cloned
Document: Document{title='Cloned Title', content='This is the cloned
content.'} |
Explanation
Ø Prototype
Interface (Prototype): This defines the clone() method, which allows
objects to be cloned.
Ø Concrete
Prototype (Document): Implements the Prototype interface and provides the
cloning logic. The class overrides the clone() method from Object to return a
copy of the document.
Ø Client
(PrototypePatternDemo): The client creates a new document by cloning the
original one, then modifies the clone without affecting the original document.
Shallow vs. Deep Copy
In this example, we're using a shallow copy, which
means that the fields of the cloned object are copied as-is. If the fields are
primitive types (e.g., int, boolean) or immutable objects (e.g., String), this
is sufficient. However, if the object contains references to other mutable
objects, a deep copy might be necessary to avoid unexpected behavior.
To create a deep copy, you would need to manually
clone each object referenced by the original object, ensuring that the clone
doesn't share any mutable references with the original.
Advantages of Prototype Pattern
- Improves
performance: Cloning objects is generally faster than creating them
from scratch, especially when the object initialization is complex.
- Decoupling:
The client code doesn't need to know the exact class of the object it is
cloning, only that it supports the clone() operation.
- Dynamic
object creation: The client can easily create new objects by cloning
existing ones, which can be customized as needed.
When to Use the Prototype Pattern
Ø When
the creation of new objects is costly (e.g., database connections, large
datasets).
Ø When
there are many different configurations or states of an object and creating
them from scratch is expensive or complex.
Ø When
you need to create objects at runtime based on some existing prototypes.
The Prototype pattern provides an efficient and flexible way
to create new objects by cloning an existing one, allowing you to avoid the
overhead of full object creation.
The Facade Design Pattern
The Facade Design Pattern is a structural design
pattern that provides a simplified interface to a complex subsystem or set of
classes. The purpose of the Facade is to hide the complexity of the system by
providing a higher-level interface that makes the subsystem easier to use.
Key Concepts:
Ø Subsystem
Classes: These are the classes that perform the actual work and contain the
complex logic. The client typically does not interact directly with these
classes.
Ø Facade:
This class provides simple methods that delegate client requests to the
appropriate subsystem classes.
Ø Client:
The client interacts with the Facade rather than the complex subsystem
directly.
Java Implementation: Facade Pattern
Scenario: Home Theater System
In this example, we will implement a facade for controlling
a home theater system. The system consists of a TV, SoundSystem, and DVDPlayer.
The client interacts with the HomeTheaterFacade to turn the system on and off,
without needing to deal with each component separately.
1. Subsystem Classes
These are the complex classes that perform specific tasks.
TV Class
java
class TV { public void turnOn() { System.out.println("TV is turned
ON."); } public void turnOff() { System.out.println("TV is turned
OFF."); } } |
SoundSystem Class
java
class
SoundSystem { public void turnOn() { System.out.println("Sound system
is turned ON."); } public void turnOff() { System.out.println("Sound system
is turned OFF."); } public void setVolume(int volume) { System.out.println("Sound system
volume set to " + volume); } } |
DVDPlayer Class
java
class DVDPlayer
{ public void turnOn() { System.out.println("DVD Player
is turned ON."); } public void turnOff() { System.out.println("DVD Player
is turned OFF."); } public void playMovie(String movie) { System.out.println("Playing
movie: " + movie); } } |
2. Facade Class
The Facade class simplifies the interaction with the complex
subsystem by providing a unified interface.
HomeTheaterFacade
java
class
HomeTheaterFacade { private TV tv; private SoundSystem soundSystem; private DVDPlayer dvdPlayer; public HomeTheaterFacade(TV tv,
SoundSystem soundSystem, DVDPlayer dvdPlayer) { this.tv = tv; this.soundSystem = soundSystem; this.dvdPlayer = dvdPlayer; } // Method to turn everything on and start
a movie public void watchMovie(String movie) { System.out.println("Starting the
movie setup..."); tv.turnOn(); soundSystem.turnOn(); soundSystem.setVolume(5); dvdPlayer.turnOn(); dvdPlayer.playMovie(movie); System.out.println("Movie is
playing. Enjoy!"); } // Method to turn everything off public void endMovie() { System.out.println("Shutting
down the home theater..."); dvdPlayer.turnOff(); soundSystem.turnOff(); tv.turnOff(); } } |
3. Client Code
The client interacts with the HomeTheaterFacade rather than
dealing with each subsystem directly.
java
public class
FacadePatternDemo { public static void main(String[] args) { // Create subsystem components TV tv = new TV(); SoundSystem soundSystem = new
SoundSystem(); DVDPlayer dvdPlayer = new
DVDPlayer(); // Create the facade HomeTheaterFacade homeTheater = new
HomeTheaterFacade(tv, soundSystem, dvdPlayer); // Watch a movie
homeTheater.watchMovie("Inception"); // End the movie homeTheater.endMovie(); } } |
4. Output
plaintext
Starting the movie
setup... TV is turned
ON. Sound system
is turned ON. Sound system
volume set to 5. DVD Player is
turned ON. Playing
movie: Inception Movie is
playing. Enjoy! Shutting down
the home theater... DVD Player is
turned OFF. Sound system
is turned OFF. TV is turned
OFF. |
Explanation
Ø Subsystem
Classes (TV, SoundSystem, DVDPlayer): These classes represent the
components of the home theater system, each with their own methods to handle
specific tasks like turning on, adjusting volume, or playing a movie.
Ø Facade
(HomeTheaterFacade): The facade provides a simple interface (watchMovie()
and endMovie()) for the client to interact with, hiding the complexity of the
individual components.
Ø Client
Code: The client interacts with the HomeTheaterFacade, which handles all
the operations internally, making the process easy and straightforward.
Advantages of the Facade Pattern
- Simplified
Interface: It provides a high-level interface that makes the subsystem
easier to use.
- Loose
Coupling: The client is decoupled from the subsystem components. If
the subsystem changes, only the facade needs to be modified, not the
client.
- Improved
Readability: The facade improves code readability by hiding complex
logic behind simple methods.
- Modular
Design: Subsystems can evolve independently of the client using them,
as long as the facade interface remains consistent.
When to Use the Facade Pattern
Ø When
you have a complex system with many interdependent classes and you want to
provide a simple interface for clients.
Ø When
you want to decouple the client code from the subsystem classes.
Ø When
you want to reduce the dependencies of client code on a particular subsystem.
The Facade pattern is useful when working with large,
complex systems and you want to provide a clear and simple interface for the
client to interact with. It hides the complexity of the underlying system,
making it easier to work with.
The Flyweight Design Pattern
The Flyweight Design Pattern is a structural design
pattern that focuses on reducing the memory usage by sharing as much data as
possible with similar objects. It is used when a large number of objects are
created, which have only a few distinct properties that can be shared to minimize
resource consumption.
In the Flyweight pattern:
Ø Intrinsic
State: The data that is shared across multiple objects.
Ø Extrinsic
State: The data that is unique to each object and cannot be shared.
Key Concepts:
- Flyweight
Interface: Defines a method that Flyweight objects must implement.
- Concrete
Flyweight: Implements the Flyweight interface and contains the shared
(intrinsic) state.
- Flyweight
Factory: Manages a pool of flyweight objects and returns shared
objects.
- Client:
Uses the flyweight objects, passing extrinsic state to them as needed.
Example Scenario: A Text Editor with Character Objects
Suppose we are designing a text editor where each character
on the screen is represented by an object. Instead of creating thousands of
individual character objects, we can use the Flyweight pattern to share common
character data (e.g., the actual character, font type) and store unique data
like position separately.
Java Implementation: Flyweight Pattern
1. Flyweight Interface
This interface defines the method to display a character,
taking the extrinsic state (e.g., position) as a parameter.
java
interface
CharacterFlyweight { void display(int positionX, int
positionY); } |
2. Concrete Flyweight
This class represents a specific character that shares its
intrinsic state (the character itself) and takes extrinsic state (the position)
during rendering.
java
class
Character implements CharacterFlyweight { private char letter; // Intrinsic state:
the actual character public Character(char letter) { this.letter = letter; } @Override public void display(int positionX, int
positionY) { System.out.println("Displaying
character '" + letter + "' at position (" + positionX +
", " + positionY + ")"); } } |
3. Flyweight Factory
The factory manages a pool of flyweight objects. It creates
a new object only if one doesn't already exist.
java
import
java.util.HashMap; import
java.util.Map; class
CharacterFactory { private Map<Character,
CharacterFlyweight> characters = new HashMap<>(); public CharacterFlyweight
getCharacter(char letter) { // Check if the character is already
in the pool CharacterFlyweight character =
characters.get(letter); // If not, create a new Character
object and add it to the pool if (character == null) { character = new
Character(letter); characters.put(letter,
character); } return character; } // Optionally, a method to show how many
unique flyweights exist public int getTotalCharactersCreated() { return characters.size(); } } |
4. Client Code
The client uses the CharacterFactory to obtain flyweight
characters and passes the extrinsic state (position) to the display() method.
java
public class
FlyweightPatternDemo { public static void main(String[] args) { CharacterFactory factory = new
CharacterFactory(); // Create some characters and display
them at different positions CharacterFlyweight charA =
factory.getCharacter('A'); charA.display(10, 20); // Output: Displaying character 'A' at
position (10, 20) CharacterFlyweight charB =
factory.getCharacter('B'); charB.display(30, 40); // Output: Displaying character 'B' at
position (30, 40) CharacterFlyweight anotherCharA =
factory.getCharacter('A'); anotherCharA.display(50, 60); // Output: Displaying character 'A' at
position (50, 60) // Check how many unique characters
were created System.out.println("Total unique
characters created: " + factory.getTotalCharactersCreated()); // Output: Total unique characters
created: 2 } } |
5. Output
plaintext
Displaying
character 'A' at position (10, 20) Displaying
character 'B' at position (30, 40) Displaying
character 'A' at position (50, 60) Total unique
characters created: 2 |
Explanation
Ø Flyweight
Interface (CharacterFlyweight): This defines the display() method, which
accepts extrinsic data such as the character's position.
Ø Concrete
Flyweight (Character): This class stores the intrinsic state (the actual
character) and implements the display() method, using the extrinsic state
(position) provided by the client.
Ø Flyweight
Factory (CharacterFactory): This manages the pool of flyweight objects. If
a character has already been created, it reuses the existing instance;
otherwise, it creates a new one.
Ø Client
(FlyweightPatternDemo): The client interacts with the flyweight objects by
retrieving them from the factory and passing the extrinsic state (position).
Advantages of Flyweight Pattern
- Memory
Optimization: The Flyweight pattern helps reduce the memory footprint
by sharing objects that have the same intrinsic state.
- Performance
Improvement: Since shared objects are reused, object creation overhead
is minimized.
- Scalability:
This pattern is particularly useful when dealing with large numbers of
similar objects, such as in graphical applications, caching, or game
development.
When to Use the Flyweight Pattern
Ø When
you need to create a large number of similar objects and want to reduce memory
usage by sharing data.
Ø When
the intrinsic state of an object can be shared and only the extrinsic state
varies.
Ø In
graphical systems, text editors, caching systems, or any application where many
objects are created that share common data.
Trade-offs
Ø Complexity:
While the Flyweight pattern reduces memory usage, it can add complexity by
separating intrinsic and extrinsic states.
Ø Thread
Safety: If flyweight objects are shared across multiple threads, care must
be taken to ensure thread safety.
The Flyweight Design Pattern is a powerful tool for
optimizing performance in memory-constrained environments by minimizing object
creation and reusing shared state across objects.
The Bridge Design Pattern
The Bridge Design Pattern is a structural pattern
that separates an abstraction from its implementation so that the two can vary
independently. It is particularly useful when you need to avoid a permanent
binding between an abstraction and its implementation, allowing them to evolve
separately.
Key Concepts:
- Abstraction:
Defines the abstract part of the interface and maintains a reference to
the implementer.
- Refined
Abstraction: Extends the abstraction and can provide additional
functionality.
- Implementer:
Defines the interface for implementation classes. It is usually an
abstract class or interface.
- Concrete
Implementer: Provides the concrete implementation of the implementer's
interface.
Use Case:
Suppose we have a drawing application that supports
different shapes (like circles and squares) and different colors (like red and green).
We want to be able to draw different shapes in different colors without tightly
coupling the shape and color.
Java Implementation: Bridge Pattern
1. Implementer Interface
Defines the interface for implementations that can be used
by the abstraction.
java
interface
Color { void applyColor(); } |
2. Concrete Implementers
Provide concrete implementations of the Color interface.
Red Color Implementation
java
class
RedColor implements Color { @Override public void applyColor() { System.out.println("Applying red
color."); } } |
Green Color Implementation
java
class
GreenColor implements Color { @Override public void applyColor() { System.out.println("Applying
green color."); } } |
3. Abstraction Interface
Defines the interface for abstraction and maintains a
reference to the Color implementer.
java
interface
Shape { void draw(); // Abstraction method } |
4. Concrete Abstractions
Implement the Shape interface and use the Color implementer
to apply color.
Circle Shape
java
class Circle
implements Shape { private Color color; private int radius; public Circle(Color color, int radius) { this.color = color; this.radius = radius; } @Override public void draw() { System.out.println("Drawing a
circle with radius " + radius); color.applyColor(); // Use the
implementer to apply color } } |
Square Shape
java
class Square
implements Shape { private Color color; private int side; public Square(Color color, int side) { this.color = color; this.side = side; } @Override public void draw() { System.out.println("Drawing a
square with side " + side); color.applyColor(); // Use the
implementer to apply color } } |
5. Client Code
The client creates shapes with different colors and draws
them.
java
public class
BridgePatternDemo { public static void main(String[] args) { // Create color implementations Color red = new RedColor(); Color green = new GreenColor(); // Create shapes with different
colors Shape redCircle = new Circle(red, 5); Shape greenSquare = new Square(green,
10); // Draw the shapes redCircle.draw(); greenSquare.draw(); } } |
6. Output
plaintext
Drawing a
circle with radius 5 Applying red
color. Drawing a
square with side 10 Applying
green color. |
Explanation
Ø Implementer
Interface (Color): Defines the interface for color implementations.
Ø Concrete
Implementers (RedColor, GreenColor): Provide concrete implementations for
colors.
Ø Abstraction
Interface (Shape): Defines the interface for shapes. Instead of using an
abstract class, we use an interface to define the draw() method.
Ø Concrete
Abstractions (Circle, Square): Implement the Shape interface and use the
Color implementer to apply color.
Advantages of Bridge Pattern (Without abstract Keyword)
- Simplified
Design: Interfaces are often simpler to work with and provide a clear
separation of concerns.
- Flexible
Implementation: This approach allows for flexible implementation while
adhering to the Bridge pattern principles.
- Maintainability:
By separating the abstraction from the implementation, it becomes easier
to extend and maintain the system.
When to Use the Bridge Pattern
Ø When
you want to decouple an abstraction from its implementation.
Ø When
you need to manage different hierarchies that can evolve independently.
Ø When
you want to provide a way to vary the implementation of an abstraction without
changing the abstraction itself.
The Bridge Design Pattern effectively separates the
abstraction and its implementation, allowing both to evolve independently. By
using interfaces instead of abstract classes, you can still achieve this
separation without the need for the abstract keyword.
The Strategy Design Pattern
The Strategy Design Pattern is a behavioral design
pattern that enables selecting an algorithm’s behavior at runtime. Instead of
implementing multiple variations of a behavior within a single class, the
Strategy pattern allows you to encapsulate different algorithms into separate classes,
making it easy to swap them out at runtime.
Key Concepts:
- Strategy
Interface: Defines a common interface for all supported algorithms.
- Concrete
Strategies: Implement different algorithms, each in a separate class.
- Context
Class: Uses the Strategy interface to call the algorithm defined by a Concrete
Strategy.
When to Use the Strategy Pattern:
Ø When
multiple algorithms or strategies exist for a specific task, and you want to
switch between them dynamically.
Ø When
you want to avoid complex conditionals (e.g., if-else or switch) to select
different behaviors.
Java Implementation of the Strategy Pattern:
1. Strategy Interface
Defines a common interface for all strategies (algorithms).
In this example, we will implement different strategies for a simple
mathematical operation (addition, subtraction, multiplication).
java
interface
Strategy { int doOperation(int num1, int num2); } |
2. Concrete Strategies
These classes implement the Strategy interface to define
specific algorithms.
Addition Strategy
java
class
OperationAdd implements Strategy { @Override public int doOperation(int num1, int
num2) { return num1 + num2; } } |
Subtraction Strategy
java
class
OperationSubtract implements Strategy { @Override public int doOperation(int num1, int
num2) { return num1 - num2; } } |
Multiplication Strategy
java
class
OperationMultiply implements Strategy { @Override public int doOperation(int num1, int
num2) { return num1 * num2; } } |
3. Context Class
The Context class maintains a reference to the current
strategy and provides a method to execute the strategy.
java
class Context
{ private Strategy strategy; // Constructor to set a strategy public Context(Strategy strategy) { this.strategy = strategy; } // Method to execute the strategy public int executeStrategy(int num1, int
num2) { return strategy.doOperation(num1,
num2); } } |
4. Client Code
The client can choose the strategy at runtime, making the
algorithm choice flexible and interchangeable.
java
public class
StrategyPatternDemo { public static void main(String[] args) { // Using the addition strategy Context context = new Context(new
OperationAdd()); System.out.println("10 + 5 =
" + context.executeStrategy(10, 5)); // Switching to subtraction strategy context = new Context(new
OperationSubtract()); System.out.println("10 - 5 =
" + context.executeStrategy(10, 5)); // Switching to multiplication
strategy context = new Context(new
OperationMultiply()); System.out.println("10 * 5 =
" + context.executeStrategy(10, 5)); } } |
5. Output:
plaintext
10 + 5 = 15 10 - 5 = 5 10 * 5 = 50 |
Explanation:
- Strategy
Interface (Strategy): Defines a method (doOperation) that each
strategy must implement.
- Concrete
Strategies (OperationAdd, OperationSubtract, OperationMultiply):
Provide different implementations of the doOperation method.
- Context
Class (Context): Allows the client to choose and switch between
different strategies at runtime.
Advantages of the Strategy Pattern:
- Open/Closed
Principle: The Strategy pattern follows the open/closed principle,
allowing new strategies (algorithms) to be added without modifying
existing code.
- Elimination
of Conditional Logic: The pattern eliminates complex conditional
statements that would otherwise be used to switch between algorithms.
- Reusability
and Flexibility: Strategies can be reused by different parts of the
program, making the design more flexible and maintainable.
Disadvantages of the Strategy Pattern:
- Overhead:
The client needs to be aware of different strategies and manage them,
which can increase the complexity.
- Increase
in the Number of Classes: Each strategy requires a separate class,
which may lead to an increase in the number of classes in the program.
Real-World Example: Sorting Algorithms
Let’s consider a real-world example where the Strategy
pattern can be used to choose different sorting algorithms dynamically.
Strategy Interface:
java
interface
SortStrategy { void sort(int[] numbers); } |
Concrete Strategies:
Bubble Sort:
java
class
BubbleSort implements SortStrategy { @Override public void sort(int[] numbers) { // Implement bubble sort algorithm System.out.println("Sorting
using Bubble Sort"); // Bubble sort logic here... } } |
Quick Sort:
java
class
QuickSort implements SortStrategy { @Override public void sort(int[] numbers) { // Implement quick sort algorithm System.out.println("Sorting
using Quick Sort"); // Quick sort logic here... } } |
Merge Sort:
java
class
MergeSort implements SortStrategy { @Override public void sort(int[] numbers) { // Implement merge sort algorithm System.out.println("Sorting
using Merge Sort"); // Merge sort logic here... } } |
Context Class:
java
class
SortingContext { private SortStrategy sortStrategy; // Set the desired sorting strategy public void setSortStrategy(SortStrategy
sortStrategy) { this.sortStrategy = sortStrategy; } // Sort the numbers using the chosen
strategy public void sortNumbers(int[] numbers) { sortStrategy.sort(numbers); } } |
Client Code:
java
public class
SortingStrategyDemo { public static void main(String[] args) { int[] numbers = {5, 3, 8, 6, 2}; SortingContext context = new
SortingContext(); // Use Bubble Sort context.setSortStrategy(new
BubbleSort()); context.sortNumbers(numbers); // Switch to Quick Sort context.setSortStrategy(new
QuickSort()); context.sortNumbers(numbers); // Switch to Merge Sort context.setSortStrategy(new
MergeSort()); context.sortNumbers(numbers); } } |
Output:
plaintext
Sorting using
Bubble Sort Sorting using
Quick Sort Sorting using
Merge Sort |
Summary:
Ø The Strategy
Pattern allows for selecting different algorithms (or strategies) at
runtime without modifying the classes that use them.
Ø By
encapsulating each algorithm in its own class and providing a common interface,
it becomes easy to switch between different strategies dynamically.
Ø It
promotes flexibility, modularity, and adherence to design
principles like Open/Closed Principle.
The Observer Design Pattern
The Observer Design Pattern is a behavioral design
pattern that defines a one-to-many dependency between objects. When one object
(the subject) changes its state, all its dependents (observers) are notified
and updated automatically. This pattern is useful for implementing distributed
event-handling systems.
Key Concepts:
- Subject
(Observable): The object being observed. It maintains a list of
observers and provides methods to add, remove, and notify observers.
- Observer:
The objects that observe the subject. They get notified when the subject
changes state.
- Loose
Coupling: The subject and observers are loosely coupled, allowing
changes in one part of the system without impacting others.
When to Use the Observer Pattern:
Ø When
you have an object that changes frequently, and you want other objects to be
automatically notified of these changes.
Ø When
you want to implement a publish-subscribe mechanism in a system.
Java Implementation of the Observer Pattern
Java provides built-in support for the Observer pattern
through the Observable class and Observer interface. However, they are
considered somewhat outdated (as of Java 9) and are not recommended for new
development. Instead, you can implement your own custom version of the Observer
pattern.
Here’s how you can implement it:
1. Subject Interface
This defines the methods for registering, removing, and
notifying observers.
java
import
java.util.ArrayList; import
java.util.List; // Subject
interface interface
Subject { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(); } |
2. Observer Interface
Defines the update method that will be called by the subject
when it changes.
java
// Observer
interface interface
Observer { void update(String message); } |
3. Concrete Subject
Implements the Subject interface and maintains a list of
observers.
java
class
NewsAgency implements Subject { private List<Observer> observers;
// List of observers private String news; // State to be observed public NewsAgency() { observers = new ArrayList<>(); } // Register an observer @Override public void registerObserver(Observer
observer) { observers.add(observer); } // Remove an observer @Override public void removeObserver(Observer
observer) { observers.remove(observer); } // Notify all registered observers @Override public void notifyObservers() { for (Observer observer : observers) { observer.update(news); // Notify
each observer of the state change } } // Update news and notify observers public void setNews(String news) { this.news = news; notifyObservers(); } } |
4. Concrete Observers
These are the classes that implement the Observer interface
and get notified of changes in the subject.
java
class
NewsChannel implements Observer { private String channelName; public NewsChannel(String channelName) { this.channelName = channelName; } // Update method to receive news from the
subject @Override public void update(String news) { System.out.println(channelName +
" received news: " + news); } } |
5. Client Code
In the client, you create a Subject (in this case, a news
agency) and attach several Observer objects (news channels). When the state of
the Subject changes, all registered observers are notified automatically.
java
public class
ObserverPatternDemo { public static void main(String[] args) { // Create the subject NewsAgency newsAgency = new
NewsAgency(); // Create observers NewsChannel bbc = new
NewsChannel("BBC"); NewsChannel cnn = new
NewsChannel("CNN"); NewsChannel alJazeera = new
NewsChannel("Al Jazeera"); // Register observers to the subject newsAgency.registerObserver(bbc); newsAgency.registerObserver(cnn);
newsAgency.registerObserver(alJazeera); // Update news (this will notify all
registered observers) newsAgency.setNews("The stock
market is up by 5% today."); // Remove one observer and update
news newsAgency.removeObserver(cnn); newsAgency.setNews("Breaking
news: Heavy rain expected tomorrow."); } } |
6. Output:
plaintext
BBC received
news: The stock market is up by 5% today. CNN received
news: The stock market is up by 5% today. Al Jazeera
received news: The stock market is up by 5% today. BBC received
news: Breaking news: Heavy rain expected tomorrow. Al Jazeera
received news: Breaking news: Heavy rain expected tomorrow. |
Explanation:
- Subject
(NewsAgency): Maintains a list of observers and notifies them when its
state (news) changes.
- Observer
(NewsChannel): Implements the Observer interface and receives updates
from the subject.
- Client:
Registers multiple observers (news channels) with the subject and
simulates state changes in the subject (news updates).
Real-World Example: Weather Monitoring System
Consider a weather station that broadcasts weather updates
to various display devices like TV screens, phones, or websites.
Subject Interface:
java
interface
WeatherSubject { void registerObserver(WeatherObserver
observer); void removeObserver(WeatherObserver
observer); void notifyObservers(); } |
Observer Interface:
java
interface
WeatherObserver { void update(float temperature, float
humidity, float pressure); } |
Concrete Observer:
java
class
WeatherStation implements WeatherSubject { private List<WeatherObserver>
observers; private float temperature; private float humidity; private float pressure; public WeatherStation() { observers = new ArrayList<>(); } @Override public void
registerObserver(WeatherObserver observer) { observers.add(observer); } @Override public void
removeObserver(WeatherObserver observer) { observers.remove(observer); } @Override public void notifyObservers() { for (WeatherObserver observer :
observers) { observer.update(temperature,
humidity, pressure); } } public void setMeasurements(float
temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; notifyObservers(); // Notify
observers when data changes } } |
Client Code:
java
class
WeatherDisplay implements WeatherObserver { private String displayName; public WeatherDisplay(String displayName)
{ this.displayName = displayName; } @Override public void update(float temperature,
float humidity, float pressure) { System.out.println(displayName +
" Display - Temp: " + temperature + ", Humidity: " +
humidity + ", Pressure: " + pressure); } } |
Output:
plaintext
public class
WeatherObserverDemo { public static void main(String[] args) { WeatherStation weatherStation = new
WeatherStation(); // Create displays WeatherDisplay phoneDisplay = new
WeatherDisplay("Phone"); WeatherDisplay tvDisplay = new
WeatherDisplay("TV"); // Register displays with the weather
station
weatherStation.registerObserver(phoneDisplay);
weatherStation.registerObserver(tvDisplay); // Update weather measurements weatherStation.setMeasurements(30,
65, 1012); weatherStation.setMeasurements(28,
70, 1010); } } |
Advantages of the Observer Pattern:
- Loose
Coupling: The subject and observers are loosely coupled, meaning the
subject doesn’t need to know much about the observers.
- Scalability:
New observers can be added easily without modifying the subject.
- Open/Closed
Principle: The subject can be extended with new observers without
changing its existing behavior.
Disadvantages of the Observer Pattern:
- Unexpected
Updates: Observers can receive updates they don’t need, leading to
performance issues.
- Memory
Leaks: If observers are not properly removed from the subject, it can
lead to memory leaks, as the subject holds references to observers.
When to Use the Observer Pattern:
Ø When
an object’s state needs to be automatically communicated to other objects
without tight coupling.
Ø When
multiple objects need to be updated in response to changes in one subject
(e.g., GUIs, event handling, messaging systems).
Conclusion:
The Observer Pattern provides an elegant way to
decouple the subject from its observers, allowing you to create systems where
multiple objects are kept in sync without tight dependencies. It’s particularly
useful in event-driven systems or any system where multiple objects need to
respond to changes in the state of another object.
The Command Design Pattern
The Command Design Pattern is a behavioral design
pattern that turns a request into a stand-alone object that contains all the
information about the request. This allows for parameterizing objects with
operations, delaying execution, queuing commands, and implementing undoable operations.
Key Concepts:
- Command
Interface: Declares an interface for executing operations.
- Concrete
Command: Implements the command interface, defining the binding
between a receiver and an action.
- Receiver:
The object that performs the actual action when the command is executed.
- Invoker:
Asks the command to carry out the request.
- Client:
Creates a command and sets its receiver.
When to Use the Command Pattern:
Ø When
you want to parameterize objects with operations (i.e., you can pass commands
like you pass data).
Ø When
you want to queue, log, or schedule requests.
Ø When
you need undo/redo functionality.
Java Implementation of the Command Pattern
1. Command Interface
Defines the method that each command must implement.
java
interface
Command { void execute(); } |
2. Concrete Commands
These classes implement the Command interface and bind an
action to a receiver.
LightOnCommand:
java
class
LightOnCommand implements Command { private Light light; // Constructor to set the receiver public LightOnCommand(Light light) { this.light = light; } // Execute the command (action to turn
the light on) @Override public void execute() { light.turnOn(); } } |
LightOffCommand:
java
class
LightOffCommand implements Command { private Light light; public LightOffCommand(Light light) { this.light = light; } @Override public void execute() { light.turnOff(); } } |
3. Receiver
The Receiver class knows how to perform the actual
operations associated with carrying out a request.
java
class Light { public void turnOn() { System.out.println("The light is
on."); } public void turnOff() { System.out.println("The light is
off."); } } |
4. Invoker
The Invoker class doesn't know how to perform the actual
operation but knows how to call the command’s execute() method.
java
class
RemoteControl { private Command command; // Set the command to be executed public void setCommand(Command command) { this.command = command; } // Execute the command public void pressButton() { command.execute(); } } |
5. Client Code
The client creates and configures the commands and the
invoker.
java
public class
CommandPatternDemo { public static void main(String[] args) { // Receiver: The light Light livingRoomLight = new Light(); // Commands Command lightOn = new
LightOnCommand(livingRoomLight); Command lightOff = new
LightOffCommand(livingRoomLight); // Invoker: The remote control RemoteControl remote = new
RemoteControl(); // Turn the light on remote.setCommand(lightOn); remote.pressButton(); // Turn the light off remote.setCommand(lightOff); remote.pressButton(); } } |
6. Output:
plaintext
The light is
on. The light is
off. |
Explanation:
- Command
Interface (Command): Declares the execute() method.
- Concrete
Commands (LightOnCommand, LightOffCommand): Implement the Command
interface and bind the action (turning the light on/off) to the receiver (Light).
- Receiver
(Light): Contains the actual operations to be performed (turning the
light on/off).
- Invoker
(RemoteControl): Holds the command and executes it when the button is
pressed.
- Client:
Sets up the commands and the invoker, and executes commands.
Real-World Example: Undo Functionality
A common use of the Command pattern is to implement
undo/redo functionality. Below is an example where the pattern is used to undo
actions like turning the light on or off.
1. Command Interface with Undo:
java
interface
Command { void execute(); void undo(); } |
2. Concrete Commands with Undo:
LightOnCommand:
java
class
LightOnCommand implements Command { private Light light; public LightOnCommand(Light light) { this.light = light; } @Override public void execute() { light.turnOn(); } @Override public void undo() { light.turnOff(); } } |
LightOffCommand:
java
class
LightOffCommand implements Command { private Light light; public LightOffCommand(Light light) { this.light = light; } @Override public void execute() { light.turnOff(); } @Override public void undo() { light.turnOn(); } } |
3. Invoker with Undo Functionality:
java
class
RemoteControlWithUndo { private Command command; private Command lastCommand; public void setCommand(Command command) { this.command = command; } public void pressButton() { command.execute(); lastCommand = command; // Remember
the last command } public void pressUndo() { if (lastCommand != null) { lastCommand.undo(); } } } |
4. Client Code with Undo:
java
public class
CommandPatternWithUndoDemo { public static void main(String[] args) { Light livingRoomLight = new Light(); Command lightOn = new
LightOnCommand(livingRoomLight); Command lightOff = new
LightOffCommand(livingRoomLight); RemoteControlWithUndo remote = new
RemoteControlWithUndo(); // Turn the light on remote.setCommand(lightOn); remote.pressButton(); // Undo the light on command (turn it
off) remote.pressUndo(); // Turn the light off remote.setCommand(lightOff); remote.pressButton(); // Undo the light off command (turn
it on) remote.pressUndo(); } } |
5. Output:
plaintext
The light is
on. The light is
off. The light is
off. The light is
on. |
Advantages of the Command Pattern:
- Encapsulation:
Commands encapsulate a request as an object, allowing for parameterization
and queuing.
- Decoupling:
The invoker (e.g., a remote control) is decoupled from the receiver (e.g.,
the light), allowing commands to be reused with different receivers.
- Undo/Redo:
The command pattern makes it easy to implement undo and redo operations.
- Logging
and Queuing: You can store and log command executions for later replay
or analysis.
Disadvantages of the Command Pattern:
- Overhead:
The Command pattern can lead to more classes and code complexity,
especially when there are many concrete commands.
- Verbose:
For simple operations, using the command pattern might be overkill, as it
introduces a lot of extra layers.
When to Use the Command Pattern:
Ø When
you need to parametrize objects with actions (commands).
Ø When
you want to implement undo/redo functionality.
Ø When
you want to queue or log operations for future execution.
Ø When
you want to decouple the sender (invoker) from the receiver.
Conclusion:
The Command Pattern is a powerful pattern for
scenarios where you want to treat requests as objects, allowing for more
flexible and dynamic behavior, like undo/redo functionality or delayed command
execution. It decouples the sender and the receiver of the request, making the
system more modular and easier to extend.
The Wrapper Design Pattern or Decorator Design Pattern
The Wrapper Design Pattern, also known as the Decorator
Design Pattern, is a structural pattern that allows behavior to be added to
individual objects, either statically or dynamically, without affecting the
behavior of other objects from the same class. The key idea is to
"wrap" an object with another object to add new functionality while
maintaining the original object's interface.
Key Concepts:
- Component
Interface: This is the common interface for both concrete components
and decorators. It defines the contract for objects that can have
responsibilities added dynamically.
- Concrete
Component: This is the original object to which additional behavior
can be added.
- Decorator
(Wrapper): Implements the same interface as the concrete component and
adds new behavior before or after delegating the call to the component.
- Concrete
Decorator: A specific implementation of a decorator that adds extra
functionality to the component.
When to Use the Decorator Pattern:
Ø When
you want to add responsibilities to individual objects dynamically without
affecting others.
Ø When
extending functionality using inheritance is not practical or would result in a
large number of subclasses.
Ø When
you want to add functionality that can be toggled on or off at runtime.
Java Implementation of the Wrapper (Decorator) Pattern
Let’s create a simple example where we have a Notifier
interface, and we can "decorate" it with additional functionality
like SMS or email notifications.
1. Component Interface
This is the base interface that defines the method that both
concrete components and decorators will implement.
java
interface
Notifier { void send(String message); } |
2. Concrete Component
The concrete implementation of the Notifier interface.
java
class
SimpleNotifier implements Notifier { @Override public void send(String message) { System.out.println("Sending
basic notification: " + message); } } |
3. Decorator Class
The base decorator class, which implements the same
interface as the component and holds a reference to a component (the object to
be decorated).
java
class
NotifierDecorator implements Notifier { protected Notifier wrappedNotifier; public NotifierDecorator(Notifier
notifier) { this.wrappedNotifier = notifier; } @Override public void send(String message) { wrappedNotifier.send(message); } } |
4. Concrete Decorators
These classes extend the base decorator and add new
functionality to the send method.
SMS Notification Decorator:
java
class
SMSNotifier extends NotifierDecorator { public SMSNotifier(Notifier notifier) { super(notifier); } @Override public void send(String message) { super.send(message); // Call the
original send method sendSMS(message); // Add additional SMS functionality } private void sendSMS(String message) { System.out.println("Sending SMS
notification: " + message); } } |
Email Notification Decorator:
java
class
EmailNotifier extends NotifierDecorator { public EmailNotifier(Notifier notifier) { super(notifier); } @Override public void send(String message) { super.send(message); // Call the
original send method sendEmail(message); // Add additional email functionality } private void sendEmail(String message) { System.out.println("Sending
email notification: " + message); } } |
5. Client Code
In the client code, you can dynamically add decorators to a Notifier
object.
java
public class
DecoratorPatternDemo { public static void main(String[] args) { // Create a simple notifier Notifier simpleNotifier = new
SimpleNotifier(); // Decorate the simple notifier with
SMS functionality Notifier smsNotifier = new
SMSNotifier(simpleNotifier); // Decorate the SMS notifier with
Email functionality Notifier emailAndSMSNotifier = new
EmailNotifier(smsNotifier); // Send notifications with both SMS
and Email emailAndSMSNotifier.send("Hello,
you've got a new message!"); // Send notifications with just SMS smsNotifier.send("This is an
SMS-only message."); } } |
6. Output:
plaintext
Sending basic
notification: Hello, you've got a new message! Sending SMS
notification: Hello, you've got a new message! Sending email
notification: Hello, you've got a new message! Sending basic
notification: This is an SMS-only message. Sending SMS
notification: This is an SMS-only message. |
Explanation:
- Component
(Notifier): Defines the common interface for both the concrete
component (SimpleNotifier) and the decorators.
- Concrete
Component (SimpleNotifier): The original notifier that sends a basic
notification.
- Decorator
(NotifierDecorator): Implements the Notifier interface and delegates
the call to the wrapped notifier object.
- Concrete
Decorators (SMSNotifier, EmailNotifier): Add additional behavior
(sending SMS or email) to the wrapped notifier.
Real-World Example: Adding I/O Features
A common real-world use of the decorator pattern is in
Java’s java.io package, where streams are wrapped with additional
functionality, such as buffering or compression.
For example:
java
BufferedReader
reader = new BufferedReader(new InputStreamReader(new
FileInputStream("file.txt"))); |
Here, BufferedReader adds buffering to the InputStreamReader,
which reads bytes from a file. The BufferedReader decorator wraps the original
reader and adds new functionality (buffering).
Advantages of the Wrapper (Decorator) Pattern:
- Extensibility:
New responsibilities can be added dynamically without modifying the
original class or other instances of the class.
- Single
Responsibility Principle: Functionality is added without the need for
subclassing, allowing responsibilities to be divided into different
classes.
- Flexible
Composition: You can compose multiple decorators to add various
behaviors.
Disadvantages of the Wrapper (Decorator) Pattern:
- Complexity:
Decorators can add many layers of complexity, especially if multiple
decorators are applied to the same object.
- Debugging
Difficulty: Tracing code execution can become harder when multiple
decorators are applied.
When to Use the Wrapper (Decorator) Pattern:
Ø When
you want to add new behavior to objects dynamically without modifying their
classes.
Ø When
you need to combine multiple behaviors (like logging, validation, etc.) without
subclassing.
Ø When
you need to modify the behavior of specific objects instead of all objects of a
class.
Conclusion:
The Decorator Pattern (Wrapper Pattern) provides a
flexible alternative to subclassing for extending functionality. It allows
behavior to be added to individual objects at runtime, giving you a powerful
tool for building extensible and maintainable systems, especially when dealing
with I/O, GUI components, or other objects where dynamic behavior is needed.
0 Comments