I'm always excited to take on new projects and collaborate with innovative minds.

Email

contact@niteshsynergy.com

Website

https://www.niteshsynergy.com/

SOLID Principles: The Foundation of Object-Oriented Design

The SOLID principles are five core guidelines that help developers design robust, maintainable, and scalable object-oriented software. These principles, introduced by Robert C. Martin (Uncle Bob), form the backbone of modern software development practices.

SOLID Principles: The Foundation of Object-Oriented Design
image-211.png

simple explanation of each SOLID principle:

Keep it simple:

  • A class should do only one job.
  • Example: A class that manages users shouldn’t send emails; let another class handle emails.

2. Open-Closed Principle (OCP)

Make it flexible:

  • Code should be easy to extend without changing existing code.
  • Example: If you add a new payment method, don’t modify old code—just add the new method.

 

3. Liskov Substitution Principle (LSP)

Stick to promises:

  • A subclass should always work like its parent class.
  • Example: If you replace a Rectangle object with a Square object, the behavior shouldn’t break.

 

4. Interface Segregation Principle (ISP)

Avoid unnecessary baggage:

  • Don’t force classes to implement methods they don’t need.
  • Example: A basic printer shouldn’t be forced to implement a fax() method.

5. Dependency Inversion Principle (DIP)

Focus on the big picture:

  • High-level parts of the system shouldn’t rely on low-level details. Both should depend on common abstractions.
  • Example: A notification system should work with any service (Email, SMS) by using a common interface.

 

 

. Single Responsibility Principle (SRP)

Meaning:

A class should have only one reason to change, meaning it should have only one responsibility.

Key Points:

  • A class should focus on a single task or functionality.
  • Reduces coupling and increases cohesion.
  • Simplifies debugging and testing.

Use Case:

A system that separates data access logic, business logic, and presentation logic.

Real-Time Complex Example:

A user management module where one class handles user data and another class handles email notifications.

Code Example (Without SRP):

class UserManager {
   public void createUser(String name, String email) {
       System.out.println("User created with name: " + name);
       sendEmail(email);
   }
   public void sendEmail(String email) {
       System.out.println("Email sent to: " + email);
   }
}

 

Refactored Code (With SRP):

class UserManager {
   private EmailService emailService;
   public UserManager(EmailService emailService) {
       this.emailService = emailService;
   }
   public void createUser(String name, String email) {
       System.out.println("User created with name: " + name);
       emailService.sendEmail(email);
   }
}
class EmailService {
   public void sendEmail(String email) {
       System.out.println("Email sent to: " + email);
   }
}

 

2. Open-Closed Principle (OCP)

Meaning:

Software entities (classes, modules, functions) should be open for extension but closed for modification.

Key Points:

  • You can add new functionality without altering existing code.
  • Achieved through abstraction and polymorphism.
  • Helps prevent regression errors.

Use Case:

A payment processing system that supports multiple payment methods (credit card, PayPal, etc.).

Real-Time Complex Example:

Adding a new payment type without modifying the existing code.

Code Example (Without OCP):

class PaymentProcessor {
   public void processPayment(String type) {
       if (type.equals("CreditCard")) {
           System.out.println("Processing credit card payment...");
       } else if (type.equals("PayPal")) {
           System.out.println("Processing PayPal payment...");
       }
   }
}

 

Refactored Code (With OCP):

interface Payment {
   void pay();
}
class CreditCardPayment implements Payment {
   public void pay() {
       System.out.println("Processing credit card payment...");
   }
}
class PayPalPayment implements Payment {
   public void pay() {
       System.out.println("Processing PayPal payment...");
   }
}
class PaymentProcessor {
   public void processPayment(Payment payment) {
       payment.pay();
   }
}

3. Liskov Substitution Principle (LSP)

Meaning:

Derived classes should be substitutable for their base classes without affecting the correctness of the program.

Key Points:

  • Avoids using subclasses that break the functionality of a base class.
  • Helps ensure the "is-a" relationship is maintained.

Use Case:

A shape drawing application that works for both circles and squares.

Real-Time Complex Example:

A class hierarchy where a subclass doesn't override behavior that contradicts the base class.

Code Example (Violation of LSP):

class Rectangle {
   int width, height;
   public void setWidth(int width) {
       this.width = width;
   }
   public void setHeight(int height) {
       this.height = height;
   }
   public int getArea() {
       return width * height;
   }
}
class Square extends Rectangle {
   @Override
   public void setWidth(int width) {
       super.setWidth(width);
       super.setHeight(width);
   }
   @Override
   public void setHeight(int height) {
       super.setHeight(height);
       super.setWidth(height);
   }
}

Refactored Code (With LSP):

 

interface Shape {
   int getArea();
}
class Rectangle implements Shape {
   int width, height;
   public Rectangle(int width, int height) {
       this.width = width;
       this.height = height;
   }
   public int getArea() {
       return width * height;
   }
}
class Square implements Shape {
   int side;
   public Square(int side) {
       this.side = side;
   }
   public int getArea() {
       return side * side;
   }
}

 

4. Interface Segregation Principle (ISP)

Meaning:

A class should not be forced to implement interfaces it does not use.

Key Points:

  • Split large interfaces into smaller, specific ones.
  • Ensures only relevant methods are implemented.

Use Case:

A printer interface that segregates basic printing and advanced faxing/scanning functionalities.

Real-Time Complex Example:

A multifunctional printer (MFP) where some models don't support scanning or faxing.

Code Example (Without ISP):

 

interface Printer {
   void print();
   void scan();
   void fax();
}
class BasicPrinter implements Printer {
   public void print() {
       System.out.println("Printing...");
   }
   public void scan() {
       throw new UnsupportedOperationException("Scan not supported");
   }
   public void fax() {
       throw new UnsupportedOperationException("Fax not supported");
   }
}


 

Refactored Code (With ISP):

interface Print {
   void print();
}
interface Scan {
   void scan();
}
interface Fax {
   void fax();
}
class BasicPrinter implements Print {
   public void print() {
       System.out.println("Printing...");
   }
}
class MultiFunctionPrinter implements Print, Scan, Fax {
   public void print() {
       System.out.println("Printing...");
   }
   public void scan() {
       System.out.println("Scanning...");
   }
   public void fax() {
       System.out.println("Faxing...");
   }
}

 

5. Dependency Inversion Principle (DIP)

Meaning:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Key Points:

  • Increases modularity and flexibility.
  • Dependency injection is a common implementation.

Use Case:

A notification system where the high-level module doesn't depend on specific email or SMS classes.

Real-Time Complex Example:

Switching from email notifications to SMS notifications without changing the core business logic.

Code Example (Without DIP):

 

class EmailService {
   public void sendEmail(String message) {
       System.out.println("Sending email: " + message);
   }
}
class NotificationManager {
   private EmailService emailService;
   public NotificationManager() {
       this.emailService = new EmailService();
   }
   public void notify(String message) {
       emailService.sendEmail(message);
   }
}

 

Refactored Code (With DIP):

interface NotificationService {
   void notify(String message);
}
class EmailService implements NotificationService {
   public void notify(String message) {
       System.out.println("Sending email: " + message);
   }
}
class SMSService implements NotificationService {
   public void notify(String message) {
       System.out.println("Sending SMS: " + message);
   }
}
class NotificationManager {
   private NotificationService notificationService;
   public NotificationManager(NotificationService notificationService) {
       this.notificationService = notificationService;
   }
   public void notify(String message) {
       notificationService.notify(message);
   }
}

Why Are SOLID Principles Important?

  1. Maintainability: Easier to debug and modify code.
  2. Scalability: Supports new requirements without breaking existing code.
  3. Testability: Enhances unit testing by promoting loose coupling.
  4. Reusability: Encourages modular design.
  5. Readability: Clear structure simplifies understanding for new developers.

By adhering to these principles, developers create robust, flexible, and efficient software systems.

 

 

1. Single Responsibility Principle (SRP)

Each class or module in the game should do only one thing.

Examples:

  1. PlayerManager: Handles player stats like health, score, and inventory.
  2. EnemySpawner: Handles spawning enemies at specific intervals or locations.
  3. SoundManager: Plays background music or sound effects.
  4. CollisionHandler: Manages collision detection between game objects (e.g., player and enemy).

2. Open-Closed Principle (OCP)

The game should be easy to expand without modifying existing code.

Examples:

  1. Adding new weapons: Add a new weapon class (e.g., LaserGun) without changing the base Weapon code.
  2. Adding new enemy types: Introduce a new enemy (e.g., FlyingEnemy) by extending a base Enemy class.
  3. Adding new power-ups: Add a new power-up (e.g., ShieldPowerUp) without touching existing power-ups.
  4. Adding new levels: Create a new Level class with unique features without altering the LevelManager.

3. Liskov Substitution Principle (LSP)

You should be able to replace one class with its subclass without breaking the game.

Examples:

  1. A FlyingEnemy should behave like a regular Enemy when added to the enemy list.
  2. A BossEnemy should fit anywhere a normal Enemy is expected but with advanced behaviors.
  3. A PlayerControlledCar should behave like a Vehicle in a racing game.
  4. A MagicBullet should work like a Bullet but with added effects (e.g., area damage).

4. Interface Segregation Principle (ISP)

Game objects should only implement methods they actually use.

Examples:

  1. A PlayerCharacter implements Move() and Attack(), but not Fly() if the player can’t fly.
  2. A FlyingEnemy implements Fly() but not Swim() since it never swims.
  3. A NPCVendor implements Talk() and Trade(), but not Attack().
  4. A StaticObstacle (e.g., a wall) doesn’t need Move() or Attack(), only Collide().

5. Dependency Inversion Principle (DIP)

High-level game systems should rely on abstractions, not specific implementations.

Examples:

  1. WeaponSystem depends on a generic Weapon interface, so it works with Sword, Gun, or Bow.
  2. EnemyAI depends on an MovementStrategy interface, allowing different movement behaviors (e.g., Zigzag, Random).
  3. GameNotification depends on a NotificationService interface, so it can use EmailService, InGamePopup, or PushNotification.
  4. SoundManager uses an AudioOutput interface, so it works with headphones, speakers, or virtual sound systems.

 

 

 

 

Thank You for Your Support! 🙏

Your encouragement keeps us going!

If you find value in our content, please consider supporting us.

💡 Even a small contribution can make a big difference in helping us build better educational resources.

Donate Now

 

8 min read
Nov 23, 2024
By Nitesh Synergy
Share