After reading the SOLID Principles in JavaScript post by Subbu I thought about how our frameworks follow or don't follow these principles.
As you might have guessed, currently my framework of choice is React. So I checked if it adheres to these principles.
What does SOLID stand for?
SOLID is an acronym build by the first letter of 5 object oriented programming design principles. The basic idea is, if you follow these principles, your software gets better.
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
What do these principles imply and how does React adhere to it?
Single Responsibility Principle
What does it mean?
A class should only have a single responsibility.
How does React adhere to it?
React applications consist of components, which are classes that inherit from the React.Component
class. You can start building your application as a component and if it gets too complex, you can split this component up into multiple smaller components.
React doesn't force you to adhere to the principle, but you can split up your component classes into smaller components till you achieved single responsibility for all of your components.
For example, you could have a button component that just handles clicks and an input component that just handles user input. A level above you use a form component that uses multiple instances of the button and input component to get user credentials and above that a connection component that takes form data and sends it to a server.
Open Close Principle
What does it mean?
Software entities should be open for extension, but closed for modification. Which means you can extends it without modifying its source code.
How does React adhere to it?
Reacts component model is build around aggregation instead of inheritance. So you only extend the base React.Component
and not its children. This prevents you from overriding behavior of existing components directly. The only way is to wrap it with your own component.
You could for example wrap a Button
with a RedButton
that always applies specific styles to the basic Button
, but the Button
is closed for modification.
This is less flexible than inheritance, but it also simplifies the API. While you don't have direct access to the methods like in an extension, you only have to care about props
in your aggregation.
Liskov Substitution Principle
What does it mean?
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
How does React adhere to it?
Well, it doesn't use inheritance at all. Sure you extend React.Component
, but this class is essentially treated as abstract in React applications, you never directly create an object from it, so you never have to replace it with a child-class later.
On the other hand, you find yourself writing aggregations that should act like their wrapped components rather often. Like the Button
I mentioned before. You want that RedButton
to be already styled, but you also want it to act like the Button
, but since the API between components always is just props, it's often simple to add something while your wrappers props are passed down to the wrapped component. Because everything is dynamic, your wrapper doesn't even have to know everything about the data that would originally be passed down to the wrapped component, in the RedButton
example it would just have to know about the style.
Interface Segregation Principle
What does it mean?
Many client-specific interfaces are better than one general-purpose interface.
How does React adhere to it?
Because React is written in JavaScript, it benefits from the dynamic nature of this language. There are no formal interfaces. If you don't use refs
, which allow you to directly call class methods of a component, the only interaction between components is via props and nobody forces you to use props you don't need.
If you have a wrapper component that passes down an onClick
handler that shows an alert with the wrapped components class name, you can use this wrapper to wrap all components that use this onClick
prop and if they don't, the handler is just ignored.
My experience with this fact was that it simplified many things, you wouldn't get lost in defining many small interfaces beforehand. The drawback was that I often found me in situations where I passed down props the wrapped component did simply ignore silently. At least glamorous-native
threw a few warnings when I tried to pass down unknown CSS attributes. For this it often helps to use PropTypes or something.
Dependency Inversion Principle
What does it mean?
One should depend upon abstractions, not concretions.
How does React adhere to it?
In practice, this principle is often followed by removing class names from other classes. Like, you could have a List
that has Items
, so you could get the idea to create your Item
objects inside the List
class, now you have your List
tightly coupled with your Item
. Somewhere in your List
class is a new Item(...)
or Item.create(...)
etc.
React doesn't strictly adhere to it, you can pass an array of string to your List
component and create Item
children
from it no problem.
But you can also tell the List
it should simply render out its children
independent of what they are, maybe add some keys to it or justify them etc.
Now you can create an array of Item
s, sprinkle it with some HighlightItem
s, both created from different string arrays and put them inside your List
who won't be the wiser.
Conclusion
While React doesn't force the principles onto you, at least it often allows you to follow them. Sometimes it gets easier because of JavaScript, sometimes JavaScript makes it harder, but overall it is possible to write SOLID applications with React.