1. Single Responsibility Principle (SRP)
Definition: A class should only have one reason to change. It should focus on one responsibility or functionality.
Bad Example:
class UserManager
{
void createUser() {}
void sendEmail() {}
void logActivity() {}
}
- The
UserManager
class has multiple responsibilities: user creation, sending emails, and logging. Any change in one responsibility could affect others.
Good Example:
class UserService {
void createUser() {}
}
class EmailService {
void sendEmail() {}
}
class LoggingService {
void logActivity() {}
}
- Responsibilities are separated into different classes. This makes the code easier to maintain and extend.
Benefits:
- Easier debugging: Changes are localized.
- Better readability and modularity.
2. Open/Closed Principle (OCP)
Definition: Classes should be open for extension but closed for modification. Extend functionality without altering existing code.
Implementation: Use interfaces and polymorphism.
Example:
interface Shape {
double calculateArea();
}
class Rectangle implements Shape {
public double calculateArea() {
return length * breadth; // Logic for rectangle area
}
}
class Circle implements Shape {
public double calculateArea() {
return Math.PI * radius * radius; // Logic for circle area
}
}
Adding a new shape (e.g., Triangle
) requires implementing the Shape
interface without changing existing code.
Benefits:
- Avoids introducing bugs into tested code.
- Supports adding new features without breaking the system.
3. Liskov Substitution Principle (LSP)
Definition: Subclasses should be replaceable by their base classes without altering the correctness of the program.
Key Idea: Ensure that derived classes adhere to the behavior and expectations of the base class.
Example:
class Bird {
void fly() {}
}
class Sparrow extends Bird {
void fly() {
System.out.println("Sparrow flies");
}
}
class Penguin extends Bird {
void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
Problem: Penguin
violates the LSP because it cannot fulfill the contract of the Bird
class (fly
behavior).
Solution: Refactor the design to use interfaces:
interface Flyable {
void fly();
}
class Sparrow implements Flyable {
void fly() {
System.out.println("Sparrow flies");
}
}
class Penguin {
// No fly implementation
}
4. Interface Segregation Principle (ISP)
Definition: A class should not be forced to implement methods it doesn’t use.
Bad Example:
interface Animal {
void fly();
void swim();
void run();
}
Good Example: Break the interface into smaller ones:
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
interface Runnable {
void run();
}
class Dog implements Runnable {
void run() { System.out.println("Dog runs"); }
}
class Dog implements Animal {
void fly() { /* Irrelevant */ }
void swim() { /* Irrelevant */ }
void run() { System.out.println("Dog runs"); }
}
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should depend on abstractions, not on low-level modules.
Bad Example:
class Database {
void connect() {}
}
class UserService {
Database db = new Database(); // Tightly coupled to Database class
}
Good Example: Use an interface for abstraction:
interface Database {
void connect();
}
class MySQLDatabase implements Database {
void connect() { System.out.println("Connected to MySQL"); }
}
class UserService {
private Database db;
UserService(Database db) {
this.db = db;
}
}
Design Patterns
1. Singleton Pattern
Definition: Ensures a class has only one instance and provides a global point of access.
Example:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Usage: Common in managing configurations or logging services.
2. Factory Pattern
Definition: Creates objects without exposing the creation logic to the client.
Example:
interface Animal {
void makeSound();
}
class Dog implements Animal {
public void makeSound() {
System.out.println("Bark");
}
}
class AnimalFactory {
public Animal createAnimal(String type) {
if ("dog".equals(type)) return new Dog();
return null;
}
}
3. Abstract Factory Pattern
Definition: A factory of factories that creates families of related objects.
Example:
interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
class WindowsUIFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
4. Observer Pattern
Definition: Defines a one-to-many dependency, notifying observers of any changes.
Example:
interface Observer {
void update(String message);
}
class NewsPublisher {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
public void notifyObservers(String news) {
for (Observer observer : observers) {
observer.update(news);
}
}
}
5. Adapter Pattern
Definition: Converts the interface of a class into another interface clients expect.
Example:
interface USSocket {
void voltage110V();
}
interface EuropeanSocket {
void voltage230V();
}
class SocketAdapter implements USSocket {
private EuropeanSocket europeanSocket;
public void voltage110V() {
europeanSocket.voltage230V(); // Conversion logic
}
}
Best Practices
- Understand the Problem: Patterns solve specific issues; don’t use them blindly.
- Don’t Overengineer: Start simple, and introduce patterns only when needed.
- Prioritize Readability: Clean, easy-to-understand code is better than overly complex designs.
- Refactor Regularly: Patterns evolve as the software grows.
- Favor Composition Over Inheritance: Use interfaces and delegation instead of deep inheritance hierarchies.