0% found this document useful (0 votes)
2 views6 pages

SOLID Principles in Java – Simplified With Code

This guide explains the SOLID principles in Java, which help developers create maintainable, scalable, and testable software. Each principle is broken down with clear explanations and practical code examples, covering Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles. By following these principles, developers can enhance code quality and reduce complexity in their applications.

Uploaded by

topbinge24
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views6 pages

SOLID Principles in Java – Simplified With Code

This guide explains the SOLID principles in Java, which help developers create maintainable, scalable, and testable software. Each principle is broken down with clear explanations and practical code examples, covering Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles. By following these principles, developers can enhance code quality and reduce complexity in their applications.

Uploaded by

topbinge24
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 6

SOLID Principles in Java 3 Simplified with Code

This guide breaks down each principle with clear explanations and practical Java code examples. By adhering to these principles, developers can build more maintainable, scalable, and testable
software systems.

Single Responsibility (SRP) Open/Closed (OCP) Liskov Substitution (LSP) Interface Segregation (ISP)
Focuses on classes having only one reason to Advocates for software entities being open for Emphasizes that objects of a superclass Suggests that clients should not be forced to
change, ensuring clear responsibilities. extension, but closed for modification. should be replaceable with objects of its depend on interfaces they do not use.
subclasses without breaking the application.

Dependency Inversion (DIP)


Promotes depending on abstractions, not
concrete implementations, for flexible and
decoupled systems.
S 3 Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) dictates that a class should have one, and only one, reason to change. In other words, each class or module should be responsible for a single piece of
functionality, or have only one job. This principle is fundamental for creating modular and maintainable codebases, as it prevents classes from becoming bloated "God objects" that are difficult to
manage and prone to bugs when changes are introduced.

When a class has multiple responsibilities, a change in one responsibility might unintentionally affect another. This leads to tightly coupled code, making it harder to debug, test, and extend. By
segregating responsibilities, each component becomes more focused, easier to understand, and simpler to modify without impacting unrelated parts of the system.

ü Bad Example: Violating SRP ' Good Example: Adhering to SRP

class UserManager { class UserRepository {


void addUser(User u) { /* Handles user persistence */ } void addUser(User u) { /* Handles user persistence */ }
void validateUser(User u) { /* Handles user data validation */ } }
void sendEmail(User u) { /* Handles email notifications */ }
} class UserValidator {
void validate(User u) { /* Handles user data validation */ }
In this example, the UserManager class is responsible for database operations, data validation, }
and email communication. If the email sending logic changes, or the user validation rules are
updated, this single class would need modification, potentially introducing risks to its other class EmailService {
functionalities. void sendEmail(User u) { /* Handles email notifications */ }
}

Here, each responsibility is extracted into its own dedicated class: UserRepository for data
access, UserValidator for validation, and EmailService for sending emails. Now, changes to
validation logic will only affect UserValidator, making the system more robust and easier to
manage.
O 3 Open/Closed Principle (OCP)
The Open/Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means you should be able to add
new functionalities to a system without altering existing code. This principle is crucial for building flexible and resilient systems that can evolve without constant refactoring of established, working
components.

Achieving OCP often involves using abstractions like interfaces and abstract classes. By programming to an interface rather than a concrete implementation, you can introduce new
implementations without changing the client code that uses the interface. This promotes a modular design where new behaviors can be "plugged in" without disturbing the core logic.

1 2

Open for Extension Closed for Modification


This means that the behavior of the module can be extended to accommodate new This implies that once a module has been developed and tested, its source code should not
requirements. New features can be added by writing new code. be changed to add new functionality. Existing code remains untouched.

Java Example: Payment Processing System

interface Payment { class CardPayment implements Payment {


void pay(int amount); @Override
} public void pay(int amount) {
System.out.println("Processing Card Payment of " + amount);
Here, we define a Payment interface, which declares the contract for any payment method. // ... card specific logic
This abstraction serves as the "closed" part; client code will depend on this interface, not on }
specific payment implementations. }

class UpiPayment implements Payment {


@Override
public void pay(int amount) {
System.out.println("Processing UPI Payment of " + amount);
// ... UPI specific logic
}
}

Now, we can add new payment methods (like CardPayment or UpiPayment) by creating new
classes that implement the Payment interface. This adheres to OCP because we are extending
the system's payment capabilities without modifying the existing Payment interface or any
client code that uses it.
L 3 Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) states that if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of that
program. In simpler terms, a subclass should be able to completely substitute its superclass without breaking the application. This principle is crucial for ensuring that inheritance is used correctly
and that polymorphism works as expected.

LSP goes beyond simple type checking; it focuses on behavioral subtyping. This means that a subclass must not only conform to the method signatures of its superclass but also to its behavioral
contracts, including preconditions, postconditions, and invariants. Violations of LSP often lead to unexpected behavior, runtime errors, and code that is difficult to maintain because client code has
to add special checks for subclass types.

Java Example: Bird Flight

' Good Example: Adhering to LSP ü Bad Example: Violating LSP

class Bird { // Bad: Ostrich extends Bird but can't fly


void fly() { class Ostrich extends Bird {
System.out.println("Bird is flying."); @Override
} void fly() {
} throw new UnsupportedOperationException("Ostriches cannot fly!");
}
class Sparrow extends Bird { }
@Override
void fly() { In this scenario, an Ostrich extends Bird, but it cannot perform the fly() operation. If a client
System.out.println("Sparrow is flying gracefully."); expects any Bird to fly and attempts to call fly() on an Ostrich object, it will result in an
} UnsupportedOperationException. This violates LSP because Ostrich cannot be substituted for
} Bird without breaking the program's expected behavior. A better design would be to introduce
an interface Flyable and only implement it in birds that can fly.
Here, a Sparrow is a type of Bird, and it can indeed fly. If you substitute a Bird object with a
Sparrow object, the fly() method will still work as expected, fulfilling the contract defined by the
superclass.
I 3 Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they do not use. In simpler terms, rather than having one large, monolithic interface, it is
better to have many small, role-specific interfaces. This principle aims to reduce the side effects of changes in software by keeping interfaces focused and cohesive.

When an interface is too broad, implementing classes are forced to provide implementations for methods they don't need, often leading to empty method bodies or the throwing of
UnsupportedOperationException (a common indicator of LSP violation stemming from an ISP violation). Segregating interfaces ensures that classes only implement methods relevant to their
specific role, leading to cleaner, more maintainable code and a clearer contract for each component.

Java Example: Animal Abilities

ü Bad Example: Fat Interface ' Good Example: Segregated Interfaces

interface Animal { interface Flyable {


void fly(); void fly();
void swim(); }
void walk();
} interface Swimmable {
void swim();
In this scenario, the Animal interface forces any implementing class to provide implementations }
for fly(), swim(), and walk(). What if you have a Fish that can only swim, or a Snake that can only
walk? They would be forced to implement fly() (and swim() for Snake), leading to empty or interface Walkable {
exception-throwing methods. void walk();
}

class Duck implements Flyable, Swimmable, Walkable {


// ... implementations for all three methods
}

class Fish implements Swimmable {


// ... implementation for swim only
}

By breaking down the large Animal interface into smaller, role-specific interfaces like Flyable,
Swimmable, and Walkable, classes can now implement only the interfaces relevant to their
capabilities. A Duck can implement all three, while a Fish only needs to implement Swimmable.
This makes the design more flexible and avoids unnecessary dependencies.
D 3 Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) states that:

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

This principle promotes a design where the flow of control is "inverted" 3 instead of high-level components directly controlling low-level components, they both depend on an abstract interface.
This reduces tight coupling, making the system more flexible, testable, and adaptable to changes.

DIP is often achieved through Dependency Injection, where dependencies are provided to a class rather than being created by the class itself. This allows for easier swapping of implementations
(e.g., using a mock database for testing) and promotes a more modular architecture.

Java Example: User Service and Database

ü Bad Example: Violating DIP ' Good Example: Adhering to DIP

class MySQLDatabase { // Low-level module interface Database { // Abstraction


public void save(String data) { void save(String data);
System.out.println("Saving to MySQL: " + data); }
}
} class MySQLDatabase implements Database { // Low-level module, depends on
abstraction
class UserService { // High-level module @Override
private MySQLDatabase db = new MySQLDatabase(); // Direct dependency public void save(String data) {
public void createUser(String userData) { System.out.println("Saving to MySQL: " + data);
// ... business logic ... }
db.save(userData); }
}
} class UserService { // High-level module, depends on abstraction
private Database db;
Here, UserService (high-level module) directly depends on MySQLDatabase (low-level module).
If you later decide to switch to PostgreSQL or a NoSQL database, you would have to modify // Dependency Inversion via constructor injection
UserService, which violates OCP and makes testing difficult. public UserService(Database db) {
this.db = db;
}

public void createUser(String userData) {


// ... business logic ...
db.save(userData);
}
}

Now, UserService depends on the Database interface (abstraction), not the concrete
MySQLDatabase. MySQLDatabase also depends on the Database abstraction. This means you
can easily swap MySQLDatabase with any other Database implementation without changing
UserService, fostering flexibility and testability.

You might also like