Credit: Bayu F. Asmoro
If you have mastered and used OOP then you likewise need to learn SOLID because they are two different things.
OOP can be defined as a programming paradigm that relies on the concepts of classes and objects. This paradigm is used to structure software programs into straightforward and reusable blueprint of code or code classes. These classes are subsequently used to create individual objects in programming.
Whereas SOLID is a principle. SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. The principles are a subset of many principles promoted by American software engineer and instructor Robert C. Martin (a.k.a Uncle Bob) first introduced in his 2000 paper Design Principles and Design Patterns.
The main goal of solid principles is to reduce dependencies so engineers can change one area of software without affecting others. By following solid principles, the code created is easy to understand, maintain, and extend. Ultimately, using these design principles makes it easier for software engineers to avoid issues and to build adaptive, effective, and agile software.
The main goal of solid principles is to reduce dependencies so engineers can change one area of software without affecting others. By following solid principles, the code created is easy to understand, maintain, and extend. Ultimately, using these design principles makes it easier for software engineers to avoid issues and to build adaptive, effective, and agile software.
In this article, the principle will be introduced individually to understand solid with case studies using the Dart programming language.
S — Single Responsibility
A class should have one and only one reason to change, meaning that a class should have only one job.
This principle is used to regulate the responsibility of an entity that is in a project in this case is a module / class to meet the needs of actors. If a class has several functionalities that are not related to each other, the functionality needs to be divided into two different classes, so that one class only handles one responsibility.
Assuming that a Class has numerous responsibilities, it increases the chance of bugs since making changes to one of its responsibilities, could affect the others without you knowing.
For example, consider a case study on home services which is modeled into the following Service class code:
Based on the code above, the service class has many responsibilities. For example, to sweep, mop, cook even to escort the employer.
By applying the principles of SRP (Single Responsibility Pricinple), we can separate the responsibilities of each function into a new class. So, the result will be like this:
After being refactored, the division of responsibilities for each class has been separated based on the actor.
O — Open/Closed
Objects or entities should be open for extension but closed for modification.
Open to extension is a state when a system can be added with new required specifications. While closed for modification that’s when we want to add new specifications, we do not need to change or modify the existing system.
In general, the use of open/close rules is implemented by utilizing interfaces and abstract classes rather than using a concrete class. The purpose of using interfaces and abstract classes is to make it easily fixed after development without disturbing the inheriting class so when you create new functionality, simply create a new class and inherit the interface or abstraction.
Below is an example of a case study of calculating vehicle speed:
The above example changes the speed of the vehicle according to the given operation command. This is not good for OCP (Open Closed Principle) because this method does different things according action that is passed to getCurrentSpeed function. As new requests, actions will require the addition of new if-else conditions. This shows that the method is not closed to change.
We can improve this by abstracting the vehicle action out of the function. Let’s first create an Vehicle abstract class. This class abstractly does something. Then we derive 2 classes from Vehicle abstract class:
In this new code, we transform the if-else condition into a separate classes that extend from the Vehicle abstract class so if there is additional code, each class can perform its own function without affecting the others.
To get the speed of the vehicle when accelerating, we can simply call the vehicle acceleration class using the following code sample:
Now, final version of the function is independent based on behavior conditions, it is closed to changes. If we add new behaviors to the vehicle, we can create a new class and then extend the method from Vehicle abstract class without modifying the contents of the class.
L — Liskov Substitution
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
LSP (Liskov Substitution Principle) is a rule that applies to the inheritance hierarchy. This requires us to design our classes so that dependencies between clients can be substituted without the client knowing about the changes. Therefore, all SubClasses can at least run in the same way as their SuperClass.
Here’s a sample code of the scenario we’ll be using:
In the code example above we have an abstract class named Product which contains several abstract members. This class is inherited by another class, namely the Oil class. Currently, the class can run well according to its function.
Next we need a new product class. It’s spare part products. So, we just need to create a new class that inherits the Product class because the class is an abstraction of a Product class. However, in the Sparepart class, this does not work well because spare part do not have an expiration date.
To solve this case, we need to substitute the irrelevant function into its own abstraction class and inherit it in the relevant class. However, this change still makes the Product class the SuperClass of the current hierarchy. More or less the changes will be as follows:
By changing the code as above, we have fulfilled the existing rules. LSP is a principle that can improve the design of the system we are developing. So that dependencies between clients can be substituted without the client knowing the changes.
I — Interface Segregation
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
ISP (Interface Segregation Principle) aims to reduce the number of dependencies of a class on unneeded interface classes. In fact, classes have dependencies on other classes. The number of dependencies of functions on an interface class that can be accessed by that class should be optimized or reduced. When a class is created with a large number of functions and properties, other classes that depend on that class only need one or two functions from that class. Dependencies between classes will increase as the number of functions and properties of the class required.
To solve it. When we create a system, we must have created a class that has or implemented several public interfaces and some of these interfaces are also used and implemented by other classes in our system. The classes that we create sometimes only need a few functions that exist in the interface so that according to the rules of the interface segregation principle this is not good. But don’t worry, when the interface segregation principle is applied, each class will implement several smaller interface classes according to the functions required by these classes.
This means that interdependent classes can communicate using smaller interfaces, reducing dependency on unused functions and reducing coupling. Using a smaller interface will make it easier to implement, increase flexibility and also the possibility of being reused.
There is a case study between a square and a cube. Both belong to the type of wake field, but do they have the same formula? Consider an example of applying calculations to the following Square and Cube classes.
The square area is modeled as a Square class, while the cube is modeled as a Cube class. They are part of the ShapeInterface as follows:
Inside the ShapeInterface, there are two functions, including area and volume. When the Square class implements this interface, all the functions of the ShapeInterface will be overridden in the Square classes. However, there is something that is not quite right, namely there is a volume function, while in reality the square should not be able to calculate volume. How can we solve it?
The trick is to apply the ISP. We need to separate the volume function into another interface, the code will be like this:
By separating the interface into small parts, the uses and responsibilities of the interface will be easily understood by the developer. The goal is to produce a flexible design, by simply implementing certain interfaces rather than implementing one interface which consists of many more complex functions.
D — Dependency Inversion
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
The principle of Dependency Inversion is similar to the concept of layering in applications, where low-level modules are responsible for very detailed functions and high-level modules use low-level classes to accomplish larger tasks. This can be achieved by relying on an abstraction, when there are dependencies between classes such as interfaces, rather than direct references to other classes.
What are high-level modules and low-level modules? To make it easier to understand, we can categorize classes into a hierarchy. High-level modules are classes that deal with sets of functionality. At the highest hierarchy there are classes that implement business rules according to a predetermined design. Low-level modules are responsible for more detailed operations. At the lowest level allows this module to be responsible for writing information to the database or delivering messages to the operating system.
Let’s first consider the application of a case study between the Android OS and a smartphone, which is modeled into the AndroidOS and Smartphone class code as follows:
Based on the code above, there is an Android OS class which is a constructor to add a smartphone, in this case it is modeled as a Smartphone class. However, what if the case is on the same Android OS model, want to change the OS type. In this case want to replace it with the iPhone OS.
We cannot implement it, because the parameter of the Android OS class constructor is the Smartphone class. If we enter the iPhone OS class, the result will be an error.
One way to solve this problem is to apply the DIP (Dependency Inversion Principle). First, we first create the OSInterface as follows:
By applying the DIP, we can create a flexible system. Where dependencies or dependencies on source code only refer to abstractions not to concretions (a class). In the example above, it is illustrated in the form of an OSInterface that contains a launch method. So, after this principle is applied, the high level class does not work directly with the low level class, but uses the interface as an abstraction layer.
Summary
Up to this point, we have examined these five standards and featured their objectives. They are to assist you with making your code to be easily reusable, maintainable, scaleable and testable.
Thanks a million for reading the article. I hope you enjoy this topic as much as I wrote it.