Let’s Understand Some Design Patterns 🌸 — It’s Easy! 🥳
Let’s Understand Some Design Patterns — It’s Easy!
What the heck is a design pattern?
Design patterns are recurring solutions to common problems that arise during software design and development. They are not specific to a particular programming language or technology but are general guidelines and best practices that can be applied across various programming languages and platforms.
What would you achieve?
Reusability, Maintainability, Communication, Scalability, Quality, Best Practices.
Design patterns are typically categorized into several groups based on their purpose and the problems they address. The most commonly recognized categories of design patterns are:
-
Creational Patterns
-
Structural Patterns
-
Behavioral Patterns
-
Concurrency Patterns
-
Architectural Patterns
-
Idioms
We are going to explore first three categories.
Lets Dive In!
Creational Patterns
These patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
Examples include Singleton, Factory Method, Abstract Factory, Builder, and Prototype patterns.
I’ll provide an explanation of Singleton and Factory Method. Feel free to explore the remaining design patterns on your own! 😆
Singleton
Used to ensure that only one instance of a class is created, and the same instance is shared across the entire application.
This is how our Singleton pattern will appear in the diagram. You can skip the ‘Build’ and ‘Settings’ classes in this image and all the following images. These classes are system-generated. Our focus should be on the ‘Singleton’ class and the ‘Main’ class.
Singleton pattern is used to ensure that only one instance of a class is created, and the same instance is shared across the entire application.
The Singleton pattern is a design pattern that restricts a class to have only one instance, while providing a global point of access to this instance.
Here is an example of the Singleton pattern in Java:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Main {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("singleton1 == singleton2: " + (singleton1 == singleton2));
}
}
In this example, the Singleton class has a private constructor, which ensures that no other class can create an instance of this class. The get Instance method is a public static method that creates an instance of the Singleton class, if it doesn’t already exist, and returns a reference to it. This method is used to access the single instance of the Singleton class from anywhere in the application.
Factory Method
Used to provide a centralized point for object creation and reducing the burden of creating objects in the application.
The Factory Pattern is a creational design pattern that provides a way to create objects without specifying the exact class of object that will be created. It is a pattern that is commonly used in Java, especially when working with the Spring framework.
Can you name a specific type of object frequently created using this pattern in Spring framework?
Here’s a clue: What’s in the bowl? Take a closer look through your Java lens!
Here’s an example of a simple implementation of the Factory Pattern in Java:
interface Shape {
void draw();
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
class Square implements Shape {
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
class ShapeFactory {
public Shape getShape(String shapeType) {
if(shapeType == null) {
return null;
}
if(shapeType.equalsIgnoreCase("RECTANGLE")) {
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}
}
class FactoryPatternDemo {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();
Shape shape1 = shapeFactory.getShape("RECTANGLE");
shape1.draw();
Shape shape2 = shapeFactory.getShape("SQUARE");
shape2.draw();
}
}
In this example, the ShapeFactory class is the factory class that returns an object of the desired class based on the input shapeType. The Shape interface defines the common behavior that must be implemented by all concrete classes (Rectangle and Square). When the client calls the getShape method of the ShapeFactory class, it returns the object of the desired class, and the client can call the draw method without worrying about the specific implementation.
Structural Patterns
These patterns focus on the composition of classes or objects and how they form larger structures.
Examples include Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy patterns.
I’ll provide an explanation of Decorator. As i said earlier, feel free to explore the remaining design patterns on your own! 😆
Decorator 🌸🌺🌼
Used to add or extend the functionality of an existing object in a transparent manner.
The Decorator pattern is a structural design pattern that allows adding new behavior to objects dynamically by wrapping them in an object of a decorator class. This pattern provides a flexible alternative to using inheritance to extend the behavior of an object.
Here is a simple example of the Decorator pattern in Java:
interface Component {
void operation();
}
class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("ConcreteComponent operation");
}
}
abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
System.out.println("ConcreteDecoratorA operation");
}
}
class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
System.out.println("ConcreteDecoratorB operation");
}
}
public class Main {
public static void main(String[] args) {
// Create a ConcreteComponent object
ConcreteComponent concreteComponent = new ConcreteComponent();
// Create a ConcreteDecoratorA object and pass the ConcreteComponent object to its constructor
ConcreteDecoratorA concreteDecoratorA = new ConcreteDecoratorA(concreteComponent);
// Create a ConcreteDecoratorB object and pass the ConcreteDecoratorA object to its constructor
ConcreteDecoratorB concreteDecoratorB = new ConcreteDecoratorB(concreteDecoratorA);
// Call the operation method on the ConcreteDecoratorB object
concreteDecoratorB.operation();
}
}
In this example, the Component interface defines the basic operation. The ConcreteComponent class implements the basic operation. The Decorator abstract class wraps the Component object and defines the basic structure of the decorators. The ConcreteDecoratorA and ConcreteDecoratorB classes are concrete decorators that add their own behaviour to the basic operation.
Behavioral Patterns
These patterns are concerned with communication between objects, how objects operate and interact, and how they can be organized to accomplish specific tasks. Examples include Observer, Strategy, Template, Command, State, Memento, Chain of Responsibility, and Visitor patterns.
Observer
The Observer pattern is a design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any changes to its state.
This pattern allows multiple objects to observe a subject and be notified of any changes to its state, without tightly coupling the subject and observer objects. The observer pattern is often used in event-driven systems, where objects subscribe to receive notifications of certain events, such as changes in the state of another object. The observer pattern promotes the loose coupling of objects, which makes it easier to maintain and modify the system.
Here’s an example of the Observer design pattern in Java:
interface Observer {
void update(String message);
}
class Subject {
private List<Observer> observers = new ArrayList<>();
public void attach(Observer observer) {
observers.add(observer);
}
public void detach(Observer observer) {
observers.remove(observer);
}
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
class ConcreteObserverA implements Observer {
@Override
public void update(String message) {
System.out.println("Observer A received message: " + message);
}
}
class ConcreteObserverB implements Observer {
@Override
public void update(String message) {
System.out.println("Observer B received message: " + message);
}
}
public class Main {
public static void main(String[] args) {
Subject subject = new Subject();
Observer observerA = new ConcreteObserverA();
Observer observerB = new ConcreteObserverB();
subject.attach(observerA);
subject.attach(observerB);
subject.notifyObservers("Hello, observers!");
}
}
In this example, the Subject class maintains a list of Observer objects and provides methods to attach and detach observers. The ConcreteObserverA and ConcreteObserverB classes implement the Observer interface and provide their own update method, which gets called when the subject notifies its observers. Finally, the Main class creates an instance of the Subject class, attaches two Observer objects to it, and calls the notifyObservers method.
Strategy
Used to encapsulate algorithms or strategies into separate objects, allowing them to be interchangeable.
Here’s an example of the strategy pattern in Java:
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardPayment implements PaymentStrategy {
private String name;
private String cardNumber;
public CreditCardPayment(String name, String cardNumber) {
this.name = name;
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using Credit Card");
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
private String password;
public PayPalPayment(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using PayPal");
}
}
class ShoppingCart {
private List<Item> items;
public ShoppingCart() {
this.items = new ArrayList<>();
}
public void addItem(Item item) {
this.items.add(item);
}
public void removeItem(Item item) {
this.items.remove(item);
}
public int calculateTotal() {
int sum = 0;
for (Item item : items) {
sum += item.getPrice();
}
return sum;
}
public void pay(PaymentStrategy paymentMethod) {
int amount = calculateTotal();
paymentMethod.pay(amount);
}
}
class Item {
private String upcCode;
private int price;
public Item(String upcCode, int price) {
this.upcCode = upcCode;
this.price = price;
}
public String getUpcCode() {
return upcCode;
}
public int getPrice() {
return price;
}
}
public class Main {
public static void main(String[] args) {
// Create some items
Item item1 = new Item("12345", 10);
Item item2 = new Item("67890", 20);
// Add the items to a shopping cart
ShoppingCart cart = new ShoppingCart();
cart.addItem(item1);
cart.addItem(item2);
// Pay with a credit card
PaymentStrategy creditCard = new CreditCardPayment("John Doe", "1234567890");
cart.pay(creditCard);
// Pay with PayPal
PaymentStrategy payPal = new PayPalPayment("john.doe@example.com", "password123");
cart.pay(payPal);
}
}
In the example, we have two payment strategies, CreditCardPayment and PayPalPayment. The shopping cart holds a list of items and provides a pay method which takes a PaymentStrategy instance. The PaymentStrategy interface defines a pay method, which both concrete payment strategies implement. The concrete payment strategies are interchangeable, and the shopping cart can work with any payment strategy that implements the PaymentStrategy interface.
Template
Used to define a blueprint for methods in a superclass, but allow subclasses to override some or all of the methods as required.
The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in the superclass, but lets subclasses override specific steps of the algorithm without changing its structure. This pattern is commonly used in software development, particularly in the implementation of graphical user interfaces.
The Template Method design pattern defines the steps of an algorithm and allows subclasses to provide their own implementation for one or more steps. This pattern is useful when you need to define an algorithm that is composed of a set of reusable steps, but the actual implementation of these steps may change from one use case to another.
Here’s a simple example of the Template Method pattern in Java:
abstract class DataProcessor {
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
public void processDataAndWriteToFile() {
readData();
processData();
writeData();
}
}
class CSVDataProcessor extends DataProcessor {
protected void readData() {
System.out.println("Reading data from a CSV file");
}
protected void processData() {
System.out.println("Processing data in CSV format");
}
protected void writeData() {
System.out.println("Writing data to a CSV file");
}
}
class XMLDataProcessor extends DataProcessor {
protected void readData() {
System.out.println("Reading data from an XML file");
}
protected void processData() {
System.out.println("Processing data in XML format");
}
protected void writeData() {
System.out.println("Writing data to an XML file");
}
}
public class Main {
public static void main(String[] args) {
DataProcessor csvProcessor = new CSVDataProcessor();
DataProcessor xmlProcessor = new XMLDataProcessor();
System.out.println("Processing and writing CSV data:");
csvProcessor.processDataAndWriteToFile();
System.out.println("\nProcessing and writing XML data:");
xmlProcessor.processDataAndWriteToFile();
}
}
In this example, the DataProcessor class defines the template for processing data and writing it to a file. The CSVDataProcessor and XMLDataProcessor classes both extend DataProcessor and provide their own implementation for reading, processing, and writing data. The processDataAndWriteToFile() method in DataProcessor serves as the template method, defining the order in which the steps should be executed, while the readData(), processData(), and writeData() methods allow the subclasses to provide their own implementation for these steps.
THE END!
I hope it was fun 🤩 If you enjoyed the content and would like to show your appreciation, you can support me in two ways:
-
Give it a Clap: Just click on the 👏 button at the end of the article. Each clap is a virtual pat on the back and a way to let me know that you liked the blog.
-
Buy Me a Coffee: If you’re feeling extra generous and would like to support my work further. To leave a tip, simply click on the “Tip” button, and any amount you’re comfortable with is greatly appreciated. Your contribution will keep me fueled and motivated to create more content in the future.
Your support means a lot to me, and it encourages me to continue sharing my knowledge and experiences. I’m grateful for each and every reader who finds value in what I write. 🙏
❤️ Feeling the love? Give me a tip on Medium!