A clear distinction between junior and senior developers is how well senior developers future-proof their programs; one way of achieving this is to use a well-known code or rules, like SOLID principles, which Robert C. Martin developed in his essay, "Design Principles and Design Patterns."
SOLID is an acronym for single responsibility, open-closed, Liskov substitution, interface segregation, and dependency inversion principle, and it's important to state that SOLID is mainly for OOP.
It's important to recall that this is an introduction; thus, I will use easy scenarios, and the examples shown are for learning purposes only and not for commercial use. Lastly, I'll use sudo code to make it reachable to most people.
Single Responsibility Principle
The single responsibility principle states, "A class should have only one reason to change." According to the previous state, each class is responsible for only one part during the software lifespan.
To kick off the article, I'll create a simple User class:
class User {
function profile()
function login()
function register()
function sendVerificationEmail()
}
As we can see, the class is responsible for more than one part, and my way of applying the principles is the following:
class User {
function profile()
}
class Authentication {
function login()
}
class Signup {
function register()
function sendVerificationEmail()
}
We now have three classes, each responsible for only one part.
Open Closed Principle
The open-closed principle states, "A class should be open to extension but closed to modifying." According to the previous state, each class should be written in a way that allows the addition of functionality but does not allow alteration of its core.
To demonstrate a demo of this principle, I'll create another User class but this time, it will have a variable called type
:
class User {
var type
constructor(var type) {
this.type = type
}
function doSomething() {
if(type == "normal") {
// ...
} else if(type == "professional") {
// ...
}
}
// ...
}
User normalUser = new User("normal")
User professionalUser = new User("professional")
The class is open for modification because type
has a direct influence on the class's functionality; this is an example of why we need the open-closed principle. Let's see an example of how to apply the principle here:
abstract class User {
abstract function doSomething()
}
class NormalUser Inherits User {
function doSomething() {
// ...
}
}
class ProfessionalUser Inherits User {
function doSomething() {
// ...
}
}
User normalUser = new NormalUser()
User professionalUser = new ProfessionalUser()
By implementing the principle, we can write a more precise functionality.
Liskov Substitution Principle
The Liskov substitution principle states, "if S is a subtype of T, then objects of type T may be replaced with objects of type S" Liskov substitution is better explained with an example:
abstract class Employee {
abstract function getBonus()
}
class HardWorkingEmployee Inherits Employee {
function getBonus() {
// ...
}
}
class LackingEmployee Inherits Employee {
function getBonus() {
Error
}
}
And now, to instantiate an instance of HardWorkingEmployee
class:
Employee employee = new HardWorkingEmloyee()
employee.getBonus()
The example above would work perfectly, now let's change HardWorkingEmployee
with LackingEmployee
:
employee = new LackingEmployee()
employee.getBonus()
The example above would not work because, LackingEmployee
does not implement bonus
function, even though they both are of type Employee
, and that is a violation of the Liskov Substitution; an easy fix for this situation is the following:
abstract class Employee {
// ...
}
interface EligableForBonus {
function getBonus()
}
class HardWorkingEmployee Inherits Employee, EligableForBonus {
function getBonus() {
// ...
}
}
class LackingEmployee Inherits Employee {
// ...
}
Now LackingEmployee
does not inherit any non-implemented functionality.
Interface Segregation Principle
The interface segregation principle states, "Clients should not be forced to depend upon interfaces that they do not use." It means that when a class inherits a parent, it should implement all of its functionality.
interface UserFunctions {
function function1()
function function2()
function function3()
function function4()
}
class User inherits UserFunctions {
function function1() {
// ..
}
function function2() {
// ..
}
function function3() {
// ..
}
function function4() {
Error
}
}
The User class does not implement function4
and thus, it violates the interface segregation principle; one way of fixing it is the following:
interface CoreUserFunctions {
function function1()
function function2()
function function3()
}
interface ExtraUserFunctions {
function function4()
}
class BasicUser inherits UserFunctions {
function function1() {
// ..
}
function function2() {
// ..
}
function function3() {
// ..
}
}
class UpgradedUser inherits UserFunctions, ExtraUserFunctions {
function function1() {
// ..
}
function function2() {
// ..
}
function function3() {
// ..
}
function function4() {
// ..
}
}
Now we have BasicUser
and UpgradedUser
and both fully implement what they inherit.
Dependency Inversion Principle
The Dependency Inversion Principle states, "The interaction between high level and low level modules should be thought of as an abstract interaction between them." It means that if we have interactions between high-level and low-level modules or classes, we should add a middle layer because they both operate at different levels of complexity.
class DataAccesslayer {
function saveToDatabase(data) {
// ...
}
}
class User {
DataAccesslayer dataAccesslayer
dataAccesslayer.saveToDatabase(data)
}
The example above does not implement the Dependency inversion principle, thus any modification to DataAccessLayer
might means we need to modify User
class also, a better implementation would be like this:
interface DataAccesslayerInterface {
function saveToDatabase(data)
}
class DataAccesslayer inherits DataAccesslayerInterface {
function saveToDatabase(data) {
// ...
}
}
class User {
DataAccesslayerInterface dataAccesslayer
dataAccesslayer.saveToDatabase(data)
}
Now we have an abstraction layer, which means that modification on DataAccesslayer
will no longer need modification on User class because the abstraction layer does not change; thus function signature does not change
Top comments (0)