SOLID Principles in JavaScript: What the "S" Stands for
You may already know some design principles or design patterns. This article mainly explains the SOLID principles gradually:
- How do you code without SOLID and what are the problems?
- Which principle in SOLID should be used?
- How should we modify the code using SOLID?
I believe that contrasting and immersive examples will make it easier for you to understand SOLID principles and how to apply them to code practice.
This is the second translated article of SOLID (there are five original articles in total), the author is serhiirubets , welcome to continue to pay attention.
In this article, we will discuss what SOLID principles are, why we should use them and how to use them in JavaScript.
What is SOLID
SOLID is an acronym for Robert C. Martin's Top Five Object-Oriented Design Principles. The purpose of these principles is to make your code and architecture more readable, maintainable, and flexible.
Open-Closed Principle
O - Open closed principle . Entities (classes, modules, methods, files, etc.) should be open for extension and closed for modification. Difficult to understand by definition, here are a few examples:
Assumption: We have several different shapes, circles, directions, triangles, and need to calculate the sum of their areas. How to solve it?
Nothing hard, let's create a class for each shape, each with different fields: size, height, width, radius, and type fields. When calculating the area of each shape, we use the type field to differentiate.
class Square{
constructor(size){
this.size = size;
this.type ='square' ;
}
}
class Circle{
constructor(radius) {
this.radius = radius;
this.type = 'circle' ;
}
}
class Rect{
constructor(width, height) {
this.width = width
this.height = height;
this.type = 'rect' ;
}
}
Let's create another function to calculate the area.
function getTotalAreas (shapes){
return shapes.reduce((total, shape) =>{
if (shape.type =='square') {
total += shape.size * shape.size;
}else if (shape.type = 'circle') {
total += Math.PI * shape.radius;
}else if (shape. type == ' rect') {
total += shape.width * shape.height;
}
return total;
}, 0);
}
getTotalAreas([
new Square(5),
new Circle(4),
new Rect(7,14)
]);
It doesn't seem like a problem, but imagine if we wanted to add another shape (prototype, ellipse, diamond), what should we do? We need to create a new class for each of them, define the type and add new if/else in getTotalAreas.
Notice:
O - Open closed principle . Let's repeat it again: this principle means: Entities (classes, modules, methods, etc.) should be open for extension and closed for modification.
In getTotalAreas, it needs to be modified every time a new shape is added. This does not comply with the open-closed principle , what adjustments do we need to make?
We need to create getArea method in each class (type field is no longer needed and has been removed).
class Square {
constructor(size) {
this.size = size;
}
getArea() {
return this.size * this.size;
}
}
class Circle {
constructor(radius) {
this.radius = radius;
}
getArea() {
return Math.PI * (this.radius * this.radius);
}
}
class Rect {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
function getTotalAreas (shapes) {
return shapes. reduce((total, shape) => {
return total + shape. getArea();
},0)
}
getTotalAreas([
new Square(5),
new Circle(4),
new Rect(7,14)
]);
Now that we've followed the open-closed principle, when we want to add another shape, like a triangle, we'll create a Triangle class (open to extension), define a getArea method, and that's it. We don't need to modify the getTotalAreas method (closed for modification), we just need to add a parameter to its array when calling getTotalAreas.
Let's look at a more practical example, assuming the client receives an error validation message of the specified format:
const response = {
errors: {
name: ['The name field should be more than 2 letters', 'The name field should not contains numbers'] ,
email: ['The email field is required'],
phone: ['User with provided phone exist']
}
}
Imagine that the server uses a different service for authentication, either our own service or an external service that returns a different format error.
Let's simulate the error using the simplest possible example:
const errorFromFacebook ='Bad credentials' ;
const errorFromTwitter = ['Bad credentials'];
const errorFromGoogle = { error: 'Bad credentials' }
function requestToFacebook() {
return {
type: 'facebook',
error: errorFromFacebook
}
}
function requestToTwitter() {
return {
type: 'twitter',
error: errorFromTwitter
}
}
function requestToGoogle() {
return {
type: 'google',
error: errorFromGoogle
}
}
Let's convert the error to the format required by the client:
function getErrors() {
const errorsList = [requestToFacebook(), requestToTwitter(), requestToGoogle()];
const errors = errorsList.reduce((res, error) => {
if (error.type == ' facebook') {
res.facebookUser = [error.error]
}
if (error.type == 'twitter') {
res.twitterUser = error.error;
}
if (error.type == 'google') {
res.googleUser = [error.error];
}
return res;
},[]);
return { errors };
}
console.log(getErrors());
We get what the client expects:
{
errors: {
facebookUser:['Bad credentials'],
twitterUser:['Bad credentials'],
googleUser:['Bad credentials']
}
}
However, still the same problem, we did not follow the open-closed principle , when we need to add a new validation from an external service, we need to modify the getErrors method and add new if/else logic.
How to solve this problem? A possible solution is: we can create some generic error validation class and define some generic logic in it. We can then create our own class for each error (FaceBookValidationError, GoogleValidationError).
In each class, we can specify methods, like getErrors or TransformErrors, every validationError class should follow this rule.
const errorFromFacebook =' Bad credentials ' ;
const errorFromTwitter = ['Bad credentials'];
const errorFromGoogle = {error: ' Bad credentials'}
class ValidationError {
constructor(error) {
this.error = error;
}
getErrors() {}
}
class FacebookValidationError extends ValidationError {
getErrors() {
return { key: ' facebookUser', text:[this.error] };
}
}
class TwitterValidationError extends ValidationError {
getErrors() {
return {
key: ' twitterUser',
text: this.error
}
}
}
class GoogleValidationError extends ValidationError {
getErrors() {
return { key: ' googleUser', text: [this.error.error] }
}
}
Let's use this error validation class in Mock's function and modify the getErrors function:
function requestToFacebook() {
return new FacebookValidationError(errorFromFacebook)
}
function requestToTwitter() {
return new TwitterValidationError(errorFromTwitter)
}
function requestToGoogle() {
return new GoogleValidationError(errorFromGoogle)
}
function getErrors (errorsList) {
const errors = errorsList.reduce((res, item) => {
const error = item.getErrors();
res[error.key] = error.text
return res ;
}, {});
return {errors}
}
console.log(getErrors([requestToFacebook(), requestToTwitter(), requestToGoogle()]));
As you can see, the getErrors function receives errorList as a parameter instead of hardcoding it in the function. The running result is the same, but we follow the open-closed principle, when adding an error: we can create a new validation class for this error and specify the getErrors method (open to extensions), getErrors can help us return the external service information into the format we need. We call the getErrors of the error class in the general getErrors method without any other modification (closed for modification).
Welcome to the WeChat public account "Chaos Front End"
Recommended reading:
Understanding SOLID principles of programming based on TypeScript
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。