Videos Applying SOLID principle in JavaScript without Class and Object

Description

The SOLID principle is well-known in our industry. However, most of the articles, books, and examples are based on traditional Object-oriented language constructs. This talk will show how can we apply these principles in Javascript where classes are not necessary nor encouraged.

Chapters

  • Introduction and Speaker Background 0:00
  • Applying SOLID Principles in JavaScript without Classes 0:27
  • Speaker's Background and Software Architecture Perspective 0:46
  • SOLID Principles Overview 1:44
  • Motivation for Applying SOLID in JavaScript 2:01
  • Single Responsibility Principle: One Reason to Change 2:49
  • Example: User Registration and Password Policy 3:13
  • Separating Concerns for Clear Communication 4:42
  • Implementation with and without Classes 5:03
  • Class vs. Function Composition: Trade-offs 6:23
  • Without Classes: Incremental Development and Fluidity 7:35
  • Team Implementation and Standardization Challenges 8:46
  • Balancing Standardization and Flexibility 11:15
  • Open/Closed Principle: Extension without Modification 11:44
  • Example: Invoice Confirmation and Side Effects 11:58
  • Liskov Substitution Principle: Substitutability of Derived Classes 14:50
  • Example: Error Reporting in Production vs. Local 15:07
  • Ensuring Total Substitutability 17:01
  • Function Replaceability and React Component Example 17:21
  • Interface Segregation Principle: Avoiding Unused Dependencies 18:03
  • Example: Invoice Deletion, Recovery, and GDPR Compliance 18:14
  • Avoiding Unnecessary Parameters and Embracing Duplication 20:18
  • Dependency Inversion Principle: Decoupling High-Level from Low-Level Modules 21:43
  • Example: User Avatar Upload and Object Storage 21:58
  • Dependency Injection for Abstraction and Communication 22:58
  • Polymorphism and Perspective in Communication 24:47
  • Trade-offs of Class-Based vs. Function-Based Injection 25:35
  • Conclusion and Key Takeaways 26:43

Transcript

These community-maintained transcripts may contain inaccuracies. Please submit any corrections on GitHub.

Introduction and Speaker Background0:00

Alright, now our speaker is ready. Please give a round of applause to our speaker, Chakrit Likitkhajorn senior software engineer from Omise

Applying SOLID Principles in JavaScript without Classes0:27

Hello everyone My topic is about applying SOLID principle in JavaScript without class and object You can call me Chris, this is my nickname I try to wrap this up in 25 minutes So let's get to it

Speaker's Background and Software Architecture Perspective0:46

Let's talk a little bit about my background because it's related to how I view software architecture I was a vice president of engineering at Taskworld At that time I take care of the whole product team and responsible for making sure that the whole product can be sellable and awesome I lead both engineering and design and the product team

Now I'm working as a senior software engineer at Omise Payment I have written production code in eight different

programming languages

The way that I have a chance to work with many different departments affects how I view software architecture

and how I write code a lot You will see what it means in my presentation

SOLID Principles Overview1:44

So what is actually SOLID? I will try to make this short SOLID is five design principles introduced by Uncle Bob, which is Robert C. Martin and is very well known throughout the industry

Motivation for Applying SOLID in JavaScript2:01

The motivation of why I deliver this talk here is that a lot of programmers have read about the SOLID design patterns but it's usually being demonstrated using programming language with class and objects But JavaScript we are multi-paradigm We can use class or don't use class We can write in object-oriented or functional programming We can write basically however we want Let's explore the alternative way of implementing SOLID principle but not using class and objects at all By doing that you will see the core content of what SOLID principle actually means

Single Responsibility Principle: One Reason to Change2:49

Let's first talk about single responsibility principle It basically says that every module, every class, or every function should have just a single responsibility This is a very hard principle to absorb and to consume because it's hard to define what single responsibility means

Example: User Registration and Password Policy3:13

Let's take a look at an example Let's say that we have a requirement where we need to create a HTTP endpoint to register a new user

The passwords minimum should be four characters so user cannot create an account with very short password Here's the simplest way to write using Express

We have a username password We check for the length and return 422 and password is too weak

If it's not then we register the user then we return success response If we write code like this then what happens is there are times where people from different departments will come to you and ask what the current password policy is and should we strengthen it from security team And there's a business team that can come to ask if we should gather more information before register any user Then let's ask for company name, country, etc

Whenever you talk to another department, you talk to security team and you see that in one function you have bunch of related code and unrelated code cluttering together It's hard now if you come to the meeting with these teams It's hard to tell exactly what happened in the meeting room by looking at the code

Separating Concerns for Clear Communication4:42

Now there are two completely separate reasons to change: One is we have a new password policy from security team or we need all information on register from business team So let's separate it out to two sub components whether it's class or function

Implementation with and without Classes5:03

And surprisingly, what I have ended up with when I try

to apply this principle is I come to realize that the reason to change is source of change, or put it simply, is about the people that you communicate with.

And now if you separate to two class or two function, then you are to talk with the business team, and then you say that okay when I talk to this guy, then I need to look at the user registration class. And when I talk to security team, I just open the code and identify in a password class, and now you have the whole context of each team inside your single coding file.

And surprisingly, I found that when I come to that realization and do this presentation, I ended up seeing that Uncle Bob, the originator of SOLID principle, also come to the same conclusion that I found. He says that the purpose of single responsibility is having different C-level executive responsible for different things, which is interesting and I'm happy that I come to the same conclusion as him.

Class vs. Function Composition: Trade-offs6:23

Okay, so let's see how we can implement this. For this principle, I will compare how we can do it with class and without class, and you can see the different approaches.

So with class, then you create a user validation class that's responsible for the input, and you open this class whenever you talk to frontend team and frontend team asks about what kind of input we are receiving. And you have another class which is a password class, and you open this class whenever you talk to security team and they ask about how we store the password and how strong our password is.

And you have another class called the user data class which is responsible for saving data to the database, and you can open this class whenever you talk with the database administration teams, and you can see the whole context of how we use the database system. And you have the whole flow being composed in the controller which is one object responsible for composing the whole flow.

Without Classes: Incremental Development and Fluidity7:35

Okay, let's see how we can do this without class. It's very simple - you have a bunch of functions but the same concept. So you have three functions and each function is the one that you look at when you talk to each team. Now you have password, validate registration, and save user, so it's the same concept, just instead of using class you separate it using functions.

And to compose it you can use the pattern called

higher order function which takes a bunch of functions and creates another function. And you can see that it validates using the validator,

and after validation passes, execute business logic using the controller which is another function and it returns to OO.

And then you just use this composed function called create handler and you put it into express. It's basically the same separation, just a different style of doing things.

Team Implementation and Standardization Challenges8:46

And what I found about not using class is

it encourages incremental development. And what I mean by encourage is that let's say you have a small app, you put everything inside just one big function. And now you're becoming larger, you want to separate the concerns to different concerns, and you can just move these two lines here, you can just move this one line here, and then you can just write the higher order function and replace it with this higher order function.

You can basically just move code around and separate the concerns just like that. Unlike the object oriented approach where you need to create class, create a bunch of constructors, and after doing that then you can move code, which is more tedious. So when you're not using class, I found that it feels more fluid. The code becomes fluid - you can change however you want to compose things. It's not as rigid as the object oriented approach. So at first I really like this approach, I try to implement it into my team.

What really happen when you implement this into the team? This is what I found when we use the object-oriented approach: Since it's harder to change the structure, it incentivize the team to use the same structure. And as a result, it's easier to standardize the practice for the whole team. Even if it's hard to structure it in a different way,

but it encourage the standardization. At the opposite, function composition encourage you to have a different type of composition. You can compose it in a way that is actually very fit to the domain business domain that you are solving. But in the other hand, it encourage people to do bunch of different things in the codebase and you lose the standardization. So that is a trade-off that I found when I try to implement this into the team.

Balancing Standardization and Flexibility11:15

By knowing this trade-off, then you can see that when I try to use function composition, I need to make sure that people still conform to some standardization. And when I use object-oriented approach, then I need to make sure that people don't conform to the standard practice and try to come up with maybe better way to compose

and make sense of the code.

Open/Closed Principle: Extension without Modification11:44

Let's jump to the next principle: open-close principle. Open-close principle say that the software should be open for extension but close for modification. Let's jump to the example now.

Example: Invoice Confirmation and Side Effects11:58

Let's say that you start with the user can confirm the invoice and you have a function called 𝚌𝚘𝚗𝚏𝚒𝚛𝚖𝙸𝚗𝚟𝚘𝚒𝚌𝚎, you save it. And now after invoice is confirmed, you need to send the email to customer and you implement it here. And after this, you say that after invoice confirmed, you need to send push notification. So you just send the push notification.

And what happen is when user complain about

not getting the email, then it's in just these two lines. And when the user complain about not getting the push notification, then it's another lines inside the single function.

So generally speaking, you have one function

that's responsible for many issues. And when each team working in parallel, it introduce a lot of merge conflict. And we really like to solve the merge conflict, right?

Function will grow bigger and bigger.

When you try to modify the current behavior of function, it's possible that some people rely on the function to act in a certain way. And when you modify it, it can introduce breaking change that you don't know because the guys that depend on the behavior is someone in the team that very far from you. You ended up in the situation like this again that you have bunch of related code when you talk and work with some requirement and bunch of unrelated code when it's clutter together.

So how to solve it? Then you can use observable, event, or stream. So you can say that for the confirm invoice, you just say that after save, then after saving it, I put it into invoice stream. And then you have 𝚜𝚎𝚗𝚍𝙸𝚗𝚟𝚘𝚒𝚌𝚎𝙴𝚖𝚊𝚒𝚕 and you have 𝚜𝚎𝚗𝚍𝙿𝚞𝚜𝚑𝙽𝚘𝚝𝚒𝚏𝚒𝚌𝚊𝚝𝚒𝚘𝚗 and you push this function inside the stream, listen from it to listen for the event. And now the confirm invoice will be maintain

and look the same always. So you never modify the behavior of confirm invoice, but you just make it extendable and you extend it. And that's the open-close principle.

The benefit of using stream is that you can scale to become like you have email sender as another process. You put the stream into Kafka or RabbitMQ or whatever queue system that you have. And you can scale the system that way, but you can start small just using Rx or observable stream.

Liskov Substitution Principle: Substitutability of Derived Classes14:50

Let's talk about Liskov substitution principle. It basically say that the derived class must be substitutable for the base classes. It's hard to absorb just this sentence, so let's take a look at the example now.

Example: Error Reporting in Production vs. Local15:07

Let's say that we have the 𝚛𝚎𝚙𝚘𝚛𝚝𝙴𝚛𝚛𝚘𝚛 for the production and 𝚛𝚎𝚙𝚘𝚛𝚝𝙴𝚛𝚛𝚘𝚛 for the local machine doing different things. In production, you need to send the error to Sentry. You have some plugins that actually insert custom metadata to the error. And so the 𝚛𝚎𝚙𝚘𝚛𝚝𝙴𝚛𝚛𝚘𝚛 to the production will just also report that metadata. But for the report when you work in your local machine, you don't want to send every error to Sentry. So you just do the 𝚌𝚘𝚗𝚜𝚘𝚕𝚎.𝚎𝚛𝚛𝚘𝚛.

But what happened here is that the 𝚛𝚎𝚙𝚘𝚛𝚝𝙴𝚛𝚛𝚘𝚛 in production can explode when custom metadata is undefined. Which means that 𝚛𝚎𝚙𝚘𝚛𝚝𝙴𝚛𝚛𝚘𝚛 in production cannot substitute the 𝚛𝚎𝚙𝚘𝚛𝚝𝙴𝚛𝚛𝚘𝚛 locally. It cannot totally substitute it. And what happened is the user of the report error

need to know and need to handle some little secret that if it's in production but for some reason we doing things wrong we don't have custom metadata then we need to insert it because one of the report error implementation just cannot handle when

the custom metadata is undefined. So that's kind of break the Liskov substitution principle. And please don't do it this way because the problem of doing this way is the implementer of do something need to know a little secret about the report error which might be the guy that do the report error

function and the guy that do the do something function might be in a different team so we don't want the knowledge to be clutter all around.

Ensuring Total Substitutability17:01

And what you should do here is to make it totally substitutable so you should just handle the custom metadata error here and make sure that report error in production can be total substitute to the report error locally.

Function Replaceability and React Component Example17:21

So basically what it means is function should be replaceable. This example is not so common because we don't use this pattern a lot but it's more common when you have bunch of React components that you need to render based on if they are admin then render this component if user is just a user then render another component and you need to make sure that in every cases possible this admin page and this user page should be substitutable and there's nothing that it can everything that work with user page should work with admin page and everything that work on admin page should work in user page too.

Interface Segregation Principle: Avoiding Unused Dependencies18:03

Okay so let's talk about interface segregation principle. It says that the client should not be forced on or depend on method that they may not use.

Example: Invoice Deletion, Recovery, and GDPR Compliance18:14

Okay so let's take a look of how the example now. So let's say that you start with the delete invoice you don't want to actually delete it so you just set is delete to be true. And now after that then you need to implement the recover the deleted data so you implement it recover invoice and now you see that the code is duplicate so you just say that okay let's remove the duplication and make it like this and let's update the status and now duplication is being removed. And then it's come now you don't conform to the GDPR because GDPR say that data can be removed and user can be forgotten now you need to do a hard delete. And if you still try to conform to this you might ended up that okay let's put another parameter here which is a hard delete and if it's hard delete then you just remove the whole things and if not then you just change the boolean here change the is delete here. And when you implement it like this it become

for the delete implementer this parameter hard delete make total sense for him but for the recover implementer it's okay why I need this hard delete parameter it's not make sense for me at all it's such a nonsense.

And now if on recover we need to implement something such as if one month pass we need to reassign the invoice number then now you need to have the recover logic inside this change delete status function. And you ended up in this again that when you want

to know how we recover things you need to look a bunch of related code and when you want to know how we delete invoice there's also related code and unrelated code clutter all together again.

Avoiding Unnecessary Parameters and Embracing Duplication20:18

And the solution is simply in this case is simply re-duplicate the code and just make separate case for delete invoice and recover invoice. The more and the takeaway here for this principle is

function parameter should make sense for the majority of customers. Otherwise, let's consider having separate function. And I saw that this principle is violated because as I show in my example earlier, when you try to reduce the duplication, but then new requirement come

and you try to conform to the current structure and don't look back and see that, okay, is it still make sense to do it this way? Should we reduplicate the code? Then you are likely to violate this principle. So I would say that code duplication should not be viewed as a dogma and sometimes it's better to have some duplication, but have a very separate domain where we can look at and see that, okay, delete and recover, we do totally different things here and it makes sense.

Dependency Inversion Principle: Decoupling High-Level from Low-Level Modules21:43

Okay. So the last principle, Dependency Inversion Principle.

So it basically says that the high level module should not depend on low level modules. And the example here is that you have case where

Example: User Avatar Upload and Object Storage21:58

you need to upload user avatar, which is a high level requirement. And you need to use S3, Amazon S3, as object storage, which is infrastructure level requirement. And okay. So you can start simple, right? And you have a class user, you implement the upload avatar. And in this principle, I will compare how you do it in class and do it without class. And with class, then you just have S3 and upload file and without class, then you just do the same things. But now the next requirement come and you need to be able to store in the Google Cloud Storage as well.

And the naive implementation is you just have if like if the config is S3, then we use S3 and if not, then we just use Google Cloud. And it's the same for class and function.

Dependency Injection for Abstraction and Communication22:58

The problem about this naive implementation is now the upload function implementer need to know about the deep infrastructure, whether we use S3 and

Google Cloud Storage. So it's kind of things that now you have to know

more things in order to contribute to the project. And that's not good. And we can solve this by using technique called dependency injection. So you have class for S3 storage. In the class world, you have class called S3 storage, Google Cloud Storage, and you inject here. And in the function world, it's totally the same concept, but instead of injecting class, you inject the functions instead. So you inject the upload S3 function and inject the upload Google Cloud function. And the benefit of this is when people come to ask you how upload works and can we change it, then you can just look at the whole flow without noticing that inside the upload function, it's actually we have two storage. And it's easy to explain to the business who don't know much about our internal infrastructure that this is how the whole flow work. Just looking at the code, you can totally explain without need to write a complicated documentation. You can just go to meeting, open the laptop, look at the code, and explain it in the meeting room immediately. And if they ask, okay, how can I change it? I can answer immediately in the meeting room.

Polymorphism and Perspective in Communication24:47

And this is why we all love polymorphism. And I think this is a very important thing that polymorphism gives us. It allows us to express the different flow from different perspectives. You can express the flow of the high level flow and completely set aside the low level detail.

And now you can communicate and write a class or function that can be used when you communicate with any type of stakeholders, which is for me, as I work with multiple stakeholders since the start, then I find this style of coding really useful.

Trade-offs of Class-Based vs. Function-Based Injection25:35

But that's a trade off because when you use class and object, then you can only inject the whole objects and object can have so many methods. And you can end up that you just want just one method, but you need to inject the whole things, the whole objects that have 10 different methods into the class that you want. So let's consider if you use this approach, then please consider if the object become too big and break it down to different function. In the function, it's opposite because you can inject just one function at a time. So in the past, I ended up that have one function

that need to inject like seven or eight functions in order to make it work. And it's become really hard to keep track of where

these seven and eight functions coming from. So in the opposite way, when you use function inject function as dependencies, then you might consider grouping the function in the way that make sense in the business domain context.

Conclusion and Key Takeaways26:43

And that's it. I just went through five principles. But my takeaway here is that I kind of intentionally

structure the talk in the way that, okay, if I write code in this way, then what happened? And if I write code in another way, then what happened? And after that, I talk about which way is more desirable and why. And for me, the desirable as a coder who need to talk to a lot of stakeholders, I want to be able to just look at the code and talk everything in the meeting room. I don't want to have to write a lot of complex document and write a lot of presentation, a lot of diagram. I just want to look at the code and can communicate with every type of stakeholders in the meeting. And this is desirable for me. And I never use the word "violate" or "wrong" because

I want to say that I don't think that's about the architecture is about what is right or wrong. I don't like it when people say that, okay, this programmer violates some kind of principle. So you are very bad and you should feel bad about yourself. I think it's not a healthy way to look at software architecture. So that's why I intentionally structure my presentation this way. And at the end, I believe the architecture is about managing knowledge and communication channel within the team or within the team to the outside stakeholders. It's all about how you manage and how you intend to communicate within team and to the outside of the team. And that's all my talk. Thank you. Thank you, Mr. Chakrit. Thank you so much, Mr. Chakrit, from Omise Thailand na ka.