SOLID Design Principles in Java
SOLID design principles are five object-oriented design guidelines used to create software that is easier to understand, extend, test, and maintain.
SOLID stands for:
Letter | Principle | Main Idea |
|---|---|---|
S | Single Responsibility Principle | A class should have one responsibility |
O | Open-Closed Principle | Extend behaviour without modifying existing code |
L | Liskov Substitution Principle | Subclasses should safely replace their parent classes |
I | Interface Segregation Principle | Classes should not implement methods they do not need |
D | Dependency Inversion Principle | Depend on abstractions instead of concrete classes |
These five principles are commonly associated with loose coupling and maintainable object-oriented design.
Solid design principle in Java
1. Single Responsibility Principle
The Single Responsibility Principle (SRP) states that a class should have one responsibility or one main reason to change.
A class that handles multiple unrelated tasks becomes difficult to test and maintain.
SRP Example
// Responsible only for generating report dataclass Report { // Generate and return the report content String generate() { return "Monthly Sales Report"; }}// Responsible only for printing reportsclass ReportPrinter { // Print the generated report void print(Report report) { System.out.println(report.generate()); }}public class Main { public static void main(String[] args) { // Create the report and printer objects Report report = new Report(); ReportPrinter printer = new ReportPrinter(); // Print the report printer.print(report); // Output :- Monthly Sales Report }}Report generates the data, while ReportPrinter handles printing. Each class has a separate responsibility.
Single Responsibility Principle
2. Open-Closed Principle
The Open-Closed Principle (OCP) states that software components should be open for extension but closed for modification.
New functionality should be added by creating new classes rather than repeatedly changing tested code.
OCP Example
// Abstraction for different discount rulesinterface Discount { // Every discount must calculate a final price double apply(double price);}// Regular customers receive a 10% discountclass RegularDiscount implements Discount { @Override public double apply(double price) { return price * 0.90; }}// Premium customers receive a 20% discountclass PremiumDiscount implements Discount { @Override public double apply(double price) { return price * 0.80; }}// Calculator works with any Discount implementationclass PriceCalculator { double calculate(double price, Discount discount) { return discount.apply(price); }}public class Main { public static void main(String[] args) { // Create the calculator PriceCalculator calculator = new PriceCalculator(); // Use the regular discount implementation System.out.println( calculator.calculate(1000, new RegularDiscount()) ); // Output :- 900.0 // Use the premium discount implementation System.out.println( calculator.calculate(1000, new PremiumDiscount()) ); // Output :- 800.0 }}A new discount can be added by creating another implementation. The existing PriceCalculator does not need to be modified.
Open-Closed Principle
3. Liskov Substitution Principle
The Liskov Substitution Principle (LSP) states that a child-class object should be usable wherever its parent type is expected without breaking the program’s expected behaviour.
Subclasses must respect the behaviour promised by the base class.
LSP Example
// Base class defines a common movement operationabstract class Bird { // Every bird must provide valid movement behaviour abstract void move();}// Sparrow provides its appropriate movementclass Sparrow extends Bird { @Override void move() { System.out.println("Sparrow flies."); }}// Penguin provides its appropriate movementclass Penguin extends Bird { @Override void move() { System.out.println("Penguin swims."); }}public class Main { // This method works with any valid Bird subclass static void makeBirdMove(Bird bird) { bird.move(); } public static void main(String[] args) { // Substitute Bird with Sparrow makeBirdMove(new Sparrow()); // Output :- Sparrow flies. // Substitute Bird with Penguin makeBirdMove(new Penguin()); // Output :- Penguin swims. }}Both subclasses follow the Bird contract and can safely be passed to makeBirdMove().
Liskov Substitution Principle
4. Interface Segregation Principle
The Interface Segregation Principle (ISP) states that a class should not be forced to implement methods it does not need.
Instead of creating one large interface, create smaller interfaces for separate behaviours.
ISP Example
// Interface only for printing behaviourinterface Printable { void print();}// Interface only for scanning behaviourinterface Scannable { void scan();}// BasicPrinter needs only printing behaviourclass BasicPrinter implements Printable { @Override public void print() { System.out.println("Printing document."); }}// OfficeMachine supports both operationsclass OfficeMachine implements Printable, Scannable { @Override public void print() { System.out.println("Office machine is printing."); } @Override public void scan() { System.out.println("Office machine is scanning."); }}public class Main { public static void main(String[] args) { // BasicPrinter implements only what it needs Printable printer = new BasicPrinter(); printer.print(); // Output :- Printing document. // OfficeMachine implements both small interfaces OfficeMachine machine = new OfficeMachine(); machine.scan(); // Output :- Office machine is scanning. }}BasicPrinter is not forced to implement a scanning method because printing and scanning use separate interfaces.
Interface Segregation Principle
5. Dependency Inversion Principle
The Dependency Inversion Principle (DIP) states that high-level classes should depend on abstractions instead of concrete implementations.
This reduces coupling and makes implementations easier to replace or test.
DIP Example
// Abstraction for sending messagesinterface MessageSender { void send(String message);}// Concrete email implementationclass EmailSender implements MessageSender { @Override public void send(String message) { System.out.println("Email sent: " + message); }}// High-level class depends on the abstractionclass NotificationService { // Store the abstraction instead of a concrete sender private final MessageSender sender; // Receive the required implementation through the constructor NotificationService(MessageSender sender) { this.sender = sender; } // Send a notification through the supplied implementation void notifyUser(String message) { sender.send(message); }}public class Main { public static void main(String[] args) { // Supply a concrete implementation of the abstraction MessageSender sender = new EmailSender(); // NotificationService does not depend directly on EmailSender NotificationService service = new NotificationService(sender); // Send the notification service.notifyUser("Order shipped."); // Output :- Email sent: Order shipped. }}NotificationService depends on MessageSender, not directly on EmailSender. A different implementation, such as SmsSender, can be supplied without changing the service.
Dependency Inversion Principle
Benefits of SOLID Principles
Reduce tight coupling between classes
Make code easier to extend
Improve testability
Keep responsibilities organized
Support reusable components
Reduce the risk of changes breaking unrelated code
Summary
SOLID design principles improve object-oriented software design:
SRP ensures that classes have a single responsibility.
OCP allows functionality to be extended without modifying existing code.
LSP ensures that subclasses do not break base-class behaviour.
ISP prevents classes from implementing unnecessary methods.
Be the first to add a comment.