The SOLID principles are a set of five design principles for writing maintainable and scalable software. Each principle addresses a specific aspect of object-oriented design. Let's explore these principles with real-life examples:
Single Responsibility Principle (SRP):
The SRP states that a class should have only one reason to change.
In real life, think of a chef in a restaurant. Just as a chef specializes in cooking food and doesn't handle customer service or accounting, a class should have a single responsibility.
For example, a Customer class should be responsible for storing customer information but not for processing payments.
Open-Closed Principle (OCP):
The OCP encourages classes to be open for extension but closed for modification.
A real-life example is a software plugin system.
You can add new plugins to extend functionality without altering the core application's code. This maintains the stability of the original code.
Mobile Apps and Plugins:
- like a photo editing app. The app is a closed system with various filters and editing tools. To follow the OCP, it should be designed in a way that allows users to extend its functionality without modifying the core code.
- In this scenario:The app is "closed" because its core code (e.g., the image processing algorithms) remains unchanged.
- The app is "open" for extension because it allows users to create and add their custom filters, effects, or tools through plugins or extensions.
- So, users can enhance the app's functionality by creating and installing their plugins or extensions, thereby extending the capabilities of the app without altering its original code
Electric adapter example:
An electric adapter is a physical example of the Open-Closed Principle. The adapter itself is closed for modification; you cannot change its design to accommodate different plugs.
However, it is open for extension through additional adapters that allow you to connect various plug types to the same outlet.
This design allows for flexibility and adaptability without modifying the original adapter.
Liskov Substitution Principle (LSP):
The LSP suggests that objects of derived classes should be able to replace objects of the base class without affecting the program's correctness.
Consider a real-world example of vehicles. If a program expects a "Vehicle" object, you should be able to use a "Car" or "Bicycle" object interchangeably without issues.
Consider the concept of an animal hierarchy within a zoo. You have a base class called "Animal," which represents the common characteristics and behaviors of all animals in the zoo. There are derived classes such as "Mammal," "Bird," "Reptile," and "Fish," which inherit from the base class.
Now, let's say you have a program that simulates the behavior of animals in the zoo. You've defined certain methods and behaviors common to all animals, like "eat," "sleep," and "makeSound." According to the Liskov Substitution Principle, you should be able to substitute any specific animal for a generic "Animal" without affecting the program's correctness.
For example:A "Lion" (a derived class of "Mammal") can be substituted for an "Animal" in the program, and it can eat, sleep, and make sounds just like any other animal.
A "Penguin" (a derived class of "Bird") should also be substitutable for an "Animal" and exhibit the same basic behaviors.
Animal Kingdom and Zoos:
Consider the concept of an animal hierarchy within a zoo. You have a base class called "Animal," which represents the common characteristics and behaviors of all animals in the zoo. There are derived classes such as "Mammal," "Bird," "Reptile," and "Fish," which inherit from the base class.
Now, let's say you have a program that simulates the behavior of animals in the zoo. You've defined certain methods and behaviors common to all animals, like "eat," "sleep," and "makeSound." According to the Liskov Substitution Principle, you should be able to substitute any specific animal for a generic "Animal" without affecting the program's correctness.
For example:A "Lion" (a derived class of "Mammal") can be substituted for an "Animal" in the program, and it can eat, sleep, and make sounds just like any other animal.
A "Penguin" (a derived class of "Bird") should also be substitutable for an "Animal" and exhibit the same basic behaviors.
class Animal {
public void eat() {
System.out.println("The animal is eating.");
}
public void sleep() {
System.out.println("The animal is sleeping.");
}
public void makeSound() {
System.out.println("The animal makes a sound.");
}
}
class Lion extends Animal {
@Override
public void makeSound() {
System.out.println("The lion roars!");
}
}
class Penguin extends Animal {
@Override
public void makeSound() {
System.out.println("The penguin squawks.");
}
}
public class Zoo {
public static void main(String[] args) {
Animal genericAnimal = new Animal();
Animal lionAnimal = new Lion();
Animal penguinAnimal = new Penguin();
genericAnimal.eat();
genericAnimal.sleep();
genericAnimal.makeSound();
lionAnimal.eat();
lionAnimal.sleep();
lionAnimal.makeSound();
penguinAnimal.eat();
penguinAnimal.sleep();
penguinAnimal.makeSound();
}
}
Interface Segregation Principle (ISP):
In real life, think of a remote control. If a remote control has many buttons that control various devices, each device should only respond to the buttons relevant to it. For example, a TV remote should not have buttons for adjusting oven temperature.
To follow the ISP, you can define separate interfaces for each type of user, ensuring that no client (code that uses these interfaces) is forced to depend on methods it does not need. For instance:
The Administrator interface includes methods for viewing content, editing content, and managing users, which are relevant to administrators.
The SuperAdministrator interface extends the Administrator interface and includes an additional method for performing system maintenance.
By adhering to the ISP, you ensure that clients, like different user roles in the application, are provided with interfaces containing only the methods relevant to their needs. This way, you avoid forcing unnecessary methods on clients, which enhances the maintainability and clarity of your code.
User Permissions and Interfaces:
Imagine a system that manages user permissions for a software application. In this system, there are different types of users: regular users, administrators, and super administrators. These users have varying levels of access and capabilities within the application.To follow the ISP, you can define separate interfaces for each type of user, ensuring that no client (code that uses these interfaces) is forced to depend on methods it does not need. For instance:
interface RegularUser { void viewContent(); } interface Administrator { void viewContent(); void editContent(); void manageUsers(); } interface SuperAdministrator { void viewContent(); void editContent(); void manageUsers(); void performSystemMaintenance(); }
In this example:The RegularUser interface has only the method needed for regular users to view content.The Administrator interface includes methods for viewing content, editing content, and managing users, which are relevant to administrators.
The SuperAdministrator interface extends the Administrator interface and includes an additional method for performing system maintenance.
By adhering to the ISP, you ensure that clients, like different user roles in the application, are provided with interfaces containing only the methods relevant to their needs. This way, you avoid forcing unnecessary methods on clients, which enhances the maintainability and clarity of your code.
Dependency Inversion Principle (DIP):
The DIP promotes high-level modules to depend on abstractions, not on concrete implementations.
// Abstraction
interface Device {
void turnOn();
void turnOff();
}
// Concrete Implementations
class Light implements Device {
@Override
public void turnOn() {
// Logic to turn on the light
}
@Override
public void turnOff() {
// Logic to turn off the light
}
}
class Fan implements Device {
@Override
public void turnOn() {
// Logic to turn on the fan
}
@Override
public void turnOff() {
// Logic to turn off the fan
}
}
// High-level module depending on abstraction
class DeviceController {
private final Device device;
public DeviceController(Device device) {
this.device = device;
}
public void operateDevice() {
// Common logic to operate the device
device.turnOn();
// Additional logic if needed
device.turnOff();
}
}
In this example:
The Device interface represents the abstraction, defining common methods like turnOn and turnOff.
The Light and Fan classes are concrete implementations of the Device interface.
The Light and Fan classes are concrete implementations of the Device interface.
The DeviceController class is the high-level module that operates a device. It depends on the Device abstraction.
By adhering to the Dependency Inversion Principle, the DeviceController class is not concerned with the specific details of how each device turns on or off. It relies on the abstraction, allowing for flexibility. If a new type of device is introduced, you can create a new class implementing the Device interface without modifying the DeviceController class. This promotes a more modular and maintainable codebase.
No comments:
Post a Comment