Putting the `S` in SOLID JavaScript
Feb 16, 2021
S
- Single-Responsibility Principle (SRP) is the first design principal covered by SOLID
principals.
SOLID is an Object oriented design principal. It stands for:
The principals were originally created and promoted by Robert Martin (aka Uncle Bob).
S
- Single-responsiblity PrincipleO
- Open-closed PrincipleL
- Liskov Substitution PrincipleI
- Interface Segregation PrincipleD
- Dependency Inversion Principle
SRP simply states that Classes and functions should only have a single responsibility. This sounds fairly easy to do in principle, but in practice, especially in React it can be slightly more nuanced. Let me explain with a class that handles an Artist and the artist's details.
1// Wrong! class Artist { constructor(id) { this.id = id; this.getBio() this.getSingles(); this.getAlbums() } getBio() { fetch(`/artist/${this.id}/bio/`) //... do stuff with data } getSingles() { fetch(`/artist/${this.id}/singles/`) //... do stuff with data } getAlbums() { fetch(`/artist/${this.id}/articles/`) //... do stuff with data } } const theArtist = new Artist('formerlyKnownAsPrince');
In this example the Artist
class is responsible for multiple things. It should just represent an artist, but it also has the responsibility to get the bio, the singles and the albums.
Why is this bad? For example, if we ever need to update getBio
to have an extra property, such as this.fetchedData = true
we could end up breaking other parts of the class. We could be reeeeeally careful and maybe use more specific variables like this.fetchedBio
but the more complex our application and classes/functions get the harder it will be to manage and track this. Instead, we can split the responsibilities up and create different classes and funcions that only handle one responsibility.
1// Right! function getBio(id) { fetch(`/artist/${id}/bio/`) //... do stuff with data return bio; } function getSingles(id) { fetch(`/artist/${id}/singles/`) //... do stuff with data return singles; } function getAlbums(id) { fetch(`/artist/${this.id}/articles/`) //... do stuff with data return albums; } class Artist { constructor(bio, singles, albums) { this.bio = bio; this.singles = singles; this.albums = albums; } } function getArtistDetails(id) { const bio = getBio(id); const singles = getSingles(id); const albums = getAlbums(id); return { bio, singles, albums }; } const { bio, singles, albums } = getArtistDetails('formerlyKnownAsPrince') const theArtist = new Artist(bio, singles, albums);
Now we have split up the Artist class and moved the methods to pure functions. Each one has a single responsibility.
But what about the getArtistDetails
function being responsible for getting the 3 things (bio, singles and albums)? Well this is ok because that it's really 1 responsibility - to get the data!
Now if we want to save that artist, then we can create a new class - Saver
which can have just one responsibility too! We don't have to worry about what other methods are on the Artist or what properties it may have. We can just take an Artist
and save it without worrying about breaking other properties or methods.
1//... the rest const theArtist = new Artist(bio, singles, albums); // from before class Saver { save (data) { // something to save the data } } const saver = new Saver(); saver.save(theArtist);
React
The same principles apply to react, whether you are creating component classes
or functional components
.
1// Wrong const Artist = ({ id }) => { const [bio, setBio] = useState(null); const [singles, setSingles] = useState([]); const [albums, setAlbums] = useState([]); useEffect(()=> { fetch(`/artist/${id}/bio/`) .then(data => { setBio(data); }) }, [id]) useEffect(()=> { fetch(`/artist/${id}/singles/`) .then(data => { setSingles(data); }) }, [id]) useEffect(()=> { fetch(`/artist/${id}/albums/`) .then(data => { setAlbums(data); }) }, [id]) const singlesList = singles.map(single => ( <li key={single.id}>{single.title} - {single.releaseDate}</li> )); const albumsList = albums.map(album => ( <li key={album.id}>{album.title} <img src={album.artWorkUrl} /></li> )); return ( <div> { bio ? <h2>{bio.name}</h2> : null } <ul>{singlesList}</ul> <ul>{albumsList}</ul> </div> ) }
We have a few things going on that are wrong with the component. There is data retrieval/state logic and there is presentational logic all in the same component. We have the rendering of each list withing the main component. It makes the component very hard to reuse, or change for other uses because it's doing so many things.
Let's fix it! We will separate the data logic from the presentational UI and logic. Each part of the UI has it's own dedicated single responsibility component.
1const Bio = ({bio}) => { if (!bio) return null; return <h2>{ bio.name}</h2>; } const SingleList = ({singles}) => { const singlesList = singles.map(single => ( <li key={single.id}>{single.title} - {single.releaseDate}</li> )); return ( <ul> {singlesList} </ul> ) } const AlbumList = ({albums}) => { const albumsList = albums.map(album => ( <li key={album.id}>{album.title} <img src={album.artWorkUrl} /></li> )); return ( <ul> {albumsList} </ul> ) } const ArtistCard = ({bio, singles, albums}) => ( <div> <Bio bio={bio} /> <SingleList singles={singles} /> <AlbumList albums={albums} /> </div> ) const Artist = ({ id }) => { const [bio, setBio] = useState(null); const [singles, setSingles] = useState([]); const [albums, setAlbums] = useState([]); useEffect(()=> { fetch(`/artist/${id}/bio/`) .then(data => { setBio(data); }) }, [id]) useEffect(()=> { fetch(`/artist/${id}/singles/`) .then(data => { setSingles(data); }) }, [id]) useEffect(()=> { fetch(`/artist/${id}/albums/`) .then(data => { setAlbums(data); }) }, [id]) return <ArtistCard bio={bio} singles={singles} albums={albums} /> }
Now it means that we can reuse the UI in other places, for example we could use the ArtistCard
for a list of artists. We didn't need to recreate the same UI or handle any logic. It just works™️.
1const ArtistList = ({listOfArtists}) => { const artists = listOfArtists.map(({bio, singles, albums}) => ( <ArtistCard bio={bio} singles={singles} albums={albums} /> )); return <div>{artists}</div>; }
Because the components we have created are all pure (given the same input they will return the same value) this means that we gain 2 very useful benefits.
- We can optimise them easily with react
memo
since the same input returns the same output. - Testing is easy because, again, the output is always the same given the same input, and we don't need to depend on the data source, so we can supply our own data any way we want to mock it!
Summary
Single-Responsibility Principle is a great fundamental principle to learn and use because it makes your Classes, functions and components more reliable, reusable, optimisable and testable.
Just remember to:
- Check every function you create to see if there are any smaller parts that can be moved in to single use functions. 2.Check your functions to make sure they are not doing 2 or more different things.
- Repeat