Design Principles in Java

43
0

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

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 data
class Report {
// Generate and return the report content
String generate() {
return "Monthly Sales Report";
}
}
// Responsible only for printing reports
class 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

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 rules
interface Discount {
// Every discount must calculate a final price
double apply(double price);
}
// Regular customers receive a 10% discount
class RegularDiscount implements Discount {
@Override
public double apply(double price) {
return price * 0.90;
}
}
// Premium customers receive a 20% discount
class PremiumDiscount implements Discount {
@Override
public double apply(double price) {
return price * 0.80;
}
}
// Calculator works with any Discount implementation
class 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

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 operation
abstract class Bird {
// Every bird must provide valid movement behaviour
abstract void move();
}
// Sparrow provides its appropriate movement
class Sparrow extends Bird {
@Override
void move() {
System.out.println("Sparrow flies.");
}
}
// Penguin provides its appropriate movement
class 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

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 behaviour
interface Printable {
void print();
}
// Interface only for scanning behaviour
interface Scannable {
void scan();
}
// BasicPrinter needs only printing behaviour
class BasicPrinter implements Printable {
@Override
public void print() {
System.out.println("Printing document.");
}
}
// OfficeMachine supports both operations
class 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

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 messages
interface MessageSender {
void send(String message);
}
// Concrete email implementation
class EmailSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("Email sent: " + message);
}
}
// High-level class depends on the abstraction
class 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

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.

CS Core

Read Similar Blogs

Comments0