Object-Oriented Design Principles in Java

📘 Premium Read: Access my best content on Medium member-only articles — deep dives into Java, Spring Boot, Microservices, backend architecture, interview preparation, career advice, and industry-standard best practices.

🎓 Top 15 Udemy Courses (80-90% Discount): My Udemy Courses - Ramesh Fadatare — All my Udemy courses are real-time and project oriented courses.

▶️ Subscribe to My YouTube Channel (176K+ subscribers): Java Guides on YouTube

▶️ For AI, ChatGPT, Web, Tech, and Generative AI, subscribe to another channel: Ramesh Fadatare on YouTube

Introduction

Object-Oriented Design (OOD) principles are the foundation of building robust, scalable, and maintainable software. These principles guide the design and implementation of object-oriented systems, ensuring that the software is modular, flexible, and easy to understand. These principles are particularly important in Java as it is an object-oriented programming language.

Table of Contents

  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)
  6. Encapsulate What Varies
  7. DRY (Don't Repeat Yourself)
  8. YAGNI (You Aren't Gonna Need It)
  9. KISS (Keep It Simple, Stupid)
  10. Composition over Inheritance
  11. Dependency Injection
  12. Conclusion

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Example:

// A class with a single responsibility: handling user authentication public class AuthService { public boolean authenticate(String username, String password) { // Authentication logic return true; } } // A class with a single responsibility: managing user profiles public class UserProfileService { public void updateProfile(String userId, String newProfileData) { // Update profile logic } } 

Explanation:

  • AuthService: Handles only authentication-related tasks.
  • UserProfileService: Manages user profile-related tasks separately.

2. Open/Closed Principle (OCP)

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

The benefit of this Object-oriented design principle is that it prevents someone from changing already tried and tested code.

Example:

// Base class public abstract class Shape { abstract void draw(); } // Extension 1 public class Circle extends Shape { @Override void draw() { System.out.println("Drawing Circle"); } } // Extension 2 public class Rectangle extends Shape { @Override void draw() { System.out.println("Drawing Rectangle"); } } // Usage public class Drawing { public void drawShape(Shape shape) { shape.draw(); } } 

Explanation:

  • Shape: Base class that is closed for modification.
  • Circle and Rectangle: Classes that extend the base class, open for extension.

3. Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.

Example:

// Base class public class Bird { public void fly() { System.out.println("Bird is flying"); } } // Subclass adhering to LSP public class Sparrow extends Bird { } // Subclass violating LSP (if Penguin were to override fly with an exception or no-op) public class Penguin extends Bird { @Override public void fly() { throw new UnsupportedOperationException("Penguins can't fly"); } } 

Explanation:

  • Sparrow: Can be used in place of Bird without any issues.
  • Penguin: Violates LSP as it cannot perform the fly operation expected of a Bird.

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they do not use. Instead, interfaces should be segregated into smaller, more specific ones.

Example:

// Segregated interfaces public interface Printer { void print(); } public interface Scanner { void scan(); } // Class implementing only the required interfaces public class MultiFunctionPrinter implements Printer, Scanner { @Override public void print() { System.out.println("Printing..."); } @Override public void scan() { System.out.println("Scanning..."); } } public class SimplePrinter implements Printer { @Override public void print() { System.out.println("Printing..."); } } 

Explanation:

  • Printer and Scanner: Smaller, more specific interfaces.
  • MultiFunctionPrinter and SimplePrinter: Implement only the interfaces they need.

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces). Abstractions should not depend on details. Details should depend on abstractions.

Example:

// Abstraction public interface MessageService { void sendMessage(String message, String receiver); } // Low-level module public class EmailService implements MessageService { @Override public void sendMessage(String message, String receiver) { System.out.println("Email sent to " + receiver + " with message: " + message); } } // High-level module public class Notification { private MessageService messageService; public Notification(MessageService messageService) { this.messageService = messageService; } public void send(String message, String receiver) { messageService.sendMessage(message, receiver); } } // Usage public class Main { public static void main(String[] args) { MessageService emailService = new EmailService(); Notification notification = new Notification(emailService); notification.send("Hello, Dependency Inversion Principle!", "user@example.com"); } } 

Explanation:

  • MessageService: Interface representing the abstraction.
  • EmailService: Concrete implementation of the abstraction.
  • Notification: High-level module depending on the abstraction.

6. Encapsulate What Varies

Definition: Identify the aspects of your application that vary and separate them from what stays the same.

Example:

// Encapsulating the varying part: Payment Strategy public interface PaymentStrategy { void pay(int amount); } public class CreditCardPayment implements PaymentStrategy { @Override public void pay(int amount) { System.out.println("Paid " + amount + " using Credit Card"); } } public class PayPalPayment implements PaymentStrategy { @Override public void pay(int amount) { System.out.println("Paid " + amount + " using PayPal"); } } // Context using the strategy public class ShoppingCart { private PaymentStrategy paymentStrategy; public void setPaymentStrategy(PaymentStrategy paymentStrategy) { this.paymentStrategy = paymentStrategy; } public void checkout(int amount) { paymentStrategy.pay(amount); } } // Usage public class Main { public static void main(String[] args) { ShoppingCart cart = new ShoppingCart(); cart.setPaymentStrategy(new CreditCardPayment()); cart.checkout(100); cart.setPaymentStrategy(new PayPalPayment()); cart.checkout(200); } } 

Explanation:

  • PaymentStrategy: Interface representing the varying part.
  • CreditCardPayment and PayPalPayment: Concrete implementations of the varying part.
  • ShoppingCart: Context using the strategy.

7. DRY (Don't Repeat Yourself)

Definition: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Avoid duplicate code by abstracting common things and placing them in a single location.

Example:

public class UserService { public User getUserById(String userId) { // Common logic to fetch user by ID } public void updateUser(User user) { // Common logic to update user } } 

Explanation:

  • UserService: Contains common methods for user operations, avoiding repetition.

8. YAGNI (You Aren't Gonna Need It)

Definition: Don't add functionality until it is necessary.

Example:

public class UserService { public void createUser(String name, String email) { // Only the necessary code for creating a user } } 

Explanation:

  • UserService: Only contains code for creating a user, no unnecessary features.

9. KISS (Keep It Simple, Stupid)

Definition: Simplicity should be a key goal in design, and unnecessary complexity should be avoided.

Example:

public class Calculator { public int add(int a, int b) { return a + b; } public int subtract(int a, int b) { return a - b; } } 

Explanation:

  • Calculator: Simple methods for basic arithmetic operations, no unnecessary complexity.

10. Composition over Inheritance

Definition: Favor composition over inheritance to achieve code reuse and flexibility.

Example:

// Composition public class Engine { public void start() { System.out.println("Engine started"); } } public class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); System.out.println("Car started"); } } 

Explanation:

  • Engine: A class representing an engine.
  • Car: Uses an instance of Engine (composition) rather than inheriting from it.

11. Dependency Injection

Definition: A design pattern that allows a class to receive its dependencies from an external source rather than creating them itself.

Example:

// Dependency Injection public interface Service { void execute(); } public class ServiceImpl implements Service { @Override public void execute() { System.out.println("Service executed"); } } // Injector class public class ServiceInjector { public static Service getService() { return new ServiceImpl(); } } // Client class public class Client { private Service service; // Constructor injection public Client (Service service) { this.service = service; } public void doSomething() { service.execute(); } public static void main(String[] args) { // Injecting the dependency Service service = ServiceInjector.getService(); Client client = new Client(service); client.doSomething(); } } 

Explanation:

  • Service: Interface representing a service.
  • ServiceImpl: Concrete implementation of the service.
  • ServiceInjector: Provides the service instance.
  • Client: Receives the service dependency through constructor injection.

12. Conclusion

Object-Oriented Design principles are essential for building scalable, maintainable, and robust software systems. By adhering to principles like SRP, OCP, LSP, ISP, and DIP, as well as best practices like DRY, YAGNI, KISS, and composition over inheritance, developers can create software that is easy to understand, extend, and maintain. Additionally, applying Dependency Injection and encapsulating what varies can further enhance the flexibility and testability of your code. Understanding and applying these principles is key to becoming an effective Java developer.

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare