20
DecLearn Design Patterns in Java: Types, Examples, and Applications
Java Design Pattern - An Overview
Design Patterns in Java are reusable solutions to common problems encountered in software design. They provide a template or blueprint for solving specific design challenges in a way that is both efficient and maintainable. Design patterns in Java are not specific pieces of code but are general concepts that can be applied to enhance code structure and flexibility.
In the Design Pattern tutorial, we'll learn about what is design patterns in Java, the types of design patterns in Java, when to use them, and how they might help you write better code. Let us begin by examining "What are Design Patterns?"
What are Design Patterns in Java?
- A design pattern in Java is a general, recurring solution to a commonly encountered problem in software design that is utilized in software engineering.
- It is not a comprehensive design that can be implemented in code straight immediately.
- It is a description or model for issue solving that may be used in a variety of settings.
- Design patterns are similar to cooking recipes in that they provide a tried-and-true approach to producing consistent results without having to start from scratch each time.
Read More: |
Different Types of Software Design Principles |
What is an IoC Container or DI Container |
Types of Software Design Patterns in Java
- Creational Design Patterns in Java
- Structural Design Patterns in Java
- Behavioral Design Patterns in Java
Creational Design Patterns in Java
- Creational design patterns are one category of design patterns used in software development.
- They work on improving the flexibility and efficiency of the object production process.
- Creational design patterns define ways to create objects.
- These design patterns do not aim to compose objects directly but to introduce different ways of object creation to make the code easier and more flexible.
- Using creational design patterns, developers can maintain the creation process in one place, separately from other code, thus enabling easy maintenance and upgrade of a program.
- They basically help in building systems where the way things are created does not interfere with the overall design.
There are five types of creational design patterns:
- Singleton Design Pattern
- Factory Method Design Pattern
- Abstract Factory Design Pattern
- Builder Design Pattern
- Prototype Design Pattern
Let's start with a singleton design pattern.
1. Singleton design pattern
- Singleton Design Pattern ensures that a class has only one instance throughout the application and provides a global point to access it.
- This is useful when you need exactly one object, such as a configuration manager, to coordinate actions across the system.
Example
// Singleton class
class ScholarHatSingleton {
// Single instance of the class
private static ScholarHatSingleton instance;
// Private constructor to prevent instantiation
private ScholarHatSingleton() { }
// Public method to provide access to the instance
public static ScholarHatSingleton getInstance() {
if (instance == null) {
instance = new ScholarHatSingleton();
}
return instance;
}
// Method to display a message
public void showMessage() {
System.out.println("Welcome to ScholarHat Singleton!");
}
}
// Main class
public class SingletonExample {
public static void main(String[] args) {
// Getting the single instance of ScholarHatSingleton
ScholarHatSingleton singleton = ScholarHatSingleton.getInstance();
// Displaying the message
singleton.showMessage();
}
}
Output
Welcome to ScholarHat Singleton!
Explanation
- This code uses the Singleton design pattern. The ScholarHatSingleton class assures that just one instance of itself may be produced.
- It offers a global point of access via the getInstance() method.
- The showMessage() method displays a message, demonstrating the use of a singleton instance.
- The Main class fetches a single instance and invokes the showMessage() function.
2. Factory Method Design Pattern
- Factory Method Design Pattern defines a method for creating objects but lets subclasses decide which class to instantiate.
- This pattern is useful when you need to create objects from a family of related classes. It allows you to use a common interface while delegating the actual creation to subclasses.
- For example, a document editor may use a factory method to create different types of documents (e.g., text, spreadsheet) based on user input without knowing the specific class of the document being created.
Example
// Product Interface
interface Course {
void getCourseDetails();
}
// Concrete Product Classes
class JavaCourse implements Course {
@Override
public void getCourseDetails() {
System.out.println("ScholarHat offers an in-depth Java course.");
}
}
class PythonCourse implements Course {
@Override
public void getCourseDetails() {
System.out.println("ScholarHat offers a comprehensive Python course.");
}
}
// Factory Class
class CourseFactory {
// Method to get the course based on type
public static Course getCourse(String courseType) {
if (courseType == null) {
return null;
}
if (courseType.equalsIgnoreCase("JAVA")) {
return new JavaCourse();
} else if (courseType.equalsIgnoreCase("PYTHON")) {
return new PythonCourse();
}
return null;
}
}
// Main Class
public class FactoryPatternExample {
public static void main(String[] args) {
// Creating Java course using Factory
Course javaCourse = CourseFactory.getCourse("JAVA");
javaCourse.getCourseDetails();
// Creating Python course using Factory
Course pythonCourse = CourseFactory.getCourse("PYTHON");
pythonCourse.getCourseDetails();
}
}
Output
ScholarHat offers an in-depth Java course.
ScholarHat offers a comprehensive Python course.
Explanation
- This code demonstrates the Factory Method design pattern.
- The Course interface provides the method getCourseDetails(), which implements concrete classes such as JavaCourse and PythonCourse.
- The CourseFactory class generates instances of these concrete classes using the course type specified.
- The Main class shows how to create Java and Python courses using the factory and displays their details.
- This pattern allows you to build objects without specifying the specific class.
3. Abstract Factory design Pattern
- Abstract Factory Design Pattern provides an interface for creating families of related objects without specifying their concrete classes.
- This pattern is useful when you need to create objects that are part of a larger group, ensuring that the objects fit together well.
Example
// Abstract Product Interfaces
interface Course {
void getCourseDetails();
}
interface Certification {
void getCertificationDetails();
}
// Concrete Products for Java
class JavaCourse implements Course {
@Override
public void getCourseDetails() {
System.out.println("ScholarHat offers an in-depth Java course.");
}
}
class JavaCertification implements Certification {
@Override
public void getCertificationDetails() {
System.out.println("ScholarHat provides Java Certification after course completion.");
}
}
// Concrete Products for Python
class PythonCourse implements Course {
@Override
public void getCourseDetails() {
System.out.println("ScholarHat offers a comprehensive Python course.");
}
}
class PythonCertification implements Certification {
@Override
public void getCertificationDetails() {
System.out.println("ScholarHat provides Python Certification after course completion.");
}
}
// Abstract Factory Interface
interface CourseFactory {
Course createCourse();
Certification createCertification();
}
// Concrete Factories
class JavaFactory implements CourseFactory {
@Override
public Course createCourse() {
return new JavaCourse();
}
@Override
public Certification createCertification() {
return new JavaCertification();
}
}
class PythonFactory implements CourseFactory {
@Override
public Course createCourse() {
return new PythonCourse();
}
@Override
public Certification createCertification() {
return new PythonCertification();
}
}
// Main Class
public class AbstractFactoryPatternExample {
public static void main(String[] args) {
// Creating Java factory
CourseFactory javaFactory = new JavaFactory();
Course javaCourse = javaFactory.createCourse();
Certification javaCertification = javaFactory.createCertification();
javaCourse.getCourseDetails();
javaCertification.getCertificationDetails();
// Creating Python factory
CourseFactory pythonFactory = new PythonFactory();
Course pythonCourse = pythonFactory.createCourse();
Certification pythonCertification = pythonFactory.createCertification();
pythonCourse.getCourseDetails();
pythonCertification.getCertificationDetails();
}
}
Output
ScholarHat offers an in-depth Java course.
ScholarHat provides Java Certification after course completion.
ScholarHat offers a comprehensive Python course.
ScholarHat provides Python Certification after course completion.
Explanation
- This code uses the Abstract Factory design pattern, which entails establishing families of linked objects without identifying the concrete classes.
- In this example, Course and Certification are abstract product interfaces, whereas their concrete implementations (JavaCourse, JavaCertification, PythonCourse, and PythonCertification) are unique products.
- The CourseFactory interface provides methods for constructing these products, whereas the JavaFactory and PythonFactory are concrete factories that generate instances of the relevant products.
- The Main class demonstrates how these factories are used to generate and access course and certification information for Java and Python.
4. Builder Design Pattern
- Builder Pattern separates the process of constructing a complex object from its representation, allowing you to build different types of objects using the same construction process.
- This pattern is helpful when creating complex objects with many optional parts, making the code cleaner and more manageable.
Example
// Product Class
class ScholarHatCourse {
private String name;
private String duration;
private String level;
// Constructor (private to enforce the builder pattern)
private ScholarHatCourse(CourseBuilder builder) {
this.name = builder.name;
this.duration = builder.duration;
this.level = builder.level;
}
// Getters
public String getName() {
return name;
}
public String getDuration() {
return duration;
}
public String getLevel() {
return level;
}
// Static Builder Class
public static class CourseBuilder {
private String name;
private String duration;
private String level;
public CourseBuilder setName(String name) {
this.name = name;
return this;
}
public CourseBuilder setDuration(String duration) {
this.duration = duration;
return this;
}
public CourseBuilder setLevel(String level) {
this.level = level;
return this;
}
public ScholarHatCourse build() {
return new ScholarHatCourse(this);
}
}
}
// Main Class
public class BuilderPatternExample {
public static void main(String[] args) {
// Creating a ScholarHat course using the Builder Pattern
ScholarHatCourse javaCourse = new ScholarHatCourse.CourseBuilder()
.setName("Java Programming")
.setDuration("6 months")
.setLevel("Intermediate")
.build();
// Displaying course details
System.out.println("Course Name: " + javaCourse.getName());
System.out.println("Duration: " + javaCourse.getDuration());
System.out.println("Level: " + javaCourse.getLevel());
// Creating another course
ScholarHatCourse pythonCourse = new ScholarHatCourse.CourseBuilder()
.setName("Python Programming")
.setDuration("4 months")
.setLevel("Beginner")
.build();
// Displaying course details
System.out.println("\nCourse Name: " + pythonCourse.getName());
System.out.println("Duration: " + pythonCourse.getDuration());
System.out.println("Level: " + pythonCourse.getLevel());
}
}
output
Course Name: Java Programming
Duration: 6 months
Level: Intermediate
Course Name: Python Programming
Duration: 4 months
Level: Beginner
Explanation
- This code uses the Builder design pattern.
- ScholarHatCourse instances are created using a static inner CourseBuilder class, which accepts optional parameters such as name, duration, and level.
- This pattern is beneficial when there are several parameters in a class and makes the code more legible and maintainable.
- The Main class uses the builder to generate two-course instances and prints out their data.
5. Prototype Design Pattern
- Prototype Pattern creates new objects by copying an existing object, known as the prototype, rather than building them from scratch.
- This is useful when object creation is costly or complex, allowing for faster and more efficient object creation.
Example
// Abstract Prototype Class
abstract class ScholarHatCourse implements Cloneable {
private String courseName;
private String courseLevel;
// Getters and Setters
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public String getCourseLevel() {
return courseLevel;
}
public void setCourseLevel(String courseLevel) {
this.courseLevel = courseLevel;
}
// Abstract method to clone the object
public abstract ScholarHatCourse clone();
// Method to display course details
public void showCourseDetails() {
System.out.println("Course Name: " + courseName);
System.out.println("Course Level: " + courseLevel);
}
}
// Concrete Prototype Class for Java Course
class JavaCourse extends ScholarHatCourse {
public JavaCourse() {
this.setCourseName("Java Programming");
this.setCourseLevel("Intermediate");
}
@Override
public ScholarHatCourse clone() {
JavaCourse clone = null;
try {
clone = (JavaCourse) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
// Concrete Prototype Class for Python Course
class PythonCourse extends ScholarHatCourse {
public PythonCourse() {
this.setCourseName("Python Programming");
this.setCourseLevel("Beginner");
}
@Override
public ScholarHatCourse clone() {
PythonCourse clone = null;
try {
clone = (PythonCourse) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
// Main Class
public class PrototypePatternExample {
public static void main(String[] args) {
// Creating original Java course
JavaCourse javaCourse = new JavaCourse();
javaCourse.showCourseDetails();
// Cloning the Java course
ScholarHatCourse clonedJavaCourse = javaCourse.clone();
clonedJavaCourse.showCourseDetails();
// Creating original Python course
PythonCourse pythonCourse = new PythonCourse();
pythonCourse.showCourseDetails();
// Cloning the Python course
ScholarHatCourse clonedPythonCourse = pythonCourse.clone();
clonedPythonCourse.showCourseDetails();
}
}
Output
Course Name: Java Programming
Course Level: Intermediate
Course Name: Java Programming
Course Level: Intermediate
Course Name: Python Programming
Course Level: Beginner
Course Name: Python Programming
Course Level: Beginner
Example
- This programming uses the Prototype design pattern.
- The ScholarHatCourse class defines an abstract clone method and includes properties such as courseName and courseLevel.
- JavaCourse and PythonCourse are concrete prototypes that support the clone method, allowing their instances to be duplicated.
- The Main class generates original course instances and clones, demonstrating the ability to duplicate objects without relying on their concrete classes.
Structural Design Patterns In Java
- Structural design patterns are a type of design pattern in software development that focuses on combining classes or objects to construct larger, more complicated structures.
- They aid in organizing and managing object interactions in software systems, resulting in increased flexibility, reusability, and maintainability.
There are seven types of Structural design patterns.
- Adapter Design Pattern
- Bridge Design Pattern
- Composite Design Pattern
- Decorator Design Pattern
- Facade Design Pattern
- Flyweight Design Pattern
- Proxy Design Pattern
1. Adapter Pattern Design Pattern
- Adapter Pattern allows incompatible interfaces to work together by wrapping one interface with another.
- This pattern is like a translator that enables two systems with different languages to communicate seamlessly.
Example
// Target Interface
interface ScholarHatCourse {
void getCourseDetails();
}
// Adaptee Class (Existing Class)
class PythonCourseDetails {
public void displayCourseDetails() {
System.out.println("ScholarHat offers a comprehensive Python course.");
}
}
// Adapter Class
class PythonCourseAdapter implements ScholarHatCourse {
private PythonCourseDetails pythonCourseDetails;
public PythonCourseAdapter(PythonCourseDetails pythonCourseDetails) {
this.pythonCourseDetails = pythonCourseDetails;
}
@Override
public void getCourseDetails() {
pythonCourseDetails.displayCourseDetails();
}
}
// Main Class
public class AdapterPatternExample {
public static void main(String[] args) {
// Existing Python course details (Adaptee)
PythonCourseDetails pythonDetails = new PythonCourseDetails();
// Adapter makes PythonCourseDetails compatible with ScholarHatCourse interface
ScholarHatCourse courseAdapter = new PythonCourseAdapter(pythonDetails);
// Using the adapter to get course details
courseAdapter.getCourseDetails();
}}
Output
ScholarHat offers a comprehensive Python course.
Explanation
- This code uses the Adapter design pattern.
- Clients expect to see the ScholarHatCourse interface.
- PythonCourseDetails is an existing class (adaptee) that contains a method for displaying course data, but it does not implement the target interface.
- The PythonCourseAdapter converts PythonCourseDetails to the ScholarHatCourse interface, allowing the current class to be used anywhere the ScholarHatCourse interface is required.
- The Main class explains how to use the adaptor to retrieve course details.
2. Bridge Pattern Design Pattern
- A Bridge Pattern separates an object’s abstraction from its implementation, allowing it to vary independently.
- This pattern is useful when you want to decouple a system so that both the abstraction and implementation can evolve without affecting each other.
Example
// Implementor Interface
interface CourseContent {
void getContent();
}
// Concrete Implementor 1
class VideoContent implements CourseContent {
@Override
public void getContent() {
System.out.println("ScholarHat provides video content for the course.");
}
}
// Concrete Implementor 2
class ArticleContent implements CourseContent {
@Override
public void getContent() {
System.out.println("ScholarHat provides article content for the course.");
}
}
// Abstraction
abstract class ScholarHatCourse {
protected CourseContent content;
public ScholarHatCourse(CourseContent content) {
this.content = content;
}
public abstract void showCourseDetails();
}
// Refined Abstraction 1
class JavaCourse extends ScholarHatCourse {
public JavaCourse(CourseContent content) {
super(content);
}
@Override
public void showCourseDetails() {
System.out.println("Java Programming Course:");
content.getContent();
}
}
// Refined Abstraction 2
class PythonCourse extends ScholarHatCourse {
public PythonCourse(CourseContent content) {
super(content);
}
@Override
public void showCourseDetails() {
System.out.println("Python Programming Course:");
content.getContent();
}
}
// Main Class
public class BridgePatternExample {
public static void main(String[] args) {
// Creating a Java course with video content
ScholarHatCourse javaCourse = new JavaCourse(new VideoContent());
javaCourse.showCourseDetails();
// Creating a Python course with article content
ScholarHatCourse pythonCourse = new PythonCourse(new ArticleContent());
pythonCourse.showCourseDetails();
}
}
Output
Java Programming Course:
ScholarHat provides video content for the course.
Python Programming Course:
ScholarHat provides article content for the course.
Explanation
- This programming uses the Bridge design pattern.
- CourseContent is the implementor interface for actual implementations such as VideoContent and ArticleContent.
- ScholarHatCourse is an abstraction that contains a reference to a CourseContent object.
- The JavaCourse and PythonCourse classes extend ScholarHatCourse by implementing the showCourseDetails method, which delegates content processing to the CourseContent object.
- This strategy allows for greater flexibility in selecting different content types for different courses by divorcing course structure from content details.
3. Composite Pattern Design Pattern
- Composite Pattern lets you compose objects into tree structures to represent part-whole hierarchies.
- This pattern allows clients to treat individual objects and compositions of objects uniformly, simplifying the handling of complex structures.
Example
import java.util.ArrayList;
import java.util.List;
// Component Interface
interface ScholarHatCourse {
void showCourseDetails();
}
// Leaf Class 1
class SingleCourse implements ScholarHatCourse {
private String courseName;
public SingleCourse(String courseName) {
this.courseName = courseName;
}
@Override
public void showCourseDetails() {
System.out.println("Course: " + courseName);
}
}
// Composite Class
class CourseBundle implements ScholarHatCourse {
private List courses = new ArrayList<>();
public void addCourse(ScholarHatCourse course) {
courses.add(course);
}
public void removeCourse(ScholarHatCourse course) {
courses.remove(course);
}
@Override
public void showCourseDetails() {
for (ScholarHatCourse course : courses) {
course.showCourseDetails();
}
}
}
// Main Class
public class CompositePatternExample {
public static void main(String[] args) {
// Single courses (Leafs)
ScholarHatCourse javaCourse = new SingleCourse("Java Programming");
ScholarHatCourse pythonCourse = new SingleCourse("Python Programming");
ScholarHatCourse dataScienceCourse = new SingleCourse("Data Science");
// Course bundle (Composite)
CourseBundle programmingBundle = new CourseBundle();
programmingBundle.addCourse(javaCourse);
programmingBundle.addCourse(pythonCourse);
// Full bundle including programming and data science courses
CourseBundle fullBundle = new CourseBundle();
fullBundle.addCourse(programmingBundle);
fullBundle.addCourse(dataScienceCourse);
// Display details of the full bundle
System.out.println("ScholarHat Full Course Bundle:");
fullBundle.showCourseDetails();
}
}
Output
ScholarHat Full Course Bundle:
Course: Java Programming
Course: Python Programming
Course: Data Science
Explanation
- This code uses the Composite Design Pattern.
- The ScholarHatCourse interface represents the component interface, whereas SingleCourse is a leaf class that manages individual courses.
- The CourseBundle class is a composite that can include several ScholarHatCourse objects, allowing individual courses as well as course bundles to be treated identically.
4. Decorator Design Pattern
- Decorator Pattern adds additional responsibilities to an object dynamically without altering its structure.
- This pattern is useful when you want to enhance or modify the behavior of objects at runtime without changing their code.
Example
// Base Interface
interface Course {
String getDescription();
double getCost();
}
// Concrete Component
class BasicCourse implements Course {
@Override
public String getDescription() {
return "Basic Course";
}
@Override
public double getCost() {
return 100.00;
}
}
// Decorator
abstract class CourseDecorator implements Course {
protected Course course;
public CourseDecorator(Course course) {
this.course = course;
}
@Override
public String getDescription() {
return course.getDescription();
}
@Override
public double getCost() {
return course.getCost();
}
}
// Concrete Decorator 1
class PremiumContentDecorator extends CourseDecorator {
public PremiumContentDecorator(Course course) {
super(course);
}
@Override
public String getDescription() {
return course.getDescription() + " with Premium Content";
}
@Override
public double getCost() {
return course.getCost() + 50.00;
}
}
// Main Class
public class DecoratorPatternExample {
public static void main(String[] args) {
Course course = new BasicCourse();
System.out.println("Description: " + course.getDescription());
System.out.println("Cost: $" + course.getCost());
Course premiumCourse = new PremiumContentDecorator(course);
System.out.println("\nDescription: " + premiumCourse.getDescription());
System.out.println("Cost: $" + premiumCourse.getCost());
}
}
Output
Description: Basic Course
Cost: $100.0
Description: Basic Course with Premium Content
Cost: $150.0
Explanation
- This example shows how to use Java's Decorator Pattern to add functionality to objects dynamically.
- The BasicCourse class depicts a simple course, whereas PremiumContentDecorator improves it by adding premium material and changing the pricing.
- This demonstrates how decorators can extend object behavior without changing the original class.
5. Facade Design Pattern
- Facade Design Pattern provides a simplified interface to a complex system of classes, making it easier to interact with the system.
- This pattern is like a front desk that handles communication with a complex backend, shielding users from the complexity.
// Subsystem Classes
class CourseRegistration {
public void registerCourse(String courseName) {
System.out.println("Registering for course: " + courseName);
}
}
class PaymentProcessing {
public void processPayment(double amount) {
System.out.println("Processing payment of $" + amount);
}
}
class Certification {
public void issueCertificate(String courseName) {
System.out.println("Issuing certificate for course: " + courseName);
}
}
// Facade
class CourseFacade {
private CourseRegistration registration;
private PaymentProcessing payment;
private Certification certification;
public CourseFacade() {
registration = new CourseRegistration();
payment = new PaymentProcessing();
certification = new Certification();
}
public void enrollInCourse(String courseName, double amount) {
registration.registerCourse(courseName);
payment.processPayment(amount);
certification.issueCertificate(courseName);
}
}
// Main Class
public class FacadePatternExample {
public static void main(String[] args) {
CourseFacade facade = new CourseFacade();
facade.enrollInCourse("Java Programming", 100.00);
}
}
Output
Registering for course: Java Programming
Processing payment of $100.0
Issuing certificate for course: Java Programming
Explanation
- This example demonstrates the Facade Pattern, which streamlines interactions with complicated subsystems.
- The CourseFacade class provides a consistent interface for course registration, money processing, and certification issuance, simplifying the process of enrolling in a course with one method call.
6. Flyweight Design Pattern
- The Flyweight Design Pattern minimizes memory usage by sharing as much data as possible between similar objects.
- This pattern is useful when dealing with a large number of similar objects, as it reduces the overhead of object creation and memory consumption.
import java.util.HashMap;
import java.util.Map;
// Flyweight Interface
interface Course {
void display(String studentName);
}
// Concrete Flyweight
class ConcreteCourse implements Course {
private String courseName;
public ConcreteCourse(String courseName) {
this.courseName = courseName;
}
@Override
public void display(String studentName) {
System.out.println("Course: " + courseName + " for Student: " + studentName);
}
}
// Flyweight Factory
class CourseFactory {
private Map courseMap = new HashMap<>();
public Course getCourse(String courseName) {
Course course = courseMap.get(courseName);
if (course == null) {
course = new ConcreteCourse(courseName);
courseMap.put(courseName, course);
}
return course;
}
}
// Main Class
public class FlyweightPatternExample {
public static void main(String[] args) {
CourseFactory factory = new CourseFactory();
Course javaCourse = factory.getCourse("Java Programming");
javaCourse.display("Alice");
Course pythonCourse = factory.getCourse("Python Programming");
pythonCourse.display("Bob");
Course javaCourse2 = factory.getCourse("Java Programming");
javaCourse2.display("Charlie");
}
}
Output
Course: Java Programming for Student: Alice
Course: Python Programming for Student: Bob
Course: Java Programming for Student: Charlie
Explanation
- This example shows the Flyweight Pattern, which optimizes memory use by sharing similar objects.
- The CourseFactory manages a pool of ConcreteCourse objects, building new ones only when needed.
- This allows successive requests for the same course type to utilize existing objects, eliminating redundancy and increasing efficiency.
7. Proxy Design Pattern
- Proxy Pattern provides a placeholder or surrogate for another object to control access to it.
- This pattern is useful for managing access to resources, adding security, or optimizing performance by delaying expensive operations until they are needed.
The structure for the implementation of the Proxy Design Pattern is given below:
Example
// Subject Interface
interface ScholarHatCourse {
void enroll();
}
// Real Subject
class RealCourse implements ScholarHatCourse {
private String courseName;
public RealCourse(String courseName) {
this.courseName = courseName;
}
@Override
public void enroll() {
System.out.println("Enrolling in course: " + courseName);
}
}
// Proxy
class CourseProxy implements ScholarHatCourse {
private RealCourse realCourse;
private String courseName;
public CourseProxy(String courseName) {
this.courseName = courseName;
}
@Override
public void enroll() {
if (realCourse == null) {
realCourse = new RealCourse(courseName);
}
realCourse.enroll();
}
}
// Main Class
public class ProxyPatternExample {
public static void main(String[] args) {
ScholarHatCourse course = new CourseProxy("Java Programming");
course.enroll(); // RealCourse is created and enrolled in
}
}
Output
Explanation
- This example demonstrates the Proxy Pattern, which controls access to an object via a surrogate.
- The CourseProxy class postpones the creation of the RealCourse instance until its enroll() function is called, thus managing resource usage and adding an extra layer of control.
Behavioral Design Patterns in Java
- Behavioral design patterns are a type of design pattern in developing software that deals with the communication and interaction of objects and classes.
- They emphasize how objects and classes work together and communicate to complete tasks and responsibilities.
The following are types of behavioral patterns:
- Observer Pattern Design Pattern
- Strategy Pattern Design Pattern
- Command Pattern Design Pattern
- State Pattern Design Pattern
- Template Method Pattern Design Pattern
- Chain of Responsibility Pattern Design Pattern
- Mediator Pattern Design Pattern
- Memento Pattern Design Pattern
1. Observer Design Pattern
- Observer Design Pattern allows an object to notify other objects about changes in its state.
- This pattern is useful when you need a system where changes in one part of the system automatically update other parts, like a news feed where subscribers get updates when new articles are published.
Example
import java.util.ArrayList;
import java.util.List;
// Subject Interface
interface CourseSubject {
void addObserver(CourseObserver observer);
void removeObserver(CourseObserver observer);
void notifyObservers();
}
// Concrete Subject
class Course implements CourseSubject {
private List observers = new ArrayList<>();
private String courseName;
public Course(String courseName) {
this.courseName = courseName;
}
@Override
public void addObserver(CourseObserver observer) {
observers.add(observer);
}
@Override
public void removeObserver(CourseObserver observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (CourseObserver observer : observers) {
observer.update(courseName);
}
}
public void changeCourseName(String newCourseName) {
this.courseName = newCourseName;
notifyObservers();
}
}
// Observer Interface
interface CourseObserver {
void update(String courseName);
}
// Concrete Observer
class Student implements CourseObserver {
private String studentName;
public Student(String studentName) {
this.studentName = studentName;
}
@Override
public void update(String courseName) {
System.out.println(studentName + " received update: " + courseName);
}
}
// Main Class
public class ObserverPatternExample {
public static void main(String[] args) {
Course course = new Course("Java Programming");
Student student1 = new Student("Alice");
Student student2 = new Student("Bob");
course.addObserver(student1);
course.addObserver(student2);
course.changeCourseName("Advanced Java Programming");
}
}
Output
Alice received update: Advanced Java Programming
Bob received update: Advanced Java Programming
Explanation
- This example introduces the Observer Pattern, which enables an object (the Course) to tell multiple observers (the Student instances) of changes.
- When the Course's name changes, it tells all registered observers, allowing them to respond to the change.
- This pattern decouples the subject and observer, allowing for dynamic updates without direct connection.
2. Strategy Design Pattern
- Strategy Design Pattern enables selecting an algorithm at runtime without altering the code that uses it.
- This pattern is proper when you have multiple ways to operate, such as different sorting algorithms, and you want to choose the best one dynamically.
Read More: |
Data Structures Sorting: Types and Examples Explained |
Sort in Python- An Easy Way to Learn |
Example
// Strategy Interface
interface PaymentStrategy {
void pay(double amount);
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Credit Card.");
}
}
class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal.");
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(double amount) {
paymentStrategy.pay(amount);
}
}
// Main Class
public class StrategyPatternExample {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(100.00);
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(200.00);
}
}
Output
Paid $100.0 using Credit Card.
Paid $200.0 using PayPal
Explanation
- This example demonstrates the Strategy Pattern, which enables interchangeable algorithms or tactics.
- The ShoppingCart class can transition between multiple PaymentStrategy implementations (CreditCardPayment and PayPalPayment) at runtime, allowing for more flexible payment options at checkout.
- This solution separates the payment method from the cart, allowing for simple adjustments and extensions.
3. Command Design Pattern
- Command Design Patternturns requests or simple operations into objects, allowing them to be executed, undone, or queued.
- This pattern is handy for implementing action history or undo mechanisms, like in a text editor where you can undo previous actions.
Example
// Command Interface
interface Command {
void execute();
}
// Concrete Command
class EnrollInCourseCommand implements Command {
private String courseName;
public EnrollInCourseCommand(String courseName) {
this.courseName = courseName;
}
@Override
public void execute() {
System.out.println("Enrolled in course: " + courseName);
}
}
// Invoker
class CourseRegistration {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void performAction() {
command.execute();
}
}
// Main Class
public class CommandPatternExample {
public static void main(String[] args) {
Command enrollCommand = new EnrollInCourseCommand("Java Programming");
CourseRegistration registration = new CourseRegistration();
registration.setCommand(enrollCommand);
registration.performAction();
}
}
Output
Enrolled in course: Java Programming
Explanation
- This example demonstrates the Command Pattern, which wraps a request as an object, enabling customization and request queueing.
- The CourseRegistration class functions as an invoker, carrying out the EnrollInCourseCommand, which handles the specific operation of registering in a course.
- This technique allows you greater flexibility when controlling commands and their execution.
4. StateDesign Pattern
- State Design Pattern allows an object to change its behavior when its internal state changes.
- This pattern is useful for managing state-dependent behavior, such as a vending machine that changes its operations based on whether it's idle, accepting coins, or dispensing items.
Example
// State Interface
interface CourseState {
void handle();
}
// Concrete States
class NotStartedState implements CourseState {
@Override
public void handle() {
System.out.println("Course not started yet.");
}
}
class InProgressState implements CourseState {
@Override
public void handle() {
System.out.println("Course is in progress.");
}
}
class CompletedState implements CourseState {
@Override
public void handle() {
System.out.println("Course is completed.");
}
}
// Context
class Course {
private CourseState state;
public void setState(CourseState state) {
this.state = state;
}
public void request() {
state.handle();
}
}
// Main Class
public class StatePatternExample {
public static void main(String[] args) {
Course course = new Course();
course.setState(new NotStartedState());
course.request();
course.setState(new InProgressState());
course.request();
course.setState(new CompletedState());
course.request();
}
}
Output
Course not started yet.
Course is in progress.
Course is completed
Explanation
- This example shows the State Pattern, which lets an object adapt its behavior when its internal state changes.
- The Course class transitions between three states (NotStartedState, InProgressState, and CompletedState), each of which implements the handle() method to give appropriate behavior.
- This design simplifies state management and increases flexibility in dealing with state-dependent behavior.
5. Template Method Design Pattern
- Template Method Design Pattern defines the skeleton of an algorithm in a base class but lets subclasses override specific steps.
- This pattern ensures a consistent structure while allowing customization, like a cooking recipe where the steps are fixed, but the ingredients can vary.
Example
// Abstract Class
abstract class CourseTemplate {
public final void enroll() {
selectCourse();
makePayment();
sendConfirmation();
}
protected abstract void selectCourse();
protected abstract void makePayment();
protected void sendConfirmation() {
System.out.println("Confirmation sent.");
}
}
// Concrete Class 1
class JavaCourse extends CourseTemplate {
@Override
protected void selectCourse() {
System.out.println("Java Programming course selected.");
}
@Override
protected void makePayment() {
System.out.println("Payment made for Java course.");
}
}
// Concrete Class 2
class PythonCourse extends CourseTemplate {
@Override
protected void selectCourse() {
System.out.println("Python Programming course selected.");
}
@Override
protected void makePayment() {
System.out.println("Payment made for Python course.");
}
}
// Main Class
public class TemplateMethodPatternExample {
public static void main(String[] args) {
CourseTemplate javaCourse = new JavaCourse();
javaCourse.enroll();
CourseTemplate pythonCourse = new PythonCourse();
pythonCourse.enroll();
}
}
Output
Java Programming course selected.
Payment made for Java course.
Confirmation sent.
Python Programming course selected.
Payment made for Python course.
Confirmation sent.
Explanation
- This example introduces the Template Method Pattern, which outlines the skeleton of an algorithm in a base class while allowing subclasses to implement individual phases.
- The CourseTemplate class defines the enroll() method, including steps such as selectCourse() and makePayment(), although actual classes (JavaCourse and PythonCourse) supply their own implementations.
- This design maintains consistency while allowing for customization.
6. Chain of Responsibility Design Pattern
- Chain of Responsibility Design Pattern passes a request along a chain of handlers until one of them handles it.
- This pattern is useful for processing requests in a sequence, such as filtering and handling user inputs or processing various levels of a request in a web application.
Example
// Handler Interface
interface CourseHandler {
void setNextHandler(CourseHandler handler);
void handleRequest(String request);
}
// Concrete Handlers
class EnrollmentHandler implements CourseHandler {
private CourseHandler nextHandler;
@Override
public void setNextHandler(CourseHandler handler) {
this.nextHandler = handler;
}
@Override
public void handleRequest(String request) {
if (request.equals("Enroll")) {
System.out.println("Handling enrollment.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
class PaymentHandler implements CourseHandler {
private CourseHandler nextHandler;
@Override
public void setNextHandler(CourseHandler handler) {
this.nextHandler = handler;
}
@Override
public void handleRequest(String request) {
if (request.equals("Payment")) {
System.out.println("Handling payment.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
class CertificationHandler implements CourseHandler {
private CourseHandler nextHandler;
@Override
public void setNextHandler(CourseHandler handler) {
this.nextHandler = handler;
}
@Override
public void handleRequest(String request) {
if (request.equals("Certificate")) {
System.out.println("Handling certification.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
// Main Class
public class ChainOfResponsibilityPatternExample {
public static void main(String[] args) {
CourseHandler enrollment = new EnrollmentHandler();
CourseHandler payment = new PaymentHandler();
CourseHandler certification = new CertificationHandler();
enrollment.setNextHandler(payment);
payment.setNextHandler(certification);
enrollment.handleRequest("Payment");
enrollment.handleRequest("Certificate");
}
}
Output
Handling payment.
Handling certification.
Explanation
- This example demonstrates the Chain of Responsibility Pattern, which involves many handlers processing requests in a chain.
- Each handler (EnrollmentHandler, PaymentHandler, and CertificationHandler) determines whether it can handle the request; if not, it forwards the request to the next handler in the chain.
- This pattern encourages flexibility in request handling and allows for dynamic modifications to the processing order.
7. Mediator Design Pattern
- Mediator Design Patterncentralizes communication between objects, reducing the dependency between them.
- This pattern is useful for managing complex interactions in a system, such as coordinating the components of a chat application to handle user messages and notifications.
Example
import java.util.ArrayList;
import java.util.List;
// Mediator Interface
interface CourseMediator {
void registerStudent(Student student);
void notifyStudents(String message);
}
// Concrete Mediator
class Course implements CourseMediator {
private List students = new ArrayList<>();
@Override
public void registerStudent(Student student) {
students.add(student);
}
@Override
public void notifyStudents(String message) {
for (Student student : students) {
student.update(message);
}
}
}
// Colleague
class Student {
private String name;
private CourseMediator mediator;
public Student(String name, CourseMediator mediator) {
this.name = name;
this.mediator = mediator;
mediator.registerStudent(this);
}
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
// Main Class
public class MediatorPatternExample {
public static void main(String[] args) {
Course course = new Course();
Student student1 = new Student("Alice", course);
Student student2 = new Student("Bob", course);
course.notifyStudents("Course registration opens tomorrow.");
}
}
Output
Alice received message: Course registration opens tomorrow.
Bob received message: Course registration opens tomorrow.
Explanation
- This example shows the Mediator Pattern, which centralizes communication between objects.
- The Course class serves as an intermediary, facilitating interactions between Student instances. Instead of conversing directly, students rely on the Course to broadcast messages, which simplifies communication and reduces object dependencies.
8. Memento Design Pattern
- The Memento Design Pattern captures and restores an object's state without exposing its internal structure.
- This pattern is helpful in saving and retrieving an object's state, like implementing an undo feature in a text editor where you can revert to a previous version of the document.
Example
// Memento
class CourseMemento {
private String courseName;
public CourseMemento(String courseName) {
this.courseName = courseName;
}
public String getCourseName() {
return courseName;
}
}
// Originator
class Course {
private String courseName;
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public CourseMemento save() {
return new CourseMemento(courseName);
}
public void restore(CourseMemento memento) {
this.courseName = memento.getCourseName();
}
@Override
public String toString() {
return "Course name: " + courseName;
}
}
// Caretaker
class CourseCaretaker {
private CourseMemento memento;
public void saveMemento(CourseMemento memento) {
this.memento = memento;
}
public CourseMemento getMemento() {
return memento;
}
}
// Main Class
public class MementoPatternExample {
public static void main(String[] args) {
Course course = new Course();
CourseCaretaker caretaker = new CourseCaretaker();
course.setCourseName("Java Programming");
System.out.println(course);
caretaker.saveMemento(course.save());
course.setCourseName("Advanced Java Programming");
System.out.println(course);
course.restore(caretaker.getMemento());
System.out.println(course);
}
}
Output
Course name: Java Programming
Course name: Advanced Java Programming
Course name: Java Programming
Explanation
- This example demonstrates the Memento Pattern, which captures and restores an object's state while preserving its underlying structure.
- The Course class can save its current state as a CourseMemento and then restore it later.
- The CourseCaretaker saves the remembrance, allowing the Course to return to a prior state as necessary, effectively handling state preservation and restoration.
When to Use Design Patterns?
- Reoccurring Problems: Use design patterns when you meet reoccurring design issues with well-defined answers. Design patterns provide tried-and-true approaches to typical program design difficulties.
- Flexibility and Reusability: Use design patterns to encourage code reuse, flexibility, and maintenance. They assist in structuring code so that it is easier to adapt and extend as requirements change.
- Design Principles: Use design patterns to implement core design principles including dividing up concerns, encapsulation, and dependency inversion. They aid in improving modularity and decreasing dependency between components.
- Communication: Design patterns can help team members communicate more effectively. Design patterns provide a shared vocabulary and understanding of how to tackle certain challenges, which aids cooperation and code comprehension.
- Performance: Design patterns can sometimes increase performance by minimizing resource utilization, lowering overhead, or increasing code execution efficiency.
How to Choose the Right Design Pattern?
Choosing a design pattern depends on the problem that you are solving and the requirements for solving that problem; consider the following:
- Problem Type: Match the pattern to the nature of the problem, such as creational, structural, or behavioral issues.
- Flexibility: Assess if the pattern allows flexibility and scalability during future changes.
- Complexity: Assure the complexity of the pattern doesn't complexify your design but simplifies it.
- Common Solutions: Observe well-used patterns in similar circumstances and leverage solutions proven to work.
Choosing the right pattern will increase the maintainability and effectiveness of your design.
Read more: |
.Net Design Patterns Interview Questions, You Must Know! |
Most Frequently Asked Software Architect Interview Questions and Answers |
Summary
This tutorial presents an in-depth overview of design patterns, including definitions and implementations. It also includes creational, structural, and behavioral patterns. It begins with an overview of each category, followed by extensive explanations and sample code for patterns including Singleton, Factory Method, Adapter, and Observer. This tutorial shows when to utilize design patterns, how to select the proper one, and the best practices for successful implementation. Also, consider our Software Architecture and Design Certification Training for a better understanding of other Java concepts.