

In the ever-evolving world of software development, writing clean, maintainable, and scalable code is more important than ever. "Mastering SOLID Principles: A Simplified Guide to Better Code" is your essential guide to understanding and applying the SOLID principles, a set of five foundational concepts that will transform the way you design and write code.
What is SOLID?SOLID is an acronym representing five key principles that help developers build robust and adaptable software. These principles—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—are essential for creating systems that are easy to understand, extend, and maintain.
Why This Book?This book is designed for developers at all levels, from beginners to seasoned professionals. It takes complex concepts and breaks them down into simple, easy-to-understand explanations. Through practical examples and real-world analogies, you'll gain a clear grasp of how to implement these principles in your own projects.
Inside You'll Find:
Clear Explanations: Each principle is described in straightforward terms, making it easy to grasp even if you're new to software design.
Practical Examples: Real-life scenarios and code examples illustrate how each principle can be applied to solve common design challenges.
Illustrations and Analogies: Engaging visuals and relatable analogies help you understand abstract concepts and see their real-world applications.
Best Practices: Learn how to apply SOLID principles effectively and avoid common pitfalls in software design.
Who Should Read This Book?Whether you're a student learning about object-oriented design, a professional developer looking to refine your skills, or a team lead aiming to enhance code quality, this book provides valuable insights and practical knowledge that will benefit your coding practices.
Why SOLID MattersImplementing SOLID principles leads to more flexible, resilient, and maintainable code. By following these principles, you ensure that your software is easier to modify, extend, and test—ultimately leading to more successful projects and happier users.
Dive into "Mastering SOLID Principles: A Simplified Guide to Better Code" and discover how applying these core principles can make a significant difference in your development journey. Transform your codebase, improve your software architecture, and write better code today!
SOLID
The SOLID principles are a set of five design principles that help software developers create more maintainable, flexible, and robust object-oriented software. Each principle addresses a specific aspect of software design and, when applied together, promotes good software design practices.
Here's an very high level overview of each of the SOLID principles
Single Responsibility Principle (SRP)
Open-Closed Principle (OCP)
Liskov Substitution Principle (LSP)
Interface Segregation Principle (ISP)
Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) means that a class should have only one job or one reason to change.
Imagine you're building a toy car. There’s one part that moves the wheels, another part that makes sounds, and a separate one that flashes lights. Each part has a different job. If you want to change how the wheels move, you only change the wheel part, not the sound or light parts.
In Java, this means a class should do only one thing. If it does more, it might become confusing, like if your toy car’s wheel part also tried to make sounds.
Imagine a robot that only cleans your room. If it also started doing your homework, things might get messy. Each thing (or robot) should have just one job. In programming, each part of the code should focus on doing one thing.
Idea: A class should have only one job or responsibility.
Example: Think of a remote control. It has a single responsibility: to send commands to a device. If you add more responsibilities, like handling power, volume, and channels, it becomes less focused.
Example: If you have a class for handling user authentication, it shouldn't also be responsible for sending emails. Each class should focus on doing one thing well.
Here are the key points of the Single Responsibility Principle in OOAD
One Reason to Change: Each class should have only one responsibility or reason to change. If there is more than one reason for a class to change, it becomes more error-prone and harder to maintain.
Cohesion: The principle encourages high cohesion within classes, meaning that the class should contain members (methods and attributes) that are closely related and work together to achieve a single responsibility.
Separation of Concerns: It helps in separating different concerns or aspects of the software system into distinct classes. This separation makes the system more modular and easier to understand, modify, and maintain.
Reusability: Classes that adhere to the SRP are often more reusable, as they are specialized in a single responsibility and can be used in various contexts.
Here's an example to illustrate the Single Responsibility Principle
Suppose you're designing a banking software system. You might have a BankAccount class. According to SRP, this class should have only one responsibility, which is managing the bank account balance.
public class BankAccount {
public double getAccountBalance(){
// ADD your code to fetch Bank account
return Math.random();
}
public void createBankAccount(){
//LOGIC to create bank Account
}
public boolean validateUser(UserData request){
//Validate User
return Boolean.TRUE;
}
public void sendEmail(String emailId){
//send email to User
}
}
NOTE: It should not be responsible for handling user authentication, sending emails, or other unrelated tasks.
So, user authentication and email sending should be handled by separate classes with their own single responsibilities. This separation allows for easier maintenance, testing, and future changes without affecting unrelated parts of the system.
In summary, the Single Responsibility Principle in OOAD promotes the idea that each class should have a clear, single, and well-defined responsibility, which contributes to more maintainable and robust software systems.
We should put methods in specific classes , classes should be very specific for its purposes.As you can see below we have created 3 classes and kept their specific functionality in the class.
public class BankAccount {
public double getAccountBalance(){
// ADD your code to fetch Bank account
return Math.random();
}
public void createBankAccount(){
//LOGIC to create bank Account
}
}
Public class UserValidator{
public boolean validateUser(UserData request){
//Validate User
return Boolean.TRUE;
}
}
Public class EmailNotification{
public void sendEmail(String emailId){
//send email to User
}
}
Example 2:
public class EmployeeData {
private String name;
Long empId;
//get Name
public String getName() {
return name;
}
//get Employee ID
public Long getEmpId() {
return empId;
}
// Method to save an employee to the database
public void saveToDatabase() {
// Code to save the employee data to a database
//add your database insertion logic here
}
}
In this design, the Employee class has two responsibilities
1. Managing employee data (storing and retrieving employee attributes).
2. Saving employee data to a database.
Revised Design (Adhering to SRP)
To adhere to the Single Responsibility Principle, we separate the responsibilities into different classes. We create an Employee class for managing employee data and an EmployeeRepository class for database operations.
public class EmployeeService {
private String name;
Long empId;
//get Name
public String getName() {
return name;
}
//get Employee ID
public Long getEmpId() {
return empId;
}
}
public class EmployeeRepo {
// Method to save an employee to the database
public void saveToDatabase() {
// Code to save the employee data to a database
//add your database insertion logic here
}
}
As you can see above
1. The EmployeeData class is responsible for managing employee data (attributes and methods related to employee information).
2. The EmployeeRepository class is responsible for saving and retrieving employee data from the database.
By separating these responsibilities into different classes, we adhere to the Single Responsibility Principle. This design promotes code maintainability and flexibility because changes to the way employee data is persisted (e.g., switching from a database to a file system) won't impact the core employee data management logic in the Employee class.
In any real time applications, you need to design your code in such a way that it will somehow use the SOLID principle to make it more flexible,readable and robust.
Signs that SRP is violated:
A class or module has methods that perform unrelated tasks.
A class often changes for multiple reasons (e.g., database changes, business logic updates, or UI changes).
Difficulty in explaining what the class does in a single sentence.
How to apply SRP:
When designing a class, ask yourself: “What is the single responsibility of this class?”
If you can identify more than one responsibility, consider splitting the class into two or more classes.
Group similar responsibilities together and keep unrelated responsibilities in separate classes.
Real-world Example:
Consider a shopping cart system in an e-commerce application:
If one class handles both the logic for calculating the total price of items in the cart and processing payments, it violates SRP.
By splitting these responsibilities into a CartCalculator class for calculating totals and a PaymentProcessor class for handling payments, each class focuses on one task. This leads to cleaner, easier-to-maintain code.
Benefits of SRP:
Improved Readability: Code is easier to read and understand.
Modular Design: Changes to one responsibility don't affect others.
Scalability: As your system grows, you can easily extend or modify individual parts without breaking others.
Easier Debugging: Since each class has one responsibility, debugging becomes more straightforward.
Summary
|
Open-Closed Principle (OCP)
it says that code should be open for extension but closed for modification.
What does it mean?
Open for extension: You should be able to add new features or functionality.
Closed for modification: You shouldn’t change the existing code every time you add something new. Instead of modifying the existing thing ,you can simply add new behavior.
Idea: Software entities(classes, modules, functions) should be open for extension but closed for modification.
Example: Instead of modifying existing code, you should be able to add new features through extensions. Think of it as adding new layers to a cake without changing the existing layers.
Example: Imagine a shape drawing application. You can add new shapes without changing how existing shapes are drawn. For instance, you can add a new "Triangle" class without modifying the existing "Circle" or "Square" classes.
The open/closed principle states that classes should be open for extension but closed to change.
A class is considered “closed” to editing once it has:
Been tested to be functioning properly. The class should behave as expected.
All the attributes and behaviors are encapsulated,
Been proven to be stable within your system. The class or any instance of the class should not stop your system from running or do it harm.
Although the principle is called “closed”, it does not mean that changes cannot be made to a class during development. Things should change during the design and analysis phase of your development cycle. A “closed” class occurs when a point has been reached in development when most of the design decisions have been finalized and once you have implemented most of your system.
During the lifecycle of your software, certain classes should be closed to further changes to avoid introducing undesirable side effects. A “closed” class should still be fixed if any bugs or unexpected behaviors occur.
If a system needs to be extended or have more features added, then the “open” side of the principle comes into play. An “open” class is one that can still be built upon. There are two different ways to extend a system with the open principle.
The first way is through the inheritance of a superclass. Inheritance can be used to simply extend a class that is considered closed when you want to add more attributes and behaviors. The subclasses will have the original functions of the superclass, but extra features can be added in the subclasses. This helps preserve the integrity of the superclass, so if the extra features of the subclasses are not needed, the original.
Assume you have a system that calculates the areas of different shapes, and you want to extend it to support new shapes without modifying the existing code.This is how you can do it.
Example:
Without OCP:
Let’s say you have a program that calculates discounts for products. Right now, it gives a 10% discount. But later, you might want to add other types of discounts.
Here’s a simple code that doesn’t follow OCP:
public class DiscountCalculator {
public double calculateDiscount(String customerType, double price) {
if (customerType.equals("regular")) {
return price * 0.1; // 10% discount
} else if (customerType.equals("premium")) {
return price * 0.2; // 20% discount for premium customers
}
return 0;
}
}
Problem:
What happens if later you need to add a holiday discount? You’ll have to modify the calculateDiscount method and add more if conditions. Every time you add a new discount type, you are modifying the existing code, making it harder to maintain. This violates the Open/Closed Principle.
With OCP:
Now, let’s refactor the code to follow the Open/Closed Principle. Instead of changing the DiscountCalculator every time, we’ll make it open for extension by allowing new discounts to be added without touching the old code.
/ Step 1: Create an interface for discounts
public interface Discount {
double calculate(double price);
}
// Step 2: Create classes for each type of discount
public class RegularDiscount implements Discount {
@Override
public double calculate(double price) {
return price * 0.1; // 10% discount for regular customers
}
}
public class PremiumDiscount implements Discount {
@Override
public double calculate(double price) {
return price * 0.2; // 20% discount for premium customers
}
}
// Step 3: Use DiscountCalculator to apply any discount
public class DiscountCalculator {
public void applyDiscount(Discount discount, double price) {
System.out.println("Discounted Price: " + discount.calculate(price));
}
}
What’s the improvement?
If you want to add a HolidayDiscount, you just create a new class that implements the Discount interface.
You don’t need to change the existing code! You can extend the functionality by adding new discount types without modifying the DiscountCalculator class or existing discount types.
Here’s how you can add a new discount easily:
public class HolidayDiscount implements Discount {
@Override
public double calculate(double price) {
return price * 0.3; // 30% discount for holidays
}
}
Now you can apply any type of discount, like this:
public class Main {
public static void main(String[] args) {
DiscountCalculator calculator = new DiscountCalculator();
Discount regularDiscount = new RegularDiscount();
Discount premiumDiscount = new PremiumDiscount();
Discount holidayDiscount = new HolidayDiscount();
calculator.applyDiscount(regularDiscount, 100); // Regular Discount
calculator.applyDiscount(premiumDiscount, 100); // Premium Discount
calculator.applyDiscount(holidayDiscount, 100); // Holiday Discount
}
}
Real-world Example:
Think about mobile apps. When you download a new app, like a weather app or a game, your phone doesn’t need to change its core system. The phone’s operating system is closed for modification but open for extension because you can add new apps without affecting how the phone works.
Summary:
Open for extension: You can add new things (like new discounts or games).
Closed for modification: You don’t have to change the old stuff when adding something new.
By following the Open/Closed Principle, programmers make it easier to add new features without breaking or changing the old code, keeping the system stable and flexible.
Think and apply
|
The Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) is one of the SOLID principles in programming, and it’s all about making sure that things in your code work properly when you swap them out for something else.
Imagine a Toy Box:
Think of a toy box where you have different kinds of toys. You have a toy robot, a toy car, and a toy airplane. All these toys are different, but they share something important: they all work with the same set of toy batteries.
What is LSP?
The Liskov Substitution Principle says that if you have a toy robot and you know it works with the toy batteries, then any other toy that you put in the toy box (like the toy car or toy airplane) should also work with the same batteries without causing problems.
In other words, you should be able to swap the toy robot with the toy car or toy airplane, and they should all work the same way with the toy batteries.
Why is LSP Important?
LSP helps make sure that different parts of a program or system can be swapped out or replaced with other parts without breaking things. It ensures that new parts follow the same rules as the old parts, so everything continues to work smoothly.
Summary:
The Liskov Substitution Principle is like making sure all toys in your toy box work with the same batteries. It means that if you swap one toy for another, everything should still work perfectly. This principle helps ensure that new parts of a system can replace old parts without breaking anything, keeping everything running smoothly!
Idea: Subtypes must be substitutable for their base types.
Subtypes must be substitutable for their base types without altering the correctness of the program.
Example: If you have a base class representing shapes, any subclass (like a specific type of shape) should be able to replace the base class without causing issues.
In simpler terms, the LSP implies that objects of derived classes should be able to replace objects of the base class without affecting the correctness of the program.
In other words, if a class is designed to be a subtype (a derived or child class) of another class (the base or parent class), it should conform to the contract or behavior of the base class in a way that it can be used interchangeably.
The key ideas behind the LSP include
Behavioral Compatibility: Subtypes should not change or violate the behavioral expectations of the base type. This means that they should provide at least the same interface as the base type, and their methods should adhere to the same expectations.
Inheritance Is-a Relationship: When you create a subtype, it should genuinely represent an "is-a" relationship with the base type. In other words, a derived class should be a more specialized version of the base class.
Polymorphism: The LSP enables polymorphism, allowing objects of different classes to be treated uniformly through their common base type. This simplifies code and promotes flexibility and reuse.
Robustness: Adhering to the LSP contributes to the robustness of the software because it ensures that derived classes won't introduce unexpected behavior or errors when used in place of their base classes.
Failure to adhere to the Liskov Substitution Principle can lead to incorrect program behavior, making the software more error-prone and harder to maintain. In practice, it's crucial to carefully design and implement inheritance hierarchies to ensure that the LSP is satisfied, and derived classes are true substitutes for their base classes, providing a consistent and predictable experience when using polymorphism.
Scenario:
We have a base class Bird and subclasses Sparrow and Penguin. According to LSP, a Penguin should not break the functionality expected from a Bird.
Example Without LSP Adherence
In this example, the Penguin class does not adhere to LSP because it introduces behavior that breaks the expectations set by the Bird class.
Without LSP:
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
public class Sparrow extends Bird {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
public class Main {
public static void main(String[] args) {
Bird sparrow = new Sparrow();
Bird penguin = new Penguin();
makeBirdFly(sparrow); // This works
makeBirdFly(penguin); // This will throw an exception
}
public static void makeBirdFly(Bird bird) {
bird.fly(); // Expect this to work for all Birds
}
}
Example 2:
You have a class Car and its subclass, its violating LSP
Solution:
Bad Design:
The Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) is one of the SOLID principles in programming. It’s all about making sure that classes don’t have to depend on things they don’t use. Here’s a simple way to understand it:
The Toy Example:
Imagine you have a toy box where you can put different kinds of toys. You have a big toy box with a lot of features, but you only need some of those features for certain toys.
The Problem:
Suppose the big toy box has a lot of buttons and controls for different kinds of toys—like making noise, flashing lights, and moving. But if you only have a simple toy car that only needs to move, you don’t want to deal with all those extra buttons and controls that you don’t need.
What ISP Says:
The Interface Segregation Principle says that you should split the big toy box into smaller toy boxes with just the features that each type of toy needs. This way, a toy car only gets a box with controls for moving, while a toy robot might get a box with controls for noise, lights, and movement.
Example with Toy Boxes:
Old Way (Without ISP): You have one huge toy box with all kinds of features. Whether you have a toy car or a toy robot, you have to deal with all features, even if you don’t need them.
New Way (With ISP): You have separate toy boxes:
Toy Car Box: Has controls for moving.
Toy Robot Box: Has controls for moving, making noise, and flashing lights.
Why It’s Good:
By following ISP, each toy gets just what it needs and nothing more. This makes it simpler to use, and you don’t have to worry about features that don’t apply to your toy.
Summary:
The Interface Segregation Principle means you create smaller, specialized interfaces (or toy boxes) so that each class (or toy) only depends on what it really needs. This makes things easier to understand and work with, and avoids extra complexity!
Idea: A class should not be forced to implement interfaces it does not use.
Example: If an interface has multiple methods and a class only needs to use a few of them, it should not be obligated to implement the unnecessary methods.
Example: Think of a machine interface. If you have a simple machine that only starts and stops, it shouldn't be forced to implement a method for maintenance. Different machines can implement different interfaces based on their capabilities.
Explanation
It is one of the SOLID principles of object-oriented design. It emphasizes that classes should not be forced to implement interfaces that contain methods they don't need. In other words, clients (classes or modules that use interfaces) should not be dependent on methods they don't use. Instead, interfaces should be designed to be small, focused, and tailored to specific use cases.
Here's a detailed explanation of the Interface Segregation Principle in the context of Java.
Problem Without ISP
Suppose you're designing an interface for a generic machine.
The interface includes methods like start(), stop(), and performMaintenance().
However, not all machines require maintenance. You create a class for a basic machine that doesn't need to perform maintenance.
interface Machine {
void start();
void stop();
void performMaintenance();
}
class BasicMachine implements Machine {
public void start() {
// Implementation for starting the machine
}
public void stop() {
// Implementation for stopping the machine
}
public void performMaintenance() {
// This method is not needed for BasicMachine, but it's required to implement the Machine interface.
// The implementation is empty.
}
}
In this example, the BasicMachine class is forced to implement the performMaintenance() method, even though it doesn't need it. This violates the ISP because the class is being forced to depend on methods it doesn't use.
Applying ISP in Java
To adhere to the Interface Segregation Principle, you should design interfaces that are specific to their implementing classes. In this case, you can create a separate interface for machines that require maintenance:
interface Startable {
void start();
void stop();
}
interface Maintainable {
void performMaintenance();
}
class BasicMachine implements Startable {
public void start() {
// Implementation for starting the machine
}
public void stop() {
// Implementation for stopping the machine
}
}
class MaintenanceMachine implements Startable, Maintainable {
public void start() {
// Implementation for starting the machine
}
public void stop() {
// Implementation for stopping the machine
}
public void performMaintenance() {
// Implementation for performing maintenance
}
}
In this revised design
The Startable interface includes methods related to starting and stopping machines.
The Maintainable interface includes the performMaintenance() method, which is only relevant to machines that require maintenance.
The BasicMachine class implements the Startable interface because it only needs to start and stop, while the MaintenanceMachine class implements both the Startable and Maintainable interfaces, as it requires both functionalities.
By applying ISP, you ensure that each class or interface has a specific, well-defined purpose, and clients are only forced to depend on what they need. This promotes cleaner, more maintainable code and reduces the risk of unnecessary dependencies and empty method implementations.
OR you can make the method default so that it's NOT mandatory for clients to provide implementation.
If you are using java 9+ then you can use the default method in the interface.
NOTE: But having default method is not the best way
public interface Machine {
void start();
void stop();
default void performMaintenance(){
//default implementation
}
}
public class BasicMachine implements Machine{
@Override
public void start() {
//provide implementation to start
}
@Override
public void stop() {
//provide implementation to stop
}
}
public class AdvancedMachine implements Machine{
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public void performMaintenance() {
//Machine.super.performMaintenance();
//Add your implementation
}
}
Dependency Inversion Principle (DIP)
The Magic Remote Control:
Imagine you have a magic remote control that can control different things, like your TV, game console, and music player.
The Problem:
If the remote control is built to only work with a specific brand of TV or game console, you’d have to get a new remote control every time you change your brand or device. This is a hassle and not very flexible.
What DIP Says:
The Dependency Inversion Principle says that instead of building the remote control to work with specific devices, you should design it to work with any device that follows a certain set of rules or “instructions.”
How It Works:
High-Level Module (Magic Remote Control): Instead of depending on specific devices, it depends on general instructions for how to control any device.
Low-Level Module (Devices like TV, Game Console): These devices follow the same set of instructions so the remote control can work with any of them.
Example with Devices:
Old Way (Without DIP): The remote control is designed specifically for one brand of TV, so if you switch brands, you need a new remote.
New Way (With DIP): The remote control is designed to follow a set of basic instructions, like “turn on,” “turn off,” and “volume up.” Any device that follows these instructions can be controlled by the remote, no matter the brand.
Why It’s Good:
By following DIP, your remote control can easily work with any device that follows the same set of instructions. You don’t have to change the remote every time you get a new device. This makes the system more flexible and easier to maintain.
Summary:
The Dependency Inversion Principle is about designing things so that high-level parts (like the remote control) don’t depend on low-level parts (like specific devices) directly. Instead, they both depend on common instructions or rules. This makes everything more flexible and easier to work with!
Let's be more specific:
Idea: High-level modules should not depend on low-level modules; both should depend on abstractions.
Example: Instead of a higher-level module relying directly on a lower-level module, both should depend on an abstract interface. This promotes flexibility and makes the system less tightly coupled.
Example: Consider a light switch. It depends on electricity (low-level), but the switch and the bulb both depend on the abstraction of a "Switchable" interface. This way, you can change the bulb or the switch independently without affecting each other.
The Dependency Inversion Principle (DIP) is one of the SOLID principles of object-oriented design. It emphasizes the need for high-level modules (classes or components) to not depend on low-level modules, but rather both should depend on abstractions (interfaces or abstract classes). Additionally, it suggests that details (low-level implementation) should depend on abstractions, not the other way around.
// Low-level module
class EmailService {
public void sendEmail(String message) {
// Logic to send an email
System.out.println("Email: " + message);
}
}
// High-level module directly depends on low-level module
class NotificationService {
private EmailService emailService = new EmailService();
public void sendNotification(String message) {
emailService.sendEmail(message);
}
}
// Client code
public class Client {
public static void main(String[] args) {
NotificationService notificationService = new NotificationService();
notificationService.sendNotification("Hello, world!");
}
}
In this example, NotificationService directly depends on EmailService, violating the Dependency Inversion Principle. Now, let's refactor it to adhere to DIP:
// Abstraction for notification service
interface Notification {
void sendNotification(String message);
}
// Low-level module implements the abstraction
class EmailService implements Notification {
@Override
public void sendNotification(String message) {
// Logic to send an email
System.out.println("Email: " + message);
}
}
// High-level module depends on the abstraction
class NotificationService {
private Notification notification;
public NotificationService(Notification notification) {
this.notification = notification;
}
public void sendNotification(String message) {
notification.sendNotification(message);
}
}
// Client code
public class Client {
public static void main(String[] args) {
Notification emailService = new EmailService();
NotificationService notificationService = new NotificationService(emailService);
notificationService.sendNotification("Hello, world!");
}
}
Now, NotificationService depends on the Notification interface rather than the concrete EmailService. This adheres to the Dependency Inversion Principle, allowing for more flexibility. If you want to introduce a new notification method (e.g., SMS), you can create a new class implementing the Notification interface without modifying the existing code.
By applying DIP, the high-level module (NotificationService) is no longer tightly coupled to the low-level module (EmailService), and both depend on abstractions, providing a more flexible and maintainable design.
In other words, the Dependency Inversion Principle encourages loose coupling between different parts of a software system. This loose coupling makes the system more flexible, maintainable, and adaptable to changes.
Software dependency, or coupling, is a common problem that needs to be addressed in system design. Coupling defines how much reliance there is between different components of your software. High coupling indicates a high degree of reliance. Low coupling indicates low dependency. Dependency determines how easily changes can be made to a system.
If parts of a system are dependent on each other, then substituting a class or resource for another is not easily done, or even impossible.
The dependency inversion principle addresses dependency, to help make systems more robust and flexible. The principle states that high-level modules should depend on high-level generalizations, and not on low-level details. This keeps client classes independent of low-level functionality.
This suggests that client classes should depend on an interface or abstract class, rather than a concrete resource. Further, concrete resources should have their behaviors generalized into an interface or abstract class. Interfaces and abstract classes are high-level resources. They define a general set of behaviors. Concrete classes are low-level resources. They provide the implementation for behaviors.