Brutally SOLID Typescript

Ihsan Müjdeci
ITNEXT
Published in
5 min readOct 9, 2019

--

I will teach and demonstrate stripped down SOLID in a typescript context.
SOLID [2] are 5 design principles that when properly understood transform the way you think about code and write code.

Brutalism Architecture, GEISEL LIBRARY BY WILLIAM PEREIRA. 1970, SAN DIEGO, CA. Ryan Kelehar via Shutterstock

Single Responsibility

“Classes, functions or modules have a single reason to change”

  • Functions, classes, modules, files and folders are to be consisted of a single domain or parts of that domain split up into units, thus the “single reason to change”.
  • Grouping together of unrelated code leads to unintended side effects in code in the form of bugs.
  • Small units have the benefit of overall workflows that can be tested individually.
  • Small units exposing APIs that are thought out in what their role is in their domain leads to easier composition.
  • Project structure should reflect these individual domains [5]
  • Personally I think it’s the most complex principal to follow.

This code example violates ALL SOLID principles

We’ll start with solving for Single responsibility. This user class is responsible for many things. Sending the email, containing the url, and constructing the email message.

Now this user is responsible for only itself. The motivational email is easily testable because it returns a string and depends on nothing.

The email client is only responsible for email related tasks and is reusable. We added another function just to prove another point later

This is closer to true solid. This function is responsible for too much. But a good concept to keep in mind some functions are responsible for composing larger groups of tasks. Just as a folders responsibility is to hold other files which are composed of exposed apis and code wich cover a domain e.g User, Product, Queue.

(Polymorphic) Open-Close [9]

“Open to extension closed to modification”

  • Use and export interfaces in a way that lets you depend on them instead of concretions.
  • This exposes an api that is closed to modification and can be extended upon.
  • Using an interface to define the interaction of api as a abstract construct leads to low coupling.

Easy as that, now if we are to implement another email sender we can. Doesn't seem like anything too important but it comes together later on.

Liskov Substitution

“For b subtype of a, a may be replaced by type b”

  • Implementing interfaces leads to easy substitution from one class/object to another.
  • Can be achieved with inheritance but prefer interfaces/composition. [7]
  • Leads to great interoperability.
  • Useful to make mock classes or objects with as well.

Lets say requirements change and we need to use another email client, we create another email client with different implementation as previous client.

now in this case the code can more easily be switched out with either implementation.

Interface Segregation

“Make small client specific interfaces”

  • Functions and classes that use these honed down interfaces leads a narrow purposeful scope of methods it can access.
  • If in the future the if the underlying concrete type or implementation changes or is refactored the clients using its interface are unaffected.
  • No need to mock a whole object when testing functions or classes that depend on the interface.
  • Lightweight and easy to construct mocks.

A partial issue with the sendMotivationalEmail function is that if another developer wanted to use the function they would have full access to all the methods of the email class. This is an important point for communicating intent to the developer.

We’ve now narrowed down the scope of what the function has access to. Our intent is conveyed through code.

Dependency Inversion

“Between a high-level module and a low-level one the interaction should be thought of as an abstract interaction between them”

  • Pass in dependencies in the form of constructor or function parameters to the higher level module from the lower level module.
  • Have the higher level module accept this dependency in the type of an interface.
  • Leads to easy testing and clear boundaries of dependencies.
  • Leads to low coupling, allowing for varying implementations or business requirements changing but the contract between modules remains the same.

This is where solid comes together. The main function is responsible for bootstrap. sendMotivationalEmail is responsible for composing the email and it depends on an abstraction for the sending of the email. This comes in especially useful if we want to test the whole function.

Below are example tests that leverages all the advantages provided by solid.

Now there is a clear way to inject dependencies and we don't need to implement the whole email class. No longer does the code reach a real email server and all without the use of a library, just simple programming. The dependencies are fed in from outside the scope, and thus not constructed within a class. This gives profound ability to move code around, add new functionality and have no need to bootstrap the entire app just to test a small slice of it

Some of the examples are contrived, and some functions aren’t perfectly one responsibility but I hope you get the idea of what these principals can achieve.

Conclusion

On the surface solid revolves around interfaces in most of its principals. This is because interfaces are inherently an abstract construct not depending on any concretions. Code interactions are now loose contracts that can be adhered to by anything.
This produces a very loose coupling. With single or few method interfaces making your code base only depend on what it needs to, refactoring and mocking become simplified. Paired with Single responsibility which is having units of your code be responsible for one thing leads to abstractions at every level.
This abstraction along with defining of api boundaries with interfaces allows the slotting in and out and refactoring of code with minimal impact on the rest of the code base.

Below are a curated resources that have helped me in forming a more complete understanding of SOLID and some other general programming principles are mantras.

Last but not least… Don’t forget all caps when you spell SOLID name [10]

1: https://stackify.com/solid-design-principles/ Long but good examples of solid in java
2: https://en.wikipedia.org/wiki/SOLID Wiki on solid
3: https://blog.cleancoder.com/uncle-bob/2014/05/12/TheOpenClosedPrinciple.html Open close (uncle Bob)
4: https://www.youtube.com/watch?v=zzAdEt3xZ1M Golang solid talk
5: https://www.ardanlabs.com/blog/2017/02/package-oriented-design.html Folder layout for golang but can be adapted to other langs
6: https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html Single responsibhility (uncle Bob)
7: https://en.wikipedia.org/wiki/Composition_over_inheritance Composition over inheritance
8: https://reactjs.org/docs/composition-vs-inheritance.html React Composition vs Inheritance
9: https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle#Polymorphic_open/closed_principle Polymorphic open close
10: https://www.youtube.com/watch?v=ewc1hixzYPY ALL CAPS
11: https://medium.com/@cep21/what-accept-interfaces-return-structs-means-in-go-2fe879e25ee8 ‘Accept interfaces return structs’ (golang idiom) ‘accept interfaces return concretions’ as a more general term

--

--