S.O.L.I.D
SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.
This stands for the below listed five principles in programming:
- S β Single-responsiblity principle
- O β Open-closed principle
- L β Liskov substitution principle
- I β Interface segregation principle
- D β Dependency Inversion principle
In this article we will understand the way how these principles can be used to program the better way and efficient way.
π Single Responsibility Principle
There should never be more than one reason for a class to change.
In other words, every class should have a single responsibility or role in program's functionality, and it should encapsulate that part.
This largely helps in maintainability of the code when we have a new requirement, helps to perform testing by clear testcase, at the same time ensuring full coverage without breaking the codebase by making sure we are not pumping more responsibilites into the same classes and functions in it. Anything which is not relevant to the role of module, should be removed and implemented separately.
class Counter {
count:number=0;
increment(){
this.count+=1;
this.display(this.count);
}
decrement(){
this.count-=1;
this.display(this.count);
}
display (input: number){
console.log(input);
}
}
The above code looks simple and good, but let us say we have to change the way we display the counter value, we have to modify the Counter, but we think twice and see the responsibily of the counter is track the count and not in display which can change based on requirement, if we refractor the same as below we would have honoured the single responsibility principle.
function display(input: number){
console.log(input);
}
class Counter {
count:number=0;
increment(){
this.count+=1;
display(this.count);
}
decrement(){
this.count-=1;
display(this.count);
}
}
π openβclosed principle
Software entities should be open for extension, but closed for modification.
This principle states that the modules should be ready to include new features as per our new requirements but it should not modify everytime to accomodate it. This is were we bring the concept of generalisations. It is normally achieve by two ways:
- Inheritance
- delegate functions
class animal{
name:string;
constructor(name:string){
this.name=name;
}
makeSound(){
switch(this.name){
case 'dog':
console.log("Barking sound");
break;
case 'cat':
console.log("Meow sound");
break;
default: {
console.log("Unidentified sound");
}
}
}
}
let animalList = ['dog', 'cat'];
animalList.forEach(ani => {
new animal(ani).makeSound()
});
In the above example if we wanted to add another animal it very easy to add, but we may end up in always disturbing the class during every requirement, rather if we when we honour this print, it may require more lines of code, but eventually it is more clean and you need not modify the already defined methods, so easy to test and it does not bring new issues via new requirements like this.
interface Animal{
name:string;
sound:string;
}
class Dog implements Animal{
constructor(name:string){
this.name=name;
}
name: string;
sound = "Barking sound";
}
class Cat implements Animal{
constructor(name:string){
this.name=name;
}
name: string;
sound:string = "Meow sound"
}
class Cow implements Animal {
constructor(name:string){
this.name=name;
}
name: string;
sound:string = "Mooing sound"
}
let animalCollection:Animal [] = [new Dog('A'), new Cat('B'), new Cow('C')];
animalCollection.forEach(ani => {
console.log(ani.sound)
});
β The Liskov substitution principle
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
This principle is also refered in Design by Contract
.
Baiscally the expectation here will be like the derived class types should be completely replacable by the base class types, without causing issues in the application. This means that the functions in the derived class members if overriden should take the same argument types and eventually should be able to return the same return types.
The specific functionality of the derived class may be different, but must conform to the expected behavior of the base class. The benefit of conforming this principle consists of reduced coupling, reusability and easier to maintain.
class Rectangle {
width:number;
length:number;
constructor(wid:number, len:number){
this.width=wid;
this.length=len;
}
setWidth(wid){
this.width=wid;
}
setLength(len){
this.length=len;
}
area(){
return this.width* this.length;
}
}
class Square extends Rectangle{
setWidth(wid){
this.width=wid;
this.length=wid;
}
setLength(len){
this.length=len;
this.width=len;
}
}
function increaseWidth(rec:Rectangle){
rec.setWidth(rec.width+1);
}
let rectange = new Rectangle(2,4);
let square = new Square(2,2);
increaseWidth(rectange);
increaseWidth(square);
console.log(rectange.area());
console.log(square.area());
In the above example could be made better by making using of liskov principle as shown below we define a base class and use it in the client module invocations, which makes it consistent.
class Shape{
width:number;
length:number;
constructor(wid:number, len:number){
this.width=wid;
this.length=len;
}
area(){
return this.width* this.length;
}
setWidth(wid){
this.width=wid;
}
setLength(len){
this.length=len;
}
}
class Rectangle extends Shape {
}
class Square extends Shape{
setWidth(wid){
this.width=wid;
this.length=wid;
}
setLength(len){
this.length=len;
this.width=len;
}
}
function increaseWidth(obj:Shape){
obj.setWidth(obj.width+1);
}
let rectange = new Rectangle(2,4);
let square = new Square(2,2);
increaseWidth(rectange);
increaseWidth(square);
console.log(rectange.area());
console.log(square.area());
π The interface segregation principle
Many client-specific interfaces are better than one general-purpose interface.
The principle states that while implementing interfaces we should not force the dervied types to implement any unwanted methods, rather we can create separete interfaces and implement them thoroughly.
interface IAnimal {
run();
fly();
}
class Lion implements IAnimal {
run(){
//
};
fly(){
//
};
}
class Eagle implements IBird {
run(){
//
};
fly(){
//
};
}
When we inspect the above example we could find that that a common interface is used both not all the classed need to implement them because they don't support the feature. As a good practice, using this principle we have to break the common interface and use it fully whereever it is most appropriate in classes.
interface IAnimal {
run();
}
interface IBird {
fly();
}
class Lion implements IAnimal {
run(){
//
};
}
class Eagle implements IBird {
fly(){
//
};
}
βΊ The dependency inversion principle
Depend upon abstractions, but not on concretions.
Technically this is also referred as a combination of open-closed and liskov substitution principle.
- High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
class EmailProcessor {
notify(message: any) {
// some code which will be used for email notification.
}
}
//High level module
{
let processor = new EmailProcessor();
processor.notify("Notification");
}
In the above code snippet you can find that an email processor is used to send notification from a high level moduel. Let us suppose we need to have sms notification as well based on certain cases, we should make use of the dependancy inversion points listed above and make sure the highlevel modules don't import anything from low level module, where us there is a common interface here, which is the NotificationProcessor class used as a wrapper.
class NotificationProcessor {
processor: any;
constructor (processor){
this.processor = processor;
}
notify(message: string){
this.processor.notify(message);
}
}
class EmailNotification {
notify(message: string) {
// some code which will be used for email notification.
}
}
class SmsNotification {
notify(message: string) {
// some code which will be used for sms notifcation.
}
}
//High level module
{
let notificationProcessor1 = new NotificationProcessor(new EmailNotification());
notificationProcessor1.notify("Email Notification");
let notificationProcessor2 = new NotificationProcessor(new SmsNotification());
notificationProcessor2.notify("SMS Notification");
}
Thanks for supporting! π
Would be really great if you like to β Buy Me a Coffee, to help boost my efforts.
π Original post at π Dev Post
Top comments (0)