SOLID Principles

ANSIF
4 min readDec 13, 2024

--

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

  1. Understand the Problem: Patterns solve specific issues; don’t use them blindly.
  2. Don’t Overengineer: Start simple, and introduce patterns only when needed.
  3. Prioritize Readability: Clean, easy-to-understand code is better than overly complex designs.
  4. Refactor Regularly: Patterns evolve as the software grows.
  5. Favor Composition Over Inheritance: Use interfaces and delegation instead of deep inheritance hierarchies.

--

--

ANSIF
ANSIF

No responses yet