Header Ads Widget

Responsive Advertisement

Design pattern

 

 

Java design patterns are best practices used to solve common software design problems. They provide a way to organize code to make it more efficient, maintainable, and scalable. Here’s an overview of some common design patterns in Java:

1. Creational Patterns

Ø  Singleton: Ensures a class has only one instance and provides a global point of access to it.

Ø  Factory Method: Creates objects without specifying the exact class of object that will be created. Defines an interface for creating an object, but lets subclasses alter the type of objects that will be created.

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

Ø  Builder: Separates the construction of a complex object from its representation so that the same construction process can create different representations.

Ø  Prototype: Creates new objects by copying an existing object, known as a prototype.

2. Structural Patterns

Ø  Adapter: Allows incompatible interfaces to work together by wrapping an existing class with a new interface.

Ø  Decorator: Adds new functionality to an object dynamically without altering its structure.

Ø  Facade: Provides a simplified interface to a complex subsystem.

Ø  Proxy: Provides a surrogate or placeholder for another object to control access to it.

Ø  Composite: Composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions uniformly.

Ø  Bridge Decouples an abstraction from its implementation so that the two can vary independently.

Ø  Flyweight Reduces the cost of creating and manipulating a large number of similar objects by sharing objects where possible.

3. Behavioral Patterns

Ø  Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. The strategy pattern lets the algorithm vary independently from clients that use it.

Ø  Observer: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Ø  Command: Encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations.

Ø  State: Allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

Ø  Template Method: Defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.

Ø  Chain of Responsibility: Passes a request along a chain of handlers, allowing each handler to process the request or pass it to the next handler.

Ø  Interpreter: Defines a grammatical representation for a language and provides an interpreter to interpret sentences in the language. It is useful when you want to evaluate expressions in a language or when the grammar is simple and well-understood

Ø  Iterator: Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Ø  Mediator: Defines an object that encapsulates how a set of objects interact, promoting loose coupling by keeping objects from referring to each other explicitly.

Ø  Memento: Captures and externalizes an object's internal state so that it can be restored later, without violating encapsulation.

Ø  Visitor: Represents an operation to be performed on the elements of an object structure, allowing you to define a new operation without changing the classes of the elements on which it operates.

4. Concurrency Patterns

Ø  Thread Pool: Manages a pool of worker threads to efficiently execute tasks in parallel.

Ø  Future: Represents the result of an asynchronous computation, providing methods to check if the computation is complete, to wait for its completion, and to retrieve the result.

Ø  Producer-Consumer: Separates the creation of tasks (producer) from the execution of tasks (consumer) using a queue.

Ø  Read-Write Lock: Allows multiple threads to read a shared resource concurrently but requires exclusive access for writes, preventing conflicts.

5. Architectural Patterns

These patterns provide general solutions for software architecture problems.

Ø  Model-View-Controller (MVC)

  • Separates an application into three interconnected components: the Model (data), the View (UI), and the Controller (logic), allowing separation of concerns and independent development of components.

Ø  Model-View-ViewModel (MVVM)

·       Similar to MVC, but adds a ViewModel to handle the presentation logic, binding the View and the Model.

Ø  Data Access Object (DAO)

  • Provides an abstract interface to a database or other persistence mechanism, separating the persistence logic from the business logic.

6. Other Patterns

Ø  Null Object

·       Provides a default behavior when an object is absent or null, avoiding null references.

Ø  Service Locator

·       Provides a centralized registry for services, making it easy to find and use service instances.

 

 

 

Creating a hierarchical overview of Java design patterns

Creating a hierarchical overview of Java design patterns using a flowchart along with corresponding Java examples involves organizing the patterns into categories and illustrating their relationships in a flowchart format. Below is a conceptual flowchart representation along with simplified Java code examples for each pattern category.

Flowchart: Hierarchical Overview of Java Design Patterns

 

Hierarchical Overview of Java Design Patterns
Hierarchical Overview of Java Design Patterns


 

Java Code Examples

1. Creational Patterns

a. Singleton Pattern

java

public 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;

    }

}

 

b. Factory Method Pattern

java

interface Product {

    void use();

}

 

class ConcreteProductA implements Product {

    public void use() {

        System.out.println("Using Product A");

    }

}

 

class ConcreteProductB implements Product {

    public void use() {

        System.out.println("Using Product B");

    }

}

 

abstract class Creator {

    public abstract Product factoryMethod();

 

    public void someOperation() {

        Product product = factoryMethod();

        product.use();

    }

}

 

class ConcreteCreatorA extends Creator {

    public Product factoryMethod() {

        return new ConcreteProductA();

    }

}

 

class ConcreteCreatorB extends Creator {

    public Product factoryMethod() {

        return new ConcreteProductB();

    }

}

 

c. Abstract Factory Pattern

java

interface AbstractFactory {

    ProductA createProductA();

    ProductB createProductB();

}

 

class ConcreteFactory1 implements AbstractFactory {

    public ProductA createProductA() {

        return new ProductA1();

    }

 

    public ProductB createProductB() {

        return new ProductB1();

    }

}

 

class ConcreteFactory2 implements AbstractFactory {

    public ProductA createProductA() {

        return new ProductA2();

    }

 

    public ProductB createProductB() {

        return new ProductB2();

    }

}

 

interface ProductA {

    void use();

}

 

class ProductA1 implements ProductA {

    public void use() {

        System.out.println("Using Product A1");

    }

}

 

class ProductA2 implements ProductA {

    public void use() {

        System.out.println("Using Product A2");

    }

}

 

interface ProductB {

    void use();

}

 

class ProductB1 implements ProductB {

    public void use() {

        System.out.println("Using Product B1");

    }

}

 

class ProductB2 implements ProductB {

    public void use() {

        System.out.println("Using Product B2");

    }

}

 

2. Structural Patterns

a. Adapter Pattern

java

interface Target {

    void request();

}

 

class Adaptee {

    void specificRequest() {

        System.out.println("Specific request");

    }

}

 

class Adapter implements Target {

    private Adaptee adaptee;

 

    public Adapter(Adaptee adaptee) {

        this.adaptee = adaptee;

    }

 

    public void request() {

        adaptee.specificRequest();

    }

}

 

b. Decorator Pattern

java

interface Component {

    void operation();

}

 

class ConcreteComponent implements Component {

    public void operation() {

        System.out.println("Operation in ConcreteComponent");

    }

}

 

abstract class Decorator implements Component {

    protected Component component;

 

    public Decorator(Component component) {

        this.component = component;

    }

 

    public void operation() {

        component.operation();

    }

}

 

class ConcreteDecorator extends Decorator {

    public ConcreteDecorator(Component component) {

        super(component);

    }

 

    public void operation() {

        super.operation();

        System.out.println("Added behavior in ConcreteDecorator");

    }

}

 

c. Composite Pattern

java

import java.util.ArrayList;

import java.util.List;

 

interface Component {

    void operation();

}

 

class Leaf implements Component {

    public void operation() {

        System.out.println("Operation in Leaf");

    }

}

 

class Composite implements Component {

    private List<Component> children = new ArrayList<>();

 

    public void add(Component component) {

        children.add(component);

    }

 

    public void remove(Component component) {

        children.remove(component);

    }

 

    public void operation() {

        for (Component child : children) {

            child.operation();

        }

    }

}

 

3. Behavioral Patterns

a. Strategy Pattern

java

interface Strategy {

    void execute();

}

 

class ConcreteStrategyA implements Strategy {

    public void execute() {

        System.out.println("Executing Strategy A");

    }

}

 

class ConcreteStrategyB implements Strategy {

    public void execute() {

        System.out.println("Executing Strategy B");

    }

}

 

class Context {

    private Strategy strategy;

 

    public void setStrategy(Strategy strategy) {

        this.strategy = strategy;

    }

 

    public void executeStrategy() {

        strategy.execute();

    }

}

 

b. Observer Pattern

java

import java.util.ArrayList;

import java.util.List;

 

interface Observer {

    void update();

}

 

class ConcreteObserver implements Observer {

    public void update() {

        System.out.println("Observer updated");

    }

}

 

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

        for (Observer observer : observers) {

            observer.update();

        }

    }

}

 

c. Command Pattern

java

interface Command {

    void execute();

}

 

class ConcreteCommand implements Command {

    private Receiver receiver;

 

    public ConcreteCommand(Receiver receiver) {

        this.receiver = receiver;

    }

 

    public void execute() {

        receiver.action();

    }

}

 

class Receiver {

    public void action() {

        System.out.println("Action executed");

    }

}

 

class Invoker {

    private Command command;

 

    public void setCommand(Command command) {

        this.command = command;

    }

 

    public void executeCommand() {

        command.execute();

    }

}

 

d. Chain of Responsibility

java

 

A>

abstract class Logger {

    public static int DEBUG = 1;

    public static int INFO = 2;

    public static int ERROR = 3;

 

    protected int level;

 

    // Next element in the chain of responsibility

    protected Logger nextLogger;

 

    public void setNextLogger(Logger nextLogger) {

        this.nextLogger = nextLogger;

    }

 

    public void logMessage(int level, String message) {

        if (this.level <= level) {

            write(message);

        }

        if (nextLogger != null) {

            nextLogger.logMessage(level, message);

        }

    }

 

    protected abstract void write(String message);

}

 

 

B>

class DebugLogger extends Logger {

 

    public DebugLogger(int level) {

        this.level = level;

    }

 

    @Override

    protected void write(String message) {

        System.out.println("DEBUG Logger: " + message);

    }

}

 

class InfoLogger extends Logger {

 

    public InfoLogger(int level) {

        this.level = level;

    }

 

    @Override

    protected void write(String message) {

        System.out.println("INFO Logger: " + message);

    }

}

 

class ErrorLogger extends Logger {

 

    public ErrorLogger(int level) {

        this.level = level;

    }

 

    @Override

    protected void write(String message) {

        System.out.println("ERROR Logger: " + message);

    }

}

 

C>

class ChainPatternDemo {

 

    private static Logger getChainOfLoggers() {

 

        Logger errorLogger = new ErrorLogger(Logger.ERROR);

        Logger infoLogger = new InfoLogger(Logger.INFO);

        Logger debugLogger = new DebugLogger(Logger.DEBUG);

 

        // Setting up the chain: DebugLogger -> InfoLogger -> ErrorLogger

        debugLogger.setNextLogger(infoLogger);

        infoLogger.setNextLogger(errorLogger);

 

        return debugLogger;

    }

 

    public static void main(String[] args) {

        Logger loggerChain = getChainOfLoggers();

 

        loggerChain.logMessage(Logger.DEBUG, "This is a debug level information.");

        loggerChain.logMessage(Logger.INFO, "This is an info level information.");

        loggerChain.logMessage(Logger.ERROR, "This is an error level information.");

    }

}

 

 

Explanation

  • Logger Chain: We set up a chain of loggers (DebugLogger -> InfoLogger -> ErrorLogger) where each logger checks the message level and decides whether to handle it.
  • logMessage Method: The logMessage method checks if the current logger can handle the message. If yes, it logs the message. If not, it passes the message to the next logger in the chain.
  • Flexibility: The Chain of Responsibility pattern makes it easy to add new types of loggers or change the chain order without modifying existing classes.

 

 

 

e. interpreter

A>

interface Expression {

    int interpret();

}

 

B>

class NumberExpression implements Expression {

    private int number;

 

    public NumberExpression(int number) {

        this.number = number;

    }

 

    public NumberExpression(String number) {

        this.number = Integer.parseInt(number);

    }

 

    @Override

    public int interpret() {

        return this.number;

    }

}

 

C>

class AddExpression implements Expression {

    private Expression leftExpression;

    private Expression rightExpression;

 

    public AddExpression(Expression leftExpression, Expression rightExpression) {

        this.leftExpression = leftExpression;

        this.rightExpression = rightExpression;

    }

 

    @Override

    public int interpret() {

        return leftExpression.interpret() + rightExpression.interpret();

    }

}

 

D>

class SubtractExpression implements Expression {

    private Expression leftExpression;

    private Expression rightExpression;

 

    public SubtractExpression(Expression leftExpression, Expression rightExpression) {

        this.leftExpression = leftExpression;

        this.rightExpression = rightExpression;

    }

 

    @Override

    public int interpret() {

        return leftExpression.interpret() - rightExpression.interpret();

    }

}

 

E>

import java.util.Stack;

 

class InterpreterClient {

    public static Expression parse(String expression) {

        Stack<Expression> stack = new Stack<>();

 

        String[] tokens = expression.split(" ");

        for (String token : tokens) {

            if (isOperator(token)) {

                Expression rightExpression = stack.pop();

                Expression leftExpression = stack.pop();

                Expression operator = getOperator(token, leftExpression, rightExpression);

                int result = operator.interpret();

                stack.push(new NumberExpression(result));

            } else {

                stack.push(new NumberExpression(token));

            }

        }

 

        return stack.pop();

    }

 

    private static boolean isOperator(String token) {

        return token.equals("+") || token.equals("-");

    }

 

    private static Expression getOperator(String token, Expression left, Expression right) {

        switch (token) {

            case "+":

                return new AddExpression(left, right);

            case "-":

                return new SubtractExpression(left, right);

            default:

                throw new UnsupportedOperationException("Unknown operator: " + token);

        }

    }

}

 

F>

public class InterpreterPatternDemo {

    public static void main(String[] args) {

        String expression = "7 3 - 2 1 + +"; // (7 - 3) + (2 + 1)

        Expression result = InterpreterClient.parse(expression);

        System.out.println(expression + " = " + result.interpret());

    }

}

 

G: output

 

7 3 - 2 1 + + = 7

 

Explanation

  • Expression Interface: Defines a method interpret() that is implemented by all concrete expressions.
  • Concrete Expressions:
    • NumberExpression handles numbers.
    • AddExpression and SubtractExpression handle addition and subtraction, respectively.
  • Interpreter Client: The InterpreterClient class parses a postfix expression (where operators follow their operands) and uses a stack to evaluate the expression using the interpreter classes.
  • Postfix Expression: The expression "7 3 - 2 1 + +" represents the mathematical expression (7−3)+(2+1)(7 - 3) + (2 + 1)(7−3)+(2+1).

Use Case

  • The Interpreter pattern is suitable for scenarios where the grammar is simple and stable, and the language to be interpreted is small.
  • Examples include interpreting mathematical expressions, parsing simple configuration files, or evaluating conditions in rule engines.

 

 

 

 

4. Concurrency Patterns

a. Thread Pool Pattern

java

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

 

class Task implements Runnable {

    public void run() {

        System.out.println("Task executed by " + Thread.currentThread().getName());

    }

}

 

public class ThreadPoolExample {

    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(5);

 

        for (int i = 0; i < 10; i++) {

            executor.execute(new Task());

        }

 

        executor.shutdown();

    }

}

 

b. Producer-Consumer Pattern

java

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

 

class Task implements Runnable {

    public void run() {

        System.out.println("Task executed by " + Thread.currentThread().getName());

    }

}

 

public class ThreadPoolExample {

    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(5);

 

        for (int i = 0; i < 10; i++) {

            executor.execute(new Task());

        }

 

        executor.shutdown();

    }

}

 

c. Future Pattern

java

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.Future;

 

class Task implements Callable<String> {

    public String call() throws Exception {

        Thread.sleep(2000);

        return "Task completed";

    }

}

 

public class FutureExample {

    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(2);

 

        Future<String> future = executor.submit(new Task());

 

        try {

            System.out.println("Result: " + future.get());

        } catch (InterruptedException | ExecutionException e) {

            e.printStackTrace();

        }

 

        executor.shutdown();

    }

}

 

 

 

Difference between Java Design Patterns and SOLID principles

 

Java Design Patterns and SOLID principles are both essential concepts in software design, but they serve different purposes and operate at different levels of abstraction. Here’s a breakdown of their differences:

1. Purpose

  • Java Design Patterns:
    • Design patterns are typical solutions to common problems in software design. They are proven templates that can be adapted to solve specific design issues in your code.
    • Examples include Singleton, Factory Method, Observer, and Decorator.
    • Design patterns provide solutions to recurring problems and guide the structure and interaction of classes and objects.
  • SOLID Principles:
    • SOLID principles are a set of guidelines for writing clean, maintainable, and scalable code. They help in ensuring that the code is flexible, easy to understand, and less prone to bugs.
    • SOLID stands for:
      1. Single Responsibility Principle (SRP)
      2. Open/Closed Principle (OCP)
      3. Liskov Substitution Principle (LSP)
      4. Interface Segregation Principle (ISP)
      5. Dependency Inversion Principle (DIP)
    • These principles focus on designing individual classes and systems in a way that they are more robust and easier to manage.

2. Level of Abstraction

  • Java Design Patterns:
    • Operate at a higher level of abstraction.
    • They are more about the architecture or structure of an entire system or a subsystem.
    • Design patterns describe how classes and objects interact to solve a specific problem in a certain context.
  • SOLID Principles:
    • Operate at a lower level of abstraction.
    • They guide the design of individual classes and their interactions.
    • SOLID principles are about writing good class-level code that is easy to maintain and extend.

3. Scope

  • Java Design Patterns:
    • Applied to solve specific problems within a given scope, such as creating objects (Factory Method), managing object behavior (Observer), or structuring classes (Decorator).
    • Design patterns can be applied to various parts of the application, depending on where the design issue occurs.
  • SOLID Principles:
    • Applied universally across all parts of the codebase to ensure the code is flexible, modular, and easily understandable.
    • SOLID principles are the foundation for good object-oriented design practices and are not tied to specific problems but are general principles to follow.

4. Reusability

  • Java Design Patterns:
    • Provide reusable solutions to recurring design problems.
    • Patterns can be reused across different projects or systems whenever similar design problems arise.
  • SOLID Principles:
    • Ensure that individual components of the system are reusable.
    • By following SOLID principles, the classes you write are more likely to be reusable in different contexts because they adhere to good design practices.

5. Example Comparison

  • Single Responsibility Principle (SRP) from SOLID:
    • A class should have only one reason to change, meaning it should have only one job or responsibility.
    • For instance, if you have a class responsible for both data validation and saving data to the database, SRP would suggest splitting these responsibilities into two separate classes.
  • Factory Method Pattern:
    • Provides an interface for creating objects in a super class, but allows subclasses to alter the type of objects that will be created.
    • The Factory Method pattern can be implemented in a way that adheres to the Open/Closed Principle (OCP), another SOLID principle, by allowing the creation of objects to be extended without modifying existing code.

6. Interrelation

  • Java Design Patterns and SOLID Principles:
    • Design patterns often embody SOLID principles. For example, the Strategy Pattern aligns with the Open/Closed Principle by allowing behavior to be chosen at runtime without modifying existing code.
    • Implementing design patterns can help you adhere to SOLID principles, but it is also possible to misuse them in ways that violate SOLID principles.

Summary

ü  Java Design Patterns provide solutions to specific design problems by defining the structure and interaction between classes and objects.

ü  SOLID Principles guide the design of individual classes and their interactions to ensure the codebase is robust, maintainable, and scalable.

ü  They complement each other: design patterns often implement SOLID principles, and adhering to SOLID principles makes it easier to implement design patterns effectively.

  

Post a Comment

0 Comments