Extremely Serious

Category: Design Pattern

J2EE Design Patterns

Design patterns are fundamental concepts in software engineering that provide reusable solutions to common problems. They help in creating more maintainable and scalable software. In this article, we will explore several design patterns and provide Java examples for each pattern's practical application.

Presentation Tier

Intercepting Filter Pattern

The Intercepting Filter Pattern is used when the intent is to perform pre or post-processing with the request or response of an application, typically in web applications. In Java, you can implement this pattern using servlet filters. Here's an example:

public class LoggingFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        // Pre-processing logic
        // ...

        // Pass the request to the next filter or servlet in the chain
        chain.doFilter(request, response);

        // Post-processing logic
        // ...
    }

    // Other methods for initialization and cleanup
}

Front Controller Pattern

The Front Controller Pattern is employed when the intent is to have a centralized request handling mechanism that dispatches requests to the appropriate handlers. In Java EE applications, a servlet can act as a front controller. Here's a simplified example:

public class FrontControllerServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        // Determine the appropriate handler for the request
        // ...

        // Dispatch to the appropriate handler
        // ...
    }
}

Business Tier

Business Delegate Pattern

The Business Delegate Pattern is used when the intent is to provide a single point of entry for clients to access business services, especially when separating the presentation tier and the business tier. Here's a simple Java example:

public class BusinessDelegate {
    private BusinessService businessService;

    public BusinessDelegate() {
        businessService = new BusinessService();
    }

    public void doTask() {
        // Delegate the task to the business service
        businessService.doTask();
    }
}

Composite Entity Pattern

The Composite Entity Pattern is often used in database applications to manage a graph of persistent objects using a primary composite entity. Here's a basic Java example:

public class CompositeEntity {
    private CoarseGrainedObject cgo = new CoarseGrainedObject();

    public void setData(String data1, String data2) {
        cgo.setData(data1, data2);
    }

    public String[] getData() {
        return cgo.getData();
    }
}

Service Locator Pattern

The Service Locator Pattern is employed when the intent is to locate services using JNDI (Java Naming and Directory Interface) lookup. Here's a simplified example:

public class ServiceLocator {
    public Service getService(String serviceName) {
        // Perform JNDI lookup to obtain the service
        // ...

        // Return the service
        return service;
    }
}

Session Facade Pattern

The Session Facade Pattern can be used in Java EE applications when the intent is to encapsulate business-tier components and expose a coarse-grained service to remote clients. Here's a basic example:

public class SessionFacade {
    private BusinessComponent businessComponent;

    public SessionFacade() {
        businessComponent = new BusinessComponent();
    }

    public void performBusinessOperation() {
        // Encapsulate business logic and provide a coarse-grained service
        businessComponent.performOperation();
    }
}

Transfer Object (Value Object) Pattern

The Transfer Object Pattern is used to pass data with multiple attributes in one shot from the client to the server. In Java, this pattern is typically implemented using POJOs (Plain Old Java Objects) with getter and setter methods, and the objects must be serializable. Here's a simple example:

public class TransferObject implements Serializable {
    private String attribute1;
    private int attribute2;

    // Getter and setter methods for attributes
    // ...
}

Value List Handler Pattern

The Value List Handler Pattern is used for managing the results of search operations, especially when results can be paged and traversed iteratively. Here's a basic example:

public class ValueListHandler {
    public List<ValueObject> getPage(int pageNumber) {
        // Retrieve a specific page of results
        // ...
    }
}

Integration Tier

Data Access Object (DAO) Pattern

The Data Access Object (DAO) Pattern is commonly used in Java EE applications to separate data persistence logic into a separate layer. Here's a simplified example:

public class UserDao {
    public User getUserById(int userId) {
        // Data access logic to retrieve a user from the database
        // ...
    }
}

Service Activator Pattern

The Service Activator Pattern is used to invoke a service asynchronously. An example of this pattern is a JMS (Java Message Service) listener that waits for a messaging request and delegates it to an appropriate service. Here's a high-level example:

public class ServiceActivator {
    public void activateService(ServiceRequest request) {
        // Asynchronously invoke the service based on the request
        // ...
    }
}

In conclusion, design patterns are invaluable tools for designing and architecting software systems. They provide tested and proven solutions to recurring design problems. By applying these design patterns in your Java applications, you can enhance code reusability, maintainability, and scalability, ultimately leading to more robust and efficient software.

Behavioral Design Patterns

Behavioral design patterns focus on how objects and classes interact and communicate with each other. They deal with the responsibilities of objects and the delegation of tasks among them. These patterns help you define clear and efficient ways for objects to collaborate while keeping your codebase flexible and maintainable.

Chain of Responsibility Pattern

The Chain of Responsibility Pattern passes a request along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain.

Example

// Handler interface
abstract class Handler {
    protected Handler successor;

    public void setSuccessor(Handler successor) {
        this.successor = successor;
    }

    public abstract void handleRequest(Request request);
}

// Request class
class Request {
    private String requestType;
    private String requestDescription;

    public Request(String requestType, String requestDescription) {
        this.requestType = requestType;
        this.requestDescription = requestDescription;
    }

    public String getRequestType() {
        return requestType;
    }

    public String getRequestDescription() {
        return requestDescription;
    }
}

// Concrete Handlers
class Clerk extends Handler {
    @Override
    public void handleRequest(Request request) {
        if (request.getRequestType().equalsIgnoreCase("Leave")) {
            System.out.println("Clerk: Approving leave request - " + request.getRequestDescription());
        } else if (successor != null) {
            successor.handleRequest(request);
        }
    }
}

class Supervisor extends Handler {
    @Override
    public void handleRequest(Request request) {
        if (request.getRequestType().equalsIgnoreCase("Purchase")) {
            System.out.println("Supervisor: Approving purchase request - " + request.getRequestDescription());
        } else if (successor != null) {
            successor.handleRequest(request);
        }
    }
}

class Manager extends Handler {
    @Override
    public void handleRequest(Request request) {
        if (request.getRequestType().equalsIgnoreCase("Raise Salary")) {
            System.out.println("Manager: Approving salary raise request - " + request.getRequestDescription());
        } else {
            System.out.println("Manager: Cannot handle this request.");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Handler clerk = new Clerk();
        Handler supervisor = new Supervisor();
        Handler manager = new Manager();

        clerk.setSuccessor(supervisor);
        supervisor.setSuccessor(manager);

        Request request1 = new Request("Leave", "2 days leave");
        Request request2 = new Request("Purchase", "Office supplies");
        Request request3 = new Request("Raise Salary", "Employee X");

        clerk.handleRequest(request1);
        clerk.handleRequest(request2);
        clerk.handleRequest(request3);
    }
}

Command Pattern

The Command Pattern encapsulates a request as an object, thereby allowing you to parameterize clients with queues, requests, and operations. It also enables undoable operations and transactional behavior.

Example

// Receiver
class Light {
    void turnOn() {
        System.out.println("Light is on");
    }

    void turnOff() {
        System.out.println("Light is off");
    }
}

// Command interface
interface Command {
    void execute();
}

// Concrete Commands
class TurnOnLightCommand implements Command {
    private Light light;

    public TurnOnLightCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.turnOn();
    }
}

class TurnOffLightCommand implements Command {
    private Light light;

    public TurnOffLightCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.turnOff();
    }
}

// Invoker
class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

public class Main {
    public static void main(String[] args) {
        Light livingRoomLight = new Light();
        Command turnOnCommand = new TurnOnLightCommand(livingRoomLight);
        Command turnOffCommand = new TurnOffLightCommand(livingRoomLight);

        RemoteControl remoteControl = new RemoteControl();

        remoteControl.setCommand(turnOnCommand);
        remoteControl.pressButton();

        remoteControl.setCommand(turnOffCommand);
        remoteControl.pressButton();
    }
}

Interpreter Pattern

The Interpreter Pattern is used to define a grammar for interpreting a language and provides a way to evaluate expressions in the language.

Example

// Context
class Context {
    private String input;
    private int output;

    public Context(String input) {
        this.input = input;
    }

    public String getInput() {
        return input;
    }

    public void setOutput(int output) {
        this.output = output;
    }

    public int getOutput() {
        return output;
    }
}

// Abstract Expression
interface Expression {
    void interpret(Context context);
}

// Terminal Expression
class NumberExpression implements Expression {
    private int number;

    public NumberExpression(int number) {
        this.number = number;
    }

    @Override
    public void interpret(Context context) {
        context.setOutput(number);
    }
}

// Non-terminal Expression
class AddExpression implements Expression {
    private Expression left;
    private Expression right;

    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public void interpret(Context context) {
        left.interpret(context);
        int leftValue = context.getOutput();
        right.interpret(context);
        int rightValue = context.getOutput();
        context.setOutput(leftValue + rightValue);
    }
}

public class Main {
    public static void main(String[] args) {
        // Example: 1 + 2
        Context context = new Context("1 + 2");
        Expression expression = parseExpression(context.getInput());
        expression.interpret(context);
        System.out.println("Result: " + context.getOutput());
    }

    private static Expression parseExpression(String input) {
        String[] tokens = input.split(" ");
        Expression left = new NumberExpression(Integer.parseInt(tokens[0]));
        Expression right = new NumberExpression(Integer.parseInt(tokens[2]));
        return new AddExpression(left, right);
    }
}

Iterator Pattern

The Iterator Pattern provides a way to access elements of an aggregate object sequentially without exposing the underlying representation.

Example

import java.util.ArrayList;
import java.util.List;

// Iterator interface
interface Iterator<T> {
    boolean hasNext();
    T next();
}

// Aggregate interface
interface Container<T> {
    Iterator<T> createIterator();
}

// Concrete Iterator
class NameIterator implements Iterator<String> {
    private List<String> names;
    private int index;

    public NameIterator(List<String> names) {
        this.names = names;
        this.index = 0;
    }

    @Override
    public boolean hasNext() {
        return index < names.size();
    }

    @Override
    public String next() {
        if (hasNext()) {
            return names.get(index++);
        }
        return null;
    }
}

// Concrete Aggregate
class NameRepository implements Container<String> {
    private List<String> names;

    public NameRepository() {
        this.names = new ArrayList<>();
    }

    public void addName(String name) {
        names.add(name);
    }

    @Override
    public Iterator<String> createIterator() {
        return new NameIterator(names);
    }
}

public class Main {
    public static void main(String[] args) {
        NameRepository nameRepository = new NameRepository();
        nameRepository.addName("Alice");
        nameRepository.addName("Bob");
        nameRepository.addName("Charlie");

        Iterator<String> iterator = nameRepository.createIterator();
        while (iterator.hasNext()) {
            System.out.println("Name: " + iterator.next());
        }
    }
}

Mediator Pattern

The Mediator Pattern defines an object that centralizes communication between objects in a system. It promotes loose coupling by ensuring that objects don't communicate directly with each other.

Example

import java.util.ArrayList;
import java.util.List;

// Mediator interface
interface Mediator {
    void sendMessage(String message, Colleague colleague);
}

// Colleague interface
abstract class Colleague {
    protected Mediator mediator;

    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }

    public abstract void receive(String message);

    public abstract void send(String message);
}

// Concrete Mediator
class ChatRoom implements Mediator {
    private List<Colleague> colleagues = new ArrayList<>();

    @Override
    public void sendMessage(String message, Colleague colleague) {
        for (Colleague c : colleagues) {
            if (c != colleague) {
                c.receive(message);
            }
        }
    }

    public void addColleague(Colleague colleague) {
        colleagues.add(colleague);
    }
}

// Concrete Colleague
class User extends Colleague {
    private String name;

    public User(String name, Mediator mediator) {
        super(mediator);
        this.name = name;
    }

    @Override
    public void receive(String message) {
        System.out.println(name + " received: " + message);
    }

    @Override
    public void send(String message) {
        System.out.println(name + " sent: " + message);
        mediator.sendMessage(message, this);
    }
}

public class Main {
    public static void main(String[] args) {
        ChatRoom chatRoom = new ChatRoom();

        User user1 = new User("Alice", chatRoom);
        User user2 = new User("Bob", chatRoom);
        User user3 = new User("Charlie", chatRoom);

        chatRoom.addColleague(user1);
        chatRoom.addColleague(user2);
        chatRoom.addColleague(user3);

        user1.send("Hello, everyone!");
    }
}

Memento Pattern

The Memento Pattern provides a way to capture and externalize an object's internal state so that it can be restored to that state later.

Example

import java.util.ArrayList;
import java.util.List;

// Memento class
class EditorMemento {
    private final String content;

    public EditorMemento(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

// Originator class
class TextEditor {
    private String content;

    public void write(String content) {
        this.content = content;
    }

    public EditorMemento save() {
        return new EditorMemento(content);
    }

    public void restore(EditorMemento memento) {
        content = memento.getContent();
    }

    public String getContent() {
        return content;
    }
}

// Caretaker class
class History {
    private List<EditorMemento> mementos = new ArrayList<>();

    public void push(EditorMemento memento) {
        mementos.add(memento);
    }

    public EditorMemento pop() {
        if (!mementos.isEmpty()) {
            int lastIndex = mementos.size() - 1;
            EditorMemento lastMemento = mementos.get(lastIndex);
            mementos.remove(lastIndex);
            return lastMemento;
        }
        return null;
    }
}

public class Main {
    public static void main(String[] args) {
        TextEditor textEditor = new TextEditor();
        History history = new History();

        textEditor.write("Hello, World!");
        history.push(textEditor.save());

        textEditor.write("This is a new text.");
        history.push(textEditor.save());

        textEditor.write("Memento Pattern example.");
        System.out.println("Current Content: " + textEditor.getContent());

        // Restore to previous state
        textEditor.restore(history.pop());
        System.out.println("Restored Content: " + textEditor.getContent());

        // Restore to previous state
        textEditor.restore(history.pop());
        System.out.println("Restored Content: " + textEditor.getContent());
    }
}

Null Object Pattern

The Null Object Pattern provides an object as a surrogate for the lack of an object of a given type. It helps eliminate null references in your code.

Example

abstract class Animal {
    protected String name;

    public abstract String getName();
}

// Concrete class representing a real animal
class RealAnimal extends Animal {
    public RealAnimal(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }
}

// Null object representing a "null" animal
class NullAnimal extends Animal {
    @Override
    public String getName() {
        return "No animal found";
    }
}

public class Main {
    public static void main(String[] args) {
        Animal realDog = new RealAnimal("Dog");
        Animal nullAnimal = new NullAnimal();

        System.out.println("Real Animal: " + realDog.getName());
        System.out.println("Null Animal: " + nullAnimal.getName());
    }
}

Observer Pattern

The Observer Pattern defines a one-to-many relationship between objects so that when one object changes state, all its dependents (observers) are notified and updated automatically. This pattern is widely used in event handling systems.

Example

import java.util.ArrayList;
import java.util.List;

// Subject (Observable)
class WeatherStation {
    private List<Observer> observers = new ArrayList<>();
    private int temperature;

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void setTemperature(int temperature) {
        this.temperature = temperature;
        notifyObservers();
    }

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

// Observer
interface Observer {
    void update(int temperature);
}

// Concrete Observer
class WeatherDisplay implements Observer {
    @Override
    public void update(int temperature) {
        System.out.println("Temperature is now " + temperature + " degrees Celsius.");
    }
}

public class Main {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();
        WeatherDisplay display1 = new WeatherDisplay();
        WeatherDisplay display2 = new WeatherDisplay();

        weatherStation.addObserver(display1);
        weatherStation.addObserver(display2);

        weatherStation.setTemperature(25);
    }
}

State Pattern

The State Pattern allows an object to alter its behavior when its internal state changes. It involves defining a set of state objects and switching between them as needed.

Example

// State interface
interface State {
    void handleRequest(Context context);
}

// Concrete States
class StateIdle implements State {
    @Override
    public void handleRequest(Context context) {
        System.out.println("Context is in the idle state.");
        context.setState(new StateActive());
    }
}

class StateActive implements State {
    @Override
    public void handleRequest(Context context) {
        System.out.println("Context is in the active state.");
        context.setState(new StateIdle());
    }
}

// Context
class Context {
    private State currentState;

    public Context() {
        currentState = new StateIdle();
    }

    public void setState(State state) {
        currentState = state;
    }

    public void request() {
        currentState.handleRequest(this);
    }
}

public class Main {
    public static void main(String[] args) {
        Context context = new Context();

        context.request(); // Transition to active state
        context.request(); // Transition to idle state
    }
}

Strategy Pattern

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the client to choose the appropriate algorithm at runtime without altering the code that uses it.

Example

// Strategy interface
interface PaymentStrategy {
    void pay(int amount);
}

// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println("Paid $" + amount + " with credit card " + cardNumber);
    }
}

class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(int amount) {
        System.out.println("Paid $" + amount + " with PayPal account " + email);
    }
}

// Context
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(int totalAmount) {
        paymentStrategy.pay(totalAmount);
    }
}

public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9876-5432"));
        cart.checkout(100);

        cart.setPaymentStrategy(new PayPalPayment("example@example.com"));
        cart.checkout(50);
    }
}

Template Method Pattern

The Template Method Pattern defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.

Example

// Abstract class defining the template method
abstract class PaymentProcessor {
    public void processPayment() {
        authenticate();
        validatePaymentInfo();
        performPayment();
        sendConfirmationEmail();
    }

    abstract void authenticate();

    abstract void validatePaymentInfo();

    abstract void performPayment();

    void sendConfirmationEmail() {
        System.out.println("Sending payment confirmation email.");
    }
}

// Concrete subclass
class CreditCardPaymentProcessor extends PaymentProcessor {
    @Override
    void authenticate() {
        System.out.println("Authenticating credit card payment...");
    }

    @Override
    void validatePaymentInfo() {
        System.out.println("Validating credit card payment information...");
    }

    @Override
    void performPayment() {
        System.out.println("Processing credit card payment...");
    }
}

public class Main {
    public static void main(String[] args) {
        PaymentProcessor paymentProcessor = new CreditCardPaymentProcessor();
        paymentProcessor.processPayment();
    }
}

Visitor Pattern

The Visitor Pattern represents an operation to be performed on elements of an object structure. It lets you define a new operation without changing the classes of the elements on which it operates.

Example

// Visitor interface
interface Visitor {
    void visit(Book book);
    void visit(Video video);
}

// Visitable interface
interface Visitable {
    void accept(Visitor visitor);
}

// Concrete Visitable classes
class Book implements Visitable {
    private String title;

    public Book(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

class Video implements Visitable {
    private String title;

    public Video(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// Concrete Visitor
class DiscountVisitor implements Visitor {
    @Override
    public void visit(Book book) {
        System.out.println("Applying a 10% discount to the book: " + book.getTitle());
    }

    @Override
    public void visit(Video video) {
        System.out.println("Applying a 15% discount to the video: " + video.getTitle());
    }
}

public class Main {
    public static void main(String[] args) {
        Visitable[] items = {new Book("Design Patterns"), new Video("Java Basics")};
        Visitor discountVisitor = new DiscountVisitor();

        for (Visitable item : items) {
            item.accept(discountVisitor);
        }
    }
}

Structural Design Patterns

Structural design patterns deal with the composition of classes and objects to create larger, more complex structures while keeping the system flexible and easy to maintain. They are concerned with object relationships and the assembly of objects to form more significant structures. These patterns help to solve common design problems by promoting object reusability and flexibility.

Adapter Pattern

The Adapter Pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, making them work seamlessly.

Example

interface NewInterface {
    void doSomethingNew();
}

class LegacyLibrary {
    void doSomethingOld() {
        // Legacy implementation
        System.out.println("Legacy implementation");
    }
}

class LegacyAdapter implements NewInterface {
    private LegacyLibrary legacyLibrary;

    public LegacyAdapter(LegacyLibrary legacyLibrary) {
        this.legacyLibrary = legacyLibrary;
    }

    @Override
    public void doSomethingNew() {
        legacyLibrary.doSomethingOld();
    }
}

public class Main {
    public static void main(String[] args) {
        NewInterface newObject = new LegacyAdapter(new LegacyLibrary());
        newObject.doSomethingNew();
    }
}

Bridge Pattern

The Bridge Pattern separates an object's abstraction from its implementation, allowing them to vary independently.

Example

interface DrawingAPI {
    void drawCircle(double x, double y, double radius);
}

class DrawingAPI1 implements DrawingAPI {
    public void drawCircle(double x, double y, double radius) {
        System.out.println("Implementation 1");
    }
}

class DrawingAPI2 implements DrawingAPI {
    public void drawCircle(double x, double y, double radius) {
        System.out.println("Implementation 2");
    }
}

abstract class Shape {
    protected DrawingAPI drawingAPI;

    protected Shape(DrawingAPI drawingAPI) {
        this.drawingAPI = drawingAPI;
    }

    public abstract void draw();
}

class Circle extends Shape {
    private double x, y, radius;

    public Circle(double x, double y, double radius, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    public void draw() {
        drawingAPI.drawCircle(x, y, radius);
    }
}

public class Main {
    public static void main(String[] args) {
        DrawingAPI api1 = new DrawingAPI1();
        Shape circle1 = new Circle(1, 2, 3, api1);
        circle1.draw();
    }
}

Composite Pattern

The Composite Pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It treats individual objects and compositions of objects uniformly.

Example

import java.util.ArrayList;
import java.util.List;

interface Graphic {
    void draw();
}

class Circle implements Graphic {
    public void draw() {
        System.out.println("Drawing circle.");
    }
}

class Square implements Graphic {
    public void draw() {
        System.out.println("Drawing square.");
    }
}

class CompositeGraphic implements Graphic {
    private List<Graphic> graphics = new ArrayList<>();

    public void add(Graphic graphic) {
        graphics.add(graphic);
    }

    public void draw() {
        System.out.println("Drawing all graphics.");
        for (Graphic graphic : graphics) {
            graphic.draw();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        CompositeGraphic composite = new CompositeGraphic();
        composite.add(new Circle());
        composite.add(new Square());
        composite.draw();
    }
}

Decorator Pattern

The Decorator Pattern allows you to add behavior to objects dynamically, without altering their class.

Example

interface Coffee {
    double cost();
    String description();
}

class SimpleCoffee implements Coffee {
    public double cost() {
        return 2.0;
    }

    public String description() {
        return "Simple Coffee";
    }
}

abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    public double cost() {
        return decoratedCoffee.cost();
    }

    public String description() {
        return decoratedCoffee.description();
    }
}

class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public double cost() {
        return super.cost() + 1.0;
    }

    public String description() {
        return super.description() + ", Milk";
    }
}

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new MilkDecorator(new SimpleCoffee());
        System.out.println("Cost: $" + coffee.cost());
        System.out.println("Description: " + coffee.description());
    }
}

Facade Pattern

The Facade Pattern provides a simplified interface to a complex system of classes, making it easier to use and understand. It acts as a "facade" or entry point to access a group of related interfaces and classes.

Example

class CPU {
    public void start() {
        System.out.println("CPU start");
    }

    public void shutdown() {
        System.out.println("CPU shutdown");
    }
}

class Memory {
    public void load() {
        System.out.println("Memory load");
    }

    public void unload() {
        System.out.println("Memory unload");
    }
}

class HardDrive {
    public void read() {
        System.out.println("Hard drive read");
    }

    public void write() {
        System.out.println("Hard drive write");
    }
}

class ComputerFacade {
    private CPU cpu;
    private Memory memory;
    private HardDrive hardDrive;

    public ComputerFacade() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hardDrive = new HardDrive();
    }

    public void start() {
        cpu.start();
        memory.load();
        hardDrive.read();
    }

    public void shutdown() {
        cpu.shutdown();
        memory.unload();
        hardDrive.write();
    }
}

public class Main {
    public static void main(String[] args) {
        ComputerFacade computer = new ComputerFacade();
        computer.start();
        computer.shutdown();
    }
}

Filter Pattern

The Filter Pattern allows you to create a chain of filter objects to process a request. Each filter performs some filtering or processing on the request and passes it to the next filter in the chain.

Example

import java.util.ArrayList;
import java.util.List;

class Product {
    private String name;
    private String category;
    private double price;

    public Product(String name, String category, double price) {
        this.name = name;
        this.category = category;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public String getCategory() {
        return category;
    }

    public double getPrice() {
        return price;
    }
}

interface Filter {
    List<Product> filter(List<Product> products);
}

class CategoryFilter implements Filter {
    private String category;

    public CategoryFilter(String category) {
        this.category = category;
    }

    @Override
    public List<Product> filter(List<Product> products) {
        List<Product> filteredProducts = new ArrayList<>();
        for (Product product : products) {
            if (product.getCategory().equalsIgnoreCase(category)) {
                filteredProducts.add(product);
            }
        }
        return filteredProducts;
    }
}

class PriceRangeFilter implements Filter {
    private double minPrice;
    private double maxPrice;

    public PriceRangeFilter(double minPrice, double maxPrice) {
        this.minPrice = minPrice;
        this.maxPrice = maxPrice;
    }

    @Override
    public List<Product> filter(List<Product> products) {
        List<Product> filteredProducts = new ArrayList<>();
        for (Product product : products) {
            double price = product.getPrice();
            if (price >= minPrice && price <= maxPrice) {
                filteredProducts.add(product);
            }
        }
        return filteredProducts;
    }
}

class ProductFilter {
    public static List<Product> applyFilter(List<Product> products, Filter filter) {
        return filter.filter(products);
    }
}

public class Main {
    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Product("Laptop", "Electronics", 1000.0));
        products.add(new Product("Phone", "Electronics", 500.0));
        products.add(new Product("Shirt", "Clothing", 25.0));
        products.add(new Product("Dress", "Clothing", 50.0));
        products.add(new Product("Book", "Books", 15.0));

        System.out.println("Electronics products:");
        List<Product> electronicsProducts = ProductFilter.applyFilter(products, new CategoryFilter("Electronics"));
        printProducts(electronicsProducts);

        System.out.println("\nProducts in the price range $20 - $100:");
        List<Product> priceRangeProducts = ProductFilter.applyFilter(products, new PriceRangeFilter(20.0, 100.0));
        printProducts(priceRangeProducts);
    }

    public static void printProducts(List<Product> products) {
        for (Product product : products) {
            System.out.println("Name: " + product.getName() + ", Category: " + product.getCategory() +
                    ", Price: $" + product.getPrice());
        }
    }
}

Flyweight Pattern

The Flyweight Pattern is used to minimize memory usage or computational expenses by sharing as much as possible with related objects. It is particularly useful when you have a large number of similar objects, and you want to reduce memory usage by sharing common data among them.

Example

import java.util.HashMap;
import java.util.Map;

class Character {
    private char symbol;

    public Character(char symbol) {
        this.symbol = symbol;
    }

    public void display(int fontSize) {
        System.out.println("Character: " + symbol + ", Font Size: " + fontSize);
    }
}

class CharacterFactory {
    private Map<java.lang.Character, Character> characters = new HashMap<>();

    public Character getCharacter(char symbol) {
        if (!characters.containsKey(symbol)) {
            characters.put(symbol, new Character(symbol));
        }
        return characters.get(symbol);
    }
}

public class Main {
    public static void main(String[] args) {
        CharacterFactory characterFactory = new CharacterFactory();
        Character charA = characterFactory.getCharacter('A');
        Character charB = characterFactory.getCharacter('B');
        charA.display(12);
        charB.display(16);
    }
}

Proxy Pattern

The Proxy Pattern provides a surrogate or placeholder for another object to control access to it. It allows you to add an additional level of control over object access, such as lazy loading, access control, or monitoring.

Example

interface Image {
    void display();
}

class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading " + filename);
    }

    public void display() {
        System.out.println("Displaying " + filename);
    }
}

class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

public class Main {
    public static void main(String[] args) {
        Image image = new ProxyImage("large_image.jpg");
        image.display();
    }
}

Creational Design Patterns

Creational design patterns are a subset of design patterns in software engineering that focus on the process of object creation. These patterns provide various ways to instantiate objects while hiding the complexities involved in the process. Creational design patterns promote flexibility, reusability, and maintainability in your codebase. In this article, we'll explore some of the most commonly used creational design patterns with java examples.

Abstract Factory Pattern

Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Example

interface IShape {
    void draw();
}

abstract class AbstractShapeFactory {
    abstract IShape getShape(String targetShape);
}

class Circle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing circle.");
    }
}

class Square implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing square.");
    }
}

class Rectangle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing rectangle.");
    }
}

class NormalShapeFactory extends AbstractShapeFactory {
    @Override
    IShape getShape(String targetShape) {
        return switch(targetShape.toLowerCase()) {
            case "circle" -> new Circle();
            case "square" -> new Square();
            case "rectangle" -> new Rectangle();
            default -> null;
        };
    }
}

class RoundedSquare implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing rounded square.");
    }
}

class RoundedRectangle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing rounded rectangle.");
    }
}

class RoundedShapeFactory extends AbstractShapeFactory {
    @Override
    IShape getShape(String targetShape) {
        return switch(targetShape.toLowerCase()) {
            case "circle" -> new Circle();
            case "square" -> new RoundedSquare();
            case "rectangle" -> new RoundedRectangle();
            default -> null;
        };
    }
}

class ShapeFactoryProducer {
    public static AbstractShapeFactory getFactory(String shapeType){
        return switch (shapeType.toLowerCase()) {
            case "rounded" -> new RoundedShapeFactory();
            default -> new NormalShapeFactory();
        };
    }
}

public class Main {
    public static void main(String[] args) {
        AbstractShapeFactory normalShapeFactory = ShapeFactoryProducer.getFactory("normal");
        IShape normalSquare = normalShapeFactory.getShape("square");
        normalSquare.draw();

        AbstractShapeFactory roundedShapeFactory = ShapeFactoryProducer.getFactory("rounded");
        IShape roundedSquare = roundedShapeFactory.getShape("square");
        roundedSquare.draw();
    }
}

Builder Pattern

Separates the construction of a complex object (i.e. requires a lot of constructors and properties) from its representation, allowing the same construction process to create different representations.

Example

class Product {
    private String part1;
    private String part2;

    public void setPart1(String part1) {
        this.part1 = part1;
    }

    public void setPart2(String part2) {
        this.part2 = part2;
    }

    public void show() {
        System.out.println("Part 1: " + part1);
        System.out.println("Part 2: " + part2);
    }
}

class ProductBuilder {
    private String part1;
    private String part2;

    public ProductBuilder buildPart1(String part1) {
        this.part1 = part1;
        return this;
    }

    public ProductBuilder buildPart2(String part2) {
        this.part2 = part2;
        return this;
    }

    public Product build() {
        Product product = new Product();
        product.setPart1(part1);
        product.setPart2(part2);
        return product;
    }
}

public class Main {
    public static void main(String[] args) {
        ProductBuilder builder = new ProductBuilder();
        Product product = builder
            .buildPart1("Part 1")
            .buildPart2("Part 2")
            .build();

        product.show();
    }
}

Factory Pattern

Defines an interface for creating an object but allows subclasses to alter the type of objects that will be created.

Example

interface IShape {
    void draw();
}

interface IShapeFactory {
    IShape createShape(String shapeType);
}

class Circle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing circle.");
    }
}

class Rectangle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing rectangle.");
    }
}

class ShapeFactory implements IShapeFactory {
    @Override
    public IShape createShape(String shapeType) {
        if (shapeType.equals("circle")) {
            return new Circle();
        }
        else if (shapeType.equals("rectangle")) {
            return new Rectangle();
        }
        else {
            throw new IllegalArgumentException("Invalid shape type: " + shapeType);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ShapeFactory shapeFactory = new ShapeFactory();
        IShape shape = shapeFactory.createShape("circle");
        System.out.println(shape);
    }
}

Lazy Initialization Pattern

Delays the creation of an object or the calculation of a value until it is actually needed.

Example

class LightObject1 {
    public LightObject1() {
        System.out.println("LightObject1 initialized");
    }
}

class LightObject2 {
    public LightObject2() {
        System.out.println("LightObject2 initialized");
    }
}

class ExpensiveObject {
    public ExpensiveObject() {
        // Simulate a time-consuming initialization process
        try {
            Thread.sleep(2000); // Sleep for 2 seconds to simulate initialization time
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("ExpensiveObject initialized");
    }
}

class LazyInitialization {
    // Lightweight objects to be instantiated in the constructor
    private LightObject1 lightObject1;
    private LightObject2 lightObject2;

    // Declare a private field to hold the lazily initialized object
    private ExpensiveObject expensiveObject;

    public LazyInitialization() {
        // Instantiate the lightweight objects in the constructor
        lightObject1 = new LightObject1();
        lightObject2 = new LightObject2();
    }

    // This method initializes and returns the expensive object on-demand
    public ExpensiveObject getExpensiveObject() {
        if (expensiveObject == null) {
            // Lazy initialization: Create the expensive object when it's first requested
            expensiveObject = new ExpensiveObject();
        }
        return expensiveObject;
    }
}

public class Main {
    public static void main(String[] args) {
        LazyInitialization example = new LazyInitialization();

        // At this point, the lightweight objects are already instantiated,
        // but the expensive object has not been created yet
        System.out.println("Lightweight objects initialized");
        System.out.println("Expensive object not yet initialized");

        // When needed, get the expensive object
        ExpensiveObject obj = example.getExpensiveObject();

        // Now, the expensive object has been initialized
        System.out.println("Expensive object initialized");
    }
}

Prototype Pattern

Creates new objects by copying an existing object, known as the prototype, rather than instantiating new objects using constructors.

Example

class Prototype implements Cloneable {
    private String name;

    public Prototype(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public Prototype clone() throws CloneNotSupportedException {
        return (Prototype) super.clone();
    }
}

public class Main {

    public static void main(String ... args) {
        Prototype prototype1 = new Prototype("Prototype 1");

        try {
            Prototype prototype2 = prototype1.clone();
            System.out.println("Clone: " + prototype2.getName()); // Output: Clone: Prototype 1
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

Singleton Pattern

Ensures that a class has only one instance and provides a global point of access to that instance.

Example

final class Singleton {
    private static Singleton instance;

    private Singleton() {
        // Private constructor to prevent instantiation
    }

    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();
        System.out.println(singleton1);
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton2);
    }
}