Java Topic
Java Design Patterns
Design patterns are best practices that provide solutions to common software design problems. They offer a way to structure code for flexibility and reuse. Here, we cover some of the most commonly used design patterns in Java, including Singleton, Factory, and Facade patterns.
1. Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system.
Implementation:
public class Singleton {
private static Singleton instance;
private Singleton() {
// Private constructor to prevent instantiation
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
In this implementation, the getInstance() method ensures that only one instance of the class is created and provides access to it.
2. Factory Pattern
The Factory pattern provides a way to create objects without specifying the exact class of object that will be created. This pattern is used to create instances of classes that implement a common interface or extend a common class.
Implementation:
public interface Product {
void doSomething();
}
public class ProductA implements Product {
public void doSomething() {
System.out.println("ProductA doing something");
}
}
public class ProductB implements Product {
public void doSomething() {
System.out.println("ProductB doing something");
}
}
public class ProductFactory {
public static Product getProduct(String type) {
if (type.equals("A")) {
return new ProductA();
} else if (type.equals("B")) {
return new ProductB();
}
throw new IllegalArgumentException("Unknown product type");
}
}
In this example, the ProductFactory class creates instances of Product based on the provided type. This allows for easy addition of new product types without modifying existing code.
3. Facade Pattern
The Facade pattern provides a simplified interface to a complex subsystem. It hides the complexities of the subsystem and provides a higher-level interface that makes the subsystem easier to use.
Implementation:
public class SubsystemA {
public void operationA() {
System.out.println("SubsystemA operation");
}
}
public class SubsystemB {
public void operationB() {
System.out.println("SubsystemB operation");
}
}
public class Facade {
private SubsystemA subsystemA;
private SubsystemB subsystemB;
public Facade() {
subsystemA = new SubsystemA();
subsystemB = new SubsystemB();
}
public void simplifiedOperation() {
subsystemA.operationA();
subsystemB.operationB();
}
}
In this example, the Facade class provides a simplified interface to the complex operations of SubsystemA and SubsystemB. Clients interact with the facade rather than the subsystems directly.
4. Other Common Design Patterns
- Observer Pattern: Defines a dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
- Decorator Pattern: Allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class.
- Strategy Pattern: 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.
- Adapter Pattern: Converts the interface of a class into another interface clients expect. The adapter pattern allows classes to work together that could not otherwise because of incompatible interfaces.
Java SOLID Principles
The SOLID principles are a set of five design principles intended to make object-oriented designs more understandable, flexible, and maintainable. The principles are aimed at improving software design by promoting better practices and reducing the likelihood of introducing bugs. SOLID is an acronym for:
- S: Single Responsibility Principle (SRP)
- O: Open/Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have only one job or responsibility. By adhering to SRP, you ensure that each class handles a specific piece of functionality, making the system easier to understand and modify.
Example:
public class Report {
private String title;
public Report(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
// This method violates SRP because it handles both report generation and printing.
public void printReport() {
// Code to generate and print the report
}
}
public class ReportPrinter {
public void print(Report report) {
System.out.println(report.getTitle());
// Code to print the report
}
}
In this example, the Report class has two responsibilities: handling report data and printing the report. The ReportPrinter class handles the printing responsibility, adhering to SRP.
2. Open/Closed Principle (OCP)
The Open/Closed Principle states that a class should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code, which helps in minimizing the risk of introducing new bugs.
Example:
public interface Shape {
double calculateArea();
}
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double calculateArea() {
return width * height;
}
}
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea();
}
}
In this example, Shape is an interface, and new shapes like Rectangle can be added without modifying the AreaCalculator class. The system is open for extension but closed for modification.
3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. Subtypes must be substitutable for their base types.
Example:
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
public class Sparrow extends Bird {
// Inherits fly() method
}
public class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostriches can't fly");
}
}
public class BirdWatcher {
public void watchBird(Bird bird) {
bird.fly(); // This will fail for Ostrich
}
}
In this example, the Ostrich class violates LSP because it cannot fly, even though it inherits from Bird. The BirdWatcher class expects all birds to be able to fly.
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle states that a client should not be forced to implement interfaces it does not use. Instead of having one large interface, multiple smaller and specific interfaces should be created.
Example:
public interface Worker {
void work();
void eat();
}
public class HumanWorker implements Worker {
public void work() {
System.out.println("Human working");
}
public void eat() {
System.out.println("Human eating");
}
}
public class RobotWorker implements Worker {
public void work() {
System.out.println("Robot working");
}
public void eat() {
throw new UnsupportedOperationException("Robot does not eat");
}
}
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class HumanWorker implements Workable, Eatable {
public void work() {
System.out.println("Human working");
}
public void eat() {
System.out.println("Human eating");
}
}
public class RobotWorker implements Workable {
public void work() {
System.out.println("Robot working");
}
}
In this example, splitting the large Worker interface into smaller Workable and Eatable interfaces adheres to ISP, allowing classes to implement only the methods they need.
5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Example:
public interface Database {
void connect();
}
public class MySQLDatabase implements Database {
public void connect() {
System.out.println("Connecting to MySQL");
}
}
public class Application {
private Database database;
public Application(Database database) {
this.database = database;
}
public void start() {
database.connect();
}
}
public class Main {
public static void main(String[] args) {
Database db = new MySQLDatabase();
Application app = new Application(db);
app.start();
}
}
In this example, the Application class depends on the Database interface rather than a specific implementation. This allows you to swap out different database implementations without changing the Application class.
Comments (0)
No comments yet. Be the first to comment!