As a backend developer, there are some concepts that you must know. If you want a job in this industry, the SOLID principles are a set of guidelines that have stood the test of time and are crucial to understand. They are also common interview topics that you must answer correctly if you want to pass.
I'm going to explain all the principles to you in the easiest way I found, using anime-related examples.
Single Responsibility Principle
A class should have only one reason to change
Not only your class; your functions and modules should have one responsibility. This means that if you have a big function that does a lot of things, you should probably separate it into smaller functions.
publicclassAnime{publicstringTitle{get;set;}publicstringGenre{get;set;}publicintNumberOfEpisodes{get;privateset;}publicstringStudio{get;set;}publicstringDirector{get;set;}publicList<string>MainCharacters{get;privateset;}publicList<string>Episodes{get;privateset;}publicAnime(stringtitle,stringgenre,stringstudio,stringdirector){Title=title;Genre=genre;Studio=studio;Director=director;MainCharacters=newList<string>();Episodes=newList<string>();NumberOfEpisodes=0;}publicvoidAddCharacter(stringcharacter){MainCharacters.Add(character);}publicvoidAddEpisode(stringepisode){Episodes.Add(episode);NumberOfEpisodes++;}}
Look at this class. This class managesEpisodes
andCharacters
. These fields can be turned into classes easily.
Now let's split these properties by creating their own classes:
publicclassCharacter{publicstringName{get;set;}publicCharacter(stringname){Name=name;}publicvoidDisplayInfo(){Console.WriteLine($"Character:{Name}");}}publicclassEpisode{publicstringTitle{get;set;}publicEpisode(stringtitle){Title=title;}publicvoidDisplayInfo(){Console.WriteLine($"Episode:{Title}");}}publicclassAnime{publicstringTitle{get;set;}publicstringGenre{get;set;}publicintNumberOfEpisodes{get{returnEpisodes.Count;}}publicstringStudio{get;set;}publicstringDirector{get;set;}publicList<Character>MainCharacters{get;privateset;}publicList<Episode>Episodes{get;privateset;}publicAnime(stringtitle,stringgenre,stringstudio,stringdirector){Title=title;Genre=genre;Studio=studio;Director=director;MainCharacters=newList<Character>();Episodes=newList<Episode>();}publicvoidAddCharacter(Charactercharacter){MainCharacters.Add(character);}publicvoidAddEpisode(Episodeepisode){Episodes.Add(episode);}}
Here, there is a newCharacter
andEpisode
class, separating the responsibility of theAnime
class.
If you want to split the class more, you can create
Studio
,Director
, and evenGenre
classes too!
Open-Closed Principle
Software entities (classes, modules, functions) should be open for extension, but closed for modification
You should not modify the class if you want to add new features!
publicclassAnimeCharacter{// Create a goal for each characterpublicstringGokuGoal(){return"Save the planet";}publicstringGojoGoal(){return"Defeat Sukuna";}publicstringGokuSpecialAttack(){return"Kamehameha!!";}publicstringGojoSpecialAttack(){return"Domain expansion";}}
Here, this class has aGoal()
and aSpecialAttack()
for each character, meaning that if we want to add anotherGoal()
orSpecialAttack()
, you'll have to modify the class each time.
publicabstractclassAnimeCharacter{//SpecialAttack() is now an abstract methodpublicabstractstringSpecialAttack();publicvirtualstringGoal(){return"Save the planet";}}publicclassGoku:AnimeCharacter{// Can now override SpecialAttack() from AnimeCharacterpublicoverridestringSpecialAttack(){return"Kamehameha!!";}}publicclassGojo:AnimeCharacter{publicoverridestringSpecialAttack(){return"Domain expansion";}publicoverridestringGoal(){return"Defeat Sukuna";}}
Apply inheritance and extend its functionality. Now, there are newGojo
andGoku
classes that inherit fromAnimeCharacter
, overriding each implementation in its child class!
If you want to know more about abstraction and inheritance, I have apost prepared for you!
Liskov Substitution Principle
Derived or child classes must be substitutable for their base or parent classes
The methods of the parent class should be replaceable by methods of the child class.
publicclassStrawHat{publicvirtualvoidAttack(){Console.WriteLine("StrawHat attacks.");}publicvirtualvoidHeal(){Console.WriteLine("StrawHat heals.");}}publicclassChopper:StrawHat{publicoverridevoidAttack(){Console.WriteLine("Chopper attacks with his devil fruit power.");}publicoverridevoidHeal(){Console.WriteLine("Chopper heals with his medical skills.");}}publicclassUsopp:StrawHat{publicoverridevoidAttack(){Console.WriteLine("Usopp attacks with his slingshot.");}publicoverridevoidHeal(){// This will break the principle of LSP. Usopp cannot heal.thrownewNotImplementedException("Usopp doesn't heal.");}}
All StrawHats butChopper
know nothing about medicine and healing, likeUsopp
, so you shouldn't add the methodHeal
in the parent classStrawHat
.
Instead, you should split the skills by interfaces, like this:
publicinterfaceIAttacker{voidAttack();}publicinterfaceIHealer{voidHeal();}publicclassChopper:IHealer,IAttacker{publicvoidHeal(){Console.WriteLine("Chopper heals with his medical skills.");}publicvoidAttack(){Console.WriteLine("Chopper attacks with his devil fruit power.");}}publicclassUsopp:IAttacker{publicvoidAttack(){Console.WriteLine("Usopp attacks with his slingshot.");}}
Now, the methods are separated by interfaces where you can implement the contract wherever you need it. This comes along with the nextInterface Segregation Principle.
Interface Segregation Principle
Do not force any client to implement an interface which is irrelevant to them
In simple words: do not create a "super" interface. This principle comes along with theSingle Responsibility Principle.
publicinterfaceIAnimeCharacter{stringName{get;set;}stringPower{get;set;}voidDisplay();voidAttack();voidDefend();// Not all characters may need this method}publicclassHero:IAnimeCharacter{publicstringName{get;set;}publicstringPower{get;set;}publicstringHeroicTitle{get;set;}publicvoidDisplay(){Console.WriteLine($"Hero:{Name}, Title:{HeroicTitle}, Power:{Power}");}publicvoidAttack(){Console.WriteLine($"Hero{Name} attacks heroically with{Power}!");}publicvoidDefend(){Console.WriteLine($"Hero{Name} defends bravely!");}}publicclassVillain:IAnimeCharacter{publicstringName{get;set;}publicstringPower{get;set;}publicstringEvilPlan{get;set;}publicvoidDisplay(){Console.WriteLine($"Villain:{Name}, Plan:{EvilPlan}, Power:{Power}");}publicvoidAttack(){Console.WriteLine($"Villain{Name} attacks maliciously with{Power}!");}publicvoidDefend(){// Villain may not have a defend actionthrownewNotImplementedException($"{Name} does not defend!");}}
TheVillain
class implements the interfaceIAnimeCharacter
even when the class does not need theDefend()
function.
publicinterfaceICharacter{stringName{get;set;}stringPower{get;set;}voidDisplay();}publicinterfaceIAttackable{voidAttack();}publicinterfaceIDefendable{voidDefend();}publicclassHero:ICharacter,IAttackable,IDefendable{publicstringName{get;set;}publicstringPower{get;set;}publicstringHeroicTitle{get;set;}publicvoidDisplay(){Console.WriteLine($"Hero:{Name}, Title:{HeroicTitle}, Power:{Power}");}publicvoidAttack(){Console.WriteLine($"Hero{Name} attacks heroically with{Power}!");}publicvoidDefend(){Console.WriteLine($"Hero{Name} defends bravely!");}}publicclassVillain:ICharacter,IAttackable{publicstringName{get;set;}publicstringPower{get;set;}publicstringEvilPlan{get;set;}publicvoidDisplay(){Console.WriteLine($"Villain:{Name}, Plan:{EvilPlan}, Power:{Power}");}publicvoidAttack(){Console.WriteLine($"Villain{Name} attacks maliciously with{Power}!");}}
Now, there are three new interfaces:ICharacter
,IAttackable
, andIDefendable
, where you can use them where you really need them.
If you don't apply theSingle Responsibility Principle, theInterface Segregation Principle won't be effective either.
Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
This is very useful because you will be able to change low-level implementations without affecting high-level modules, making high-level modules independent of low-level modules.
publicclassHero{publicstringName{get;set;}publicstringPower{get;set;}publicstringHeroicTitle{get;set;}publicvoidDisplay(){Console.WriteLine($"Hero:{Name}, Title:{HeroicTitle}, Power:{Power}");}publicvoidAttack(){Console.WriteLine($"Hero{Name} attacks heroically with{Power}!");}publicvoidDefend(){Console.WriteLine($"Hero{Name} defends bravely!");}}publicclassVillain{publicstringName{get;set;}publicstringPower{get;set;}publicstringEvilPlan{get;set;}publicvoidDisplay(){Console.WriteLine($"Villain:{Name}, Plan:{EvilPlan}, Power:{Power}");}publicvoidAttack(){Console.WriteLine($"Villain{Name} attacks maliciously with{Power}!");}}publicclassAnimeBattle{privatereadonlyHero_hero;privatereadonlyVillain_villain;publicAnimeBattle(Herohero,Villainvillain){_hero=hero;_villain=villain;}publicvoidStartBattle(){_hero.Display();_villain.Display();_hero.Attack();_villain.Attack();_hero.Defend();}}
Here, I'm implementingHero
andVillain
classes directly. Any change in either theHero
class or theVillain
class (low-level) can break theAnimeBattle
class (high-level).
publicinterfaceIAnimeCharacter{stringName{get;set;}stringPower{get;set;}voidDisplay();voidAttack();}publicinterfaceIHero:IAnimeCharacter{stringHeroicTitle{get;set;}voidDefend();}publicinterfaceIVillain:IAnimeCharacter{stringEvilPlan{get;set;}}publicclassHero:IHero{publicstringName{get;set;}publicstringPower{get;set;}publicstringHeroicTitle{get;set;}publicvoidDisplay(){Console.WriteLine($"Hero:{Name}, Title:{HeroicTitle}, Power:{Power}");}publicvoidAttack(){Console.WriteLine($"Hero{Name} attacks heroically with{Power}!");}publicvoidDefend(){Console.WriteLine($"Hero{Name} defends bravely!");}}publicclassVillain:IVillain{publicstringName{get;set;}publicstringPower{get;set;}publicstringEvilPlan{get;set;}publicvoidDisplay(){Console.WriteLine($"Villain:{Name}, Plan:{EvilPlan}, Power:{Power}");}publicvoidAttack(){Console.WriteLine($"Villain{Name} attacks maliciously with{Power}!");}}publicclassAnimeBattle{privatereadonlyIHero_hero;privatereadonlyIVillain_villain;publicAnimeBattle(IHerohero,IVillainvillain){_hero=hero;_villain=villain;}publicvoidStartBattle(){_hero.Display();_villain.Display();_hero.Attack();_villain.Attack();_hero.Defend();}}
Now, any class that implementsIHero
orIVillain
can be used in theAnimeBattle
class, providing a flexible way to have different implementations and taking advantage of polymorphism.
Here's thesource code if you want to play with it!
Happy coding!
Top comments(2)

- LocationBerlin
- WorkSoftware Engineering Lead at Solsten
- Joined
Great write-up! Also wrote some thoughts about it but in context of Go. Although Golang is not a purely object-oriented language, we can still apply SOLID principles to improve our Go code -packagemain.tech/p/mastering-solid...

- LocationDomincan Republic
- PronounsHe/Him
- Joined
Good article! I'm looking forward to learning Go. Your article will be a great help!
For further actions, you may consider blocking this person and/orreporting abuse