SOLID Design Principles: Implementing Flexible and Maintainable Code

Chanuka Dinuwan
6 min readFeb 19, 2024

--

Software development field is evolving and the size of our projects has become more complicated so that clean, maintainable, and flexible code has been required. Principles SOLID are required in this situation. These design principles were proposed by Robert C. Martin and they provide a solid foundation for code that is understandable and can support modification in the future.

This article will look at each SOLID principle in depth and show its importance with simple examples. You can develop robust systems that will last long by integrating these principles into your development process.

The SOLID Principles:

  1. Single Responsibility Principle (SRP): There should be just one reason for a class to be modified. This implies keeping clear of classes that extend overloaded with functionalities, as each one may necessitate changes to the class itself. Consider a class that manages data persistence in addition to video playback. The video playback functions may potentially unintentionally be impacted if a database schema change requires modifying the persistence process. Following the SRP helps you design classes with clear objectives that are simpler to understand, modify, and test.

Ex:

public class VideoManager {
public void playVideo(String videoPath) {
// Code to play video
}

public void saveVideoData(String videoData) {
// Code to save video data
}

public String loadVideoData(String videoId) {
// Code to load video data
return null;
}
}

To adhere to the SRP, we should split this class into two separate classes, each with its own responsibility.

public class VideoPlayer {
public void playVideo(String videoPath) {
// Code to play video
}
}
public class VideoDataPersistence {
public void saveVideoData(String videoData) {
// Code to save video data
}

public String loadVideoData(String videoId) {
// Code to load video data
return null;
}
}

Now, each class has a single responsibility. VideoPlayer is responsible for video playback, and VideoDataPersistence is responsible for data persistence. This way, changes to the video playback functionality won’t affect data persistence, and vice versa.

2. Open/Closed Principle (OCP): A class has to be designed in such a way that it has only one reason for a change. This implies that we should not take classes that become oversubscribed with multiple functionalities, which could require changes in each of the functionalities. Imagine a class that is responsible for media playback and data storage. Database schema changes can often result in altering the persistence logic, which in turn can unintentionally affect the video playback functionalities. Through the implementation of the SRP, your classes are classes with a purpose and focused, which makes them simpler to understand, modify, and test.

Let’s consider the same VideoManager class from the previous example. This class is responsible for both video playback and data persistence, which violates the Single Responsibility Principle (SRP). But it also violates the Open/Closed Principle (OCP) because if we want to add a new functionality (like video editing), we would have to modify the VideoManager class itself.

public class VideoManager {
public void playVideo(String videoPath) {
// Code to play video
}

public void saveVideoData(String videoData) {
// Code to save video data
}

public String loadVideoData(String videoId) {
// Code to load video data
return null;
}

public void editVideo(String videoPath) {
// Code to edit video
}
}

To adhere to the OCP, we should create a new class for each functionality that extends a base class or implements an interface.

Here’s how we could refactor the VideoManager class:

public interface VideoOperations {
void execute(String videoPath);
}
public class VideoPlayer implements VideoOperations {
public void execute(String videoPath) {
// Code to play video
}
}
public class VideoDataPersistence implements VideoOperations {
public void execute(String videoData) {
// Code to save video data
}

public String loadVideoData(String videoId) {
// Code to load video data
return null;
}
}
public class VideoEditor implements VideoOperations {
public void execute(String videoPath) {
// Code to edit video
}
}

Now, each class has a single responsibility and adheres to the OCP. If we want to add a new functionality, we just need to create a new class that implements the VideoOperations interface. This way, we don’t need to modify the existing classes.

3. Liskov Substitution Principle (LSP): Subtypes have to be replaceable by their base types. This principle makes it possible for subtypes (specializations of a base class) to be substituted with the base class without causing unintended behavior. For example, imagine a class Video with a play() method. If PremiumVideo subclass overrides play() but adds unintended ads will be a violation of LSP. Through this rule you ensure that the subtypes satisfy the contract specified by the base class, which promotes code reuse and reliability.

public class Video {
public void play() {
// Code to play video
}
}
public class PremiumVideo extends Video {
@Override
public void play() {
// Code to play video
// No additional behavior like ads should be added here
}
}

In this example, PremiumVideo is a subclass of Video. According to LSP, we should be able to use an instance of PremiumVideo wherever we use an instance of Video, without causing any unintended behavior. This means that the play() method in PremiumVideo should not add any additional behavior like ads. If it does, it would violate LSP.

4. Interface Segregation Principle (ISP): There are many specific interfaces that are better than a single general interface. This principle proposes using small and narrow interfaces rather than monolithic interfaces that include a large number of functions. Think about a single console for video operations including video playback, video editing, and data management. Such a user interface would be both bulky and error-prone. This is achieved by identifying the interfaces as separate and smaller entities (PlaybackInterface, EditInterface, DataManagementInterface) which in turn results in code clarity, reduced coupling, and easy to implement and consume interfaces.

Let’s consider a `VideoOperations` interface that includes methods for video playback, video editing, and data management. This would be a violation of ISP because a class that only needs to play videos would still need to implement (or be aware of) methods for video editing and data management.

public interface VideoOperations {
void play(String videoPath);
void edit(String videoPath);
void saveData(String videoData);
String loadData(String videoId);
}

To adhere to the ISP, we should split this interface into three separate interfaces, each with its own responsibility.


public interface VideoPlayer {
void play(String videoPath);
}

public interface VideoEditor {
void edit(String videoPath);
}

public interface VideoDataManagement {
void saveData(String videoData);
String loadData(String videoId);
}

Now, each interface has a single responsibility. A class that only needs to play videos can implement the `VideoPlayer` interface without being aware of the `VideoEditor` and `VideoDataManagement` interfaces. This results in code that is easier to understand, modify, and test.

5. Dependency Inversion Principle (DIP): Depend on abstractions, not on concreteness. This principle implies that you should use interfaces and abstractions instead of concrete implementations in your code. This enables you to switch easily between different implementations without impacting the other parts of your code. For example, you could create an interface for database operations and inject different concrete implementations to meet your needs, instead of depending directly on a specific database implementation class. It enhances loose coupling thus making the code more adaptable and testable.

Let’s consider a `CourseService` class that directly depends on a `CourseRepository` class for database operations. This would be a violation of DIP because `CourseService` is a high-level module that is depending on a low-level module `CourseRepository`.


public class CourseRepository {
public void saveCourse(Course course) {
// Code to save course
}
}
public class CourseService {
private CourseRepository courseRepository;
public CourseService() {
this.courseRepository = new CourseRepository();
}
public void addCourse(Course course) {
courseRepository.saveCourse(course);
}
}

To adhere to the DIP, we should create an interface for the database operations and have `CourseService` depend on this interface instead of the concrete `CourseRepository` class. We can then inject different implementations of this interface into `CourseService` as needed.


public interface CourseRepository {
void saveCourse(Course course);
}

public class CourseRepositoryImpl implements CourseRepository {
public void saveCourse(Course course) {
// Code to save course
}
}

public class CourseService {
private CourseRepository courseRepository;

public CourseService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}

public void addCourse(Course course) {
courseRepository.saveCourse(course);
}
}

Now, `CourseService` depends on the abstraction `CourseRepository` and not on the concrete `CourseRepositoryImpl` class. This allows us to easily switch between different implementations of `CourseRepository` without impacting `CourseService`. This results in code that is more flexible, adaptable, and testable.

The SOLID principles offer helpful guidance on developing flexible, clean, and maintainable code. Following these principles can help you develop software systems that are more flexible to future requirements and changes, as well as simpler to understand and modify. Adopting the SOLID principles is an investment in the long-term stability and maintainability of your codebase, regardless of your level of experience as a developer. Thus, begin applying these ideas to your coding procedures right now and enjoy the advantages of creating high-quality software!

--

--