S.O.L.I.D is Basic
If you’re planning on starting your journey into the art of coding, new to software development, or even a seasoned professional looking for a refresher, then this blog is for you!
Before we get to it, as a software engineer/professional working in the IT industry, we should always ask!
Why should I learn this? What is it for? Is it necessary? How can I apply the concept?
To answer these question, we must first understand what technical debt is all about. Technical debt is referred to as code smells. It’s basically code that could’ve been written better. In other words, it’s simply refers to the additional time and effort that a dev/team needs to spend on a project due to shortcuts taken or subpar\suboptimal decisions made during the development process.
Making SOLID your foundation will equip you to avoid these kinds of mistakes and will enable you to focus on the quality of your code.
Now let’s get to it..
Single Responsibility Principle
Do one thing and do it well
Now imagine for a second the experience of opening your christmas present that was wrapped on a thick material using a swiss knife. How was it? It’s poor right..
The experience you imagined applies to code. If you have something that does something like the swiss knife, chances are you’re not going to do them optimally.
If you’re planning on writing a method that will do so much, just simply decompose the process and have multiple smaller methods. Remember, there will always come a time where requirement will change. If the time comes, you will have the responsibility on updating the code. On applying this concept, you have the ease of readibility and maintainability.
Rule of thumb from Robert C. Martin aka uncle bob: “There should never be more than one reason a class to change.”
In order for a function to do “one thing” it must be so small that no meaningful function can be extracted from it. Any function from which another can be extracted clearly does more than “one thing”.
Let’s take a look on how it works.
In this code, the FileSystem class has three responsibilities: reading files, writing files, and sending files to a server. It clearly violates the principle.
Now let’s apply the principle.
The responsibilities of reading files, writing files, and sending files to a server are now separated into two separate classes, FileSystem and FileSender, respectively. This makes the code easier to maintain and test, as changes to one responsibility don’t affect the other. Additionally, each class now only has one responsibility, which makes the code easier to understand.
Important Note: Abstraction is the elimination of the irrelevant and the amplification of the essential.
Open-Closed Principle
Achieve Modularity and Scalability
Now imagine for a second you have a small fridge that can store 10 drinks. You now have a party and need to store 20 drinks. Do you take the fridge apart and modify it to make it larger? This would involve making changes to the original design and a risk that the fridge may work once it’s done. Or do you simply just borrow/purchase a cooler/storage container and add ice?
The principle has three points:
- A class should be open for extensibility but closed for modification.
- You are not allowed to modify a class once it is being used by other clients
- Change in a class that is being used by others will cause a ripple effect of change.
This means, you should design your code in a way that allows you to add new functionality without having to change existing code. By applying this concept, it will help you achieve maintainability, flexibility, and reusability in your code. You now have the ease to modify and extend your code over time as new requirement arise, without having to make major changes to existing code. This can save you a lot of time and effort in the long run, and make your code easier for others to understand and work with.
Let’s take a look how it works.
the FileSystem class has a ReadFile method and a WriteFile method, both of which take a FileType enum parameter that determines the type of file being read or written. This is a violation, as adding a new file type requires modifying the existing code.
Now let’s apply the principle.
The TextFile and BinaryFile classes both implement the IFile interface, which defines the Read and Write methods. The FileSystem class now has a constructor that takes an IFile parameter, and it delegates the file operations to the IFile implementation. This means that adding a new file type can be done by creating a new class that implements the IFile interface, without modifying the existing code. This follows the principle, as the code is closed for modification but open for extension.
Important Note: Composition should be preferred over inheritance.
Liskov Substitution Principle
Maximize Software Interoperability
Now imagine for a second you have a base class that represents a certain type of object, and then you have subclasses that inherit from that base class, each representing a specific variation of that object. The principle says that you should be able to substitute a subclass for the base class, without causing any problems.
In other words, if you have a method that accepts an object of the base class, it should work the same way, regardless of whether you pass in an object of the base class or one of its subclasses. This allows you to make changes to your codebase over time, knowing that you won’t break existing functionality.
Let’s take a look how it works.
The File class has a virtual Open method, and the ImageFile class overrides this method and throws an exception when it is called. However, if we have a reference to a File object and it is actually an instance of ImageFile, then this will cause a runtime exception. This violates the principle because ImageFile is not substitutable for File.
Now let’s apply the principle.
The File class is now abstract, and the Open method is abstract as well. The ImageFile and TextFile classes both inherit from File and provide their own implementations of the Open method. This allows us to have a reference to a File object and know that the Open method is always available, even if the actual instance is an instance of ImageFile. This follows the principle because ImageFile and TextFile are substitutable for File.
Important Note: Subclass objects must behave the same as base class objects.
Interface Segregation Principle
Keep Interfaces Focused and Specific
Now imagine you’ve been tasked with creating a file system. You need to create classes to represent the different types of files, such as audio files, image files, and text files. Each type of file needs to have its own set of actions, such as playing an audio file, viewing an image file, or reading a text file.
Now, let’s say you’ve created an interface called “IFile” that defines all the actions that any file can perform, such as opening, closing, and deleting. But, not all types of files need to perform all these actions. For example, an image file doesn’t need to be closed or an audio file doesn’t need to be deleted.
This is the principle comes into play. You should create separate interfaces for each specific set of actions that’s different types of files need to perform. In this case, you can create an interface for audio files called “IAudioFile”, an interface for image files called “IImageFile”, and an interface for text files called “ITextFile”. Each of these interfaces will only contain the actions that are specific to the type of file they represent.
Let’s take a look how it works.
AudioFile and ImageFile classes are forced to implement all the methods defined in the IFile interface, even if they don’t actually use all of them. This leads to a lot of unnecessary code and makes it harder to maintain the file system.
Let’s take a look how it works.
We now have split the IFile interface into two smaller, more specific interfaces: IReadableFile and IWritableFile. The AudtioFile and ImageFile classes now only need to implement the methods that they actually need, resulting in a more modular and maintanable file system.
Important Note: Keep interfaces lean and focused
Dependency Inversion Principle
Elavate Abstraction for Loose Coupling
Now imagine for a second you are designing a file system that needs to access various storage devices such as hard drives, flash drives, and cloud storage. In a traditional approach, you might have the file system dependent on concrete storage devices, meaning that if you wanted to add a new storage device, you would have to make changes directly to the file system code.
The principle dictates that you should design the file system to depend on abstractions, such as an interface for storage devices. This way, if you wanted to add a new storage device, you could simply create a new implementation of the storage interface, without having to make any changes to the file system code.
This helps to decouple the file system code from the concrete storage devices, making it more flexible and scalable in the long run.
Let’s take a look how it works.
The FileSystem class directly creates an instance of the File class and calls its methods to perform various file operations. This voilates the principle because it has a high-level module FileSystem that is dependent on a low-level module File. This makes the code tightly coupled and harder to maintain and test.
Now let’s apply the principle.
FileSystem class depends on the abstract IStorageDevice interface, rather than on concrete implementations such as HardDrive and FlashDrive. This makes it more flexible and scalable, as adding a new storage device simply requires implementing the IStorageDevice interface.
The principle is achieved by inverting the direction of the dependencies, so that high-level components dependend on abstractions, while low-level components depend on concrete implementations.
Important Note: High-level modules should not depend on low-level modules. Both should depend on abstractions