Mastering SOLID Principles in C#

terrerox

Francis Terrero

Posted on July 7, 2024

Mastering SOLID Principles in C#

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.

notLikeUs

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.

public class Anime
{
    public string Title { get; set; }
    public string Genre { get; set; }
    public int NumberOfEpisodes { get; private set; }
    public string Studio { get; set; }
    public string Director { get; set; }
    public List<string> MainCharacters { get; private set; }
    public List<string> Episodes { get; private set; }

    public Anime(string title, string genre, string studio, string director)
    {
        Title = title;
        Genre = genre;
        Studio = studio;
        Director = director;
        MainCharacters = new List<string>();
        Episodes = new List<string>();
        NumberOfEpisodes = 0;
    }

    public void AddCharacter(string character)
    {
        MainCharacters.Add(character);
    }

    public void AddEpisode(string episode)
    {
        Episodes.Add(episode);
        NumberOfEpisodes++;
    }
}
Enter fullscreen mode Exit fullscreen mode

Look at this class. This class manages Episodes and Characters. These fields can be turned into classes easily.

Now let's split these properties by creating their own classes:

public class Character
{
    public string Name { get; set; }

    public Character(string name)
    {
        Name = name;
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"Character: {Name}");
    }
}

public class Episode
{
    public string Title { get; set; }

    public Episode(string title)
    {
        Title = title;
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"Episode: {Title}");
    }
}

public class Anime
{
    public string Title { get; set; }
    public string Genre { get; set; }
    public int NumberOfEpisodes { get { return Episodes.Count; } }
    public string Studio { get; set; }
    public string Director { get; set; }
    public List<Character> MainCharacters { get; private set; }
    public List<Episode> Episodes { get; private set; }

    public Anime(string title, string genre, string studio, string director)
    {
        Title = title;
        Genre = genre;
        Studio = studio;
        Director = director;
        MainCharacters = new List<Character>();
        Episodes = new List<Episode>();
    }

    public void AddCharacter(Character character)
    {
        MainCharacters.Add(character);
    }

    public void AddEpisode(Episode episode)
    {
        Episodes.Add(episode);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, there is a new Character and Episode class, separating the responsibility of the Anime class.

If you want to split the class more, you can create Studio, Director, and even Genre 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!

public class AnimeCharacter {

    // Create a goal for each character
    public string GokuGoal() { 
        return "Save the planet";
    }
    public string GojoGoal() {
        return "Defeat Sukuna";
    }
    public string GokuSpecialAttack() {
        return "Kamehameha!!";
    }
    public string GojoSpecialAttack() {
        return "Domain expansion";
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, this class has a Goal() and a SpecialAttack() for each character, meaning that if we want to add another Goal() or SpecialAttack(), you'll have to modify the class each time.

public abstract class AnimeCharacter
{
    //SpecialAttack() is now an abstract method
    public abstract string SpecialAttack(); 

    public virtual string Goal() {
        return "Save the planet";
    }
}

public class Goku : AnimeCharacter {

    // Can now override SpecialAttack() from AnimeCharacter
    public override string SpecialAttack() { 
        return "Kamehameha!!";
    }
}

public class Gojo : AnimeCharacter {

    public override string SpecialAttack() {
        return "Domain expansion";
    }

    public override string Goal() {
        return "Defeat Sukuna";
    }
}
Enter fullscreen mode Exit fullscreen mode

Apply inheritance and extend its functionality. Now, there are new Gojo and Goku classes that inherit from AnimeCharacter, overriding each implementation in its child class!

If you want to know more about abstraction and inheritance, I have a post 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.

public class StrawHat
{
    public virtual void Attack()
    {
        Console.WriteLine("StrawHat attacks.");
    }

    public virtual void Heal()
    {
        Console.WriteLine("StrawHat heals.");
    }
}

public class Chopper : StrawHat
{
    public override void Attack()
    {
        Console.WriteLine("Chopper attacks with his devil fruit power.");
    }

    public override void Heal()
    {
        Console.WriteLine("Chopper heals with his medical skills.");
    }
}

public class Usopp : StrawHat
{
    public override void Attack()
    {
        Console.WriteLine("Usopp attacks with his slingshot.");
    }

    public override void Heal()
    {
        // This will break the principle of LSP. Usopp cannot heal.
        throw new NotImplementedException("Usopp doesn't heal.");
    }
}
Enter fullscreen mode Exit fullscreen mode

All StrawHats but Chopper know nothing about medicine and healing, like Usopp, so you shouldn't add the method Heal in the parent class StrawHat.

Instead, you should split the skills by interfaces, like this:

public interface IAttacker
{
    void Attack();
}

public interface IHealer
{
    void Heal();
}

public class Chopper : IHealer, IAttacker
{
    public void Heal()
    {
        Console.WriteLine("Chopper heals with his medical skills.");
    }

    public void Attack()
    {
        Console.WriteLine("Chopper attacks with his devil fruit power.");
    }
}

public class Usopp : IAttacker
{
    public void Attack()
    {
        Console.WriteLine("Usopp attacks with his slingshot.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, the methods are separated by interfaces where you can implement the contract wherever you need it. This comes along with the next Interface 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 the Single Responsibility Principle.

public interface IAnimeCharacter
{
    string Name { get; set; }
    string Power { get; set; }
    void Display();
    void Attack();
    void Defend(); // Not all characters may need this method
}

public class Hero : IAnimeCharacter
{
    public string Name { get; set; }
    public string Power { get; set; }
    public string HeroicTitle { get; set; }

    public void Display()
    {
        Console.WriteLine($"Hero: {Name}, Title: {HeroicTitle}, Power: {Power}");
    }

    public void Attack()
    {
        Console.WriteLine($"Hero {Name} attacks heroically with {Power}!");
    }

    public void Defend()
    {
        Console.WriteLine($"Hero {Name} defends bravely!");
    }
}

public class Villain : IAnimeCharacter
{
    public string Name { get; set; }
    public string Power { get; set; }
    public string EvilPlan { get; set; }

    public void Display()
    {
        Console.WriteLine($"Villain: {Name}, Plan: {EvilPlan}, Power: {Power}");
    }

    public void Attack()
    {
        Console.WriteLine($"Villain {Name} attacks maliciously with {Power}!");
    }

    public void Defend()
    {
        // Villain may not have a defend action
        throw new NotImplementedException($"{Name} does not defend!");
    }
}
Enter fullscreen mode Exit fullscreen mode

The Villain class implements the interface IAnimeCharacter even when the class does not need the Defend() function.

public interface ICharacter
{
    string Name { get; set; }
    string Power { get; set; }
    void Display();
}

public interface IAttackable
{
    void Attack();
}

public interface IDefendable
{
    void Defend();
}

public class Hero : ICharacter, IAttackable, IDefendable
{
    public string Name { get; set; }
    public string Power { get; set; }
    public string HeroicTitle { get; set; }

    public void Display()
    {
        Console.WriteLine($"Hero: {Name}, Title: {HeroicTitle}, Power: {Power}");
    }

    public void Attack()
    {
        Console.WriteLine($"Hero {Name} attacks heroically with {Power}!");
    }

    public void Defend()
    {
        Console.WriteLine($"Hero {Name} defends bravely!");
    }
}

public class Villain : ICharacter, IAttackable


{
    public string Name { get; set; }
    public string Power { get; set; }
    public string EvilPlan { get; set; }

    public void Display()
    {
        Console.WriteLine($"Villain: {Name}, Plan: {EvilPlan}, Power: {Power}");
    }

    public void Attack()
    {
        Console.WriteLine($"Villain {Name} attacks maliciously with {Power}!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, there are three new interfaces: ICharacter, IAttackable, and IDefendable, where you can use them where you really need them.

If you don't apply the Single Responsibility Principle, the Interface 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.

public class Hero
{
    public string Name { get; set; }
    public string Power { get; set; }
    public string HeroicTitle { get; set; }

    public void Display()
    {
        Console.WriteLine($"Hero: {Name}, Title: {HeroicTitle}, Power: {Power}");
    }

    public void Attack()
    {
        Console.WriteLine($"Hero {Name} attacks heroically with {Power}!");
    }

    public void Defend()
    {
        Console.WriteLine($"Hero {Name} defends bravely!");
    }
}

public class Villain
{
    public string Name { get; set; }
    public string Power { get; set; }
    public string EvilPlan { get; set; }

    public void Display()
    {
        Console.WriteLine($"Villain: {Name}, Plan: {EvilPlan}, Power: {Power}");
    }

    public void Attack()
    {
        Console.WriteLine($"Villain {Name} attacks maliciously with {Power}!");
    }
}

public class AnimeBattle
{
    private readonly Hero _hero;
    private readonly Villain _villain;

    public AnimeBattle(Hero hero, Villain villain)
    {
        _hero = hero;
        _villain = villain;
    }

    public void StartBattle()
    {
        _hero.Display();
        _villain.Display();
        _hero.Attack();
        _villain.Attack();
        _hero.Defend();
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, I'm implementing Hero and Villain classes directly. Any change in either the Hero class or the Villain class (low-level) can break the AnimeBattle class (high-level).

public interface IAnimeCharacter
{
    string Name { get; set; }
    string Power { get; set; }
    void Display();
    void Attack();
}

public interface IHero : IAnimeCharacter
{
    string HeroicTitle { get; set; }
    void Defend();
}

public interface IVillain : IAnimeCharacter
{
    string EvilPlan { get; set; }
}

public class Hero : IHero
{
    public string Name { get; set; }
    public string Power { get; set; }
    public string HeroicTitle { get; set; }

    public void Display()
    {
        Console.WriteLine($"Hero: {Name}, Title: {HeroicTitle}, Power: {Power}");
    }

    public void Attack()
    {
        Console.WriteLine($"Hero {Name} attacks heroically with {Power}!");
    }

    public void Defend()
    {
        Console.WriteLine($"Hero {Name} defends bravely!");
    }
}

public class Villain : IVillain
{
    public string Name { get; set; }
    public string Power { get; set; }
    public string EvilPlan { get; set; }

    public void Display()
    {
        Console.WriteLine($"Villain: {Name}, Plan: {EvilPlan}, Power: {Power}");
    }

    public void Attack()
    {
        Console.WriteLine($"Villain {Name} attacks maliciously with {Power}!");
    }
}

public class AnimeBattle
{
    private readonly IHero _hero;
    private readonly IVillain _villain;

    public AnimeBattle(IHero hero, IVillain villain)
    {
        _hero = hero;
        _villain = villain;
    }

    public void StartBattle()
    {
        _hero.Display();
        _villain.Display();
        _hero.Attack();
        _villain.Attack();
        _hero.Defend();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, any class that implements IHero or IVillain can be used in the AnimeBattle class, providing a flexible way to have different implementations and taking advantage of polymorphism.

Here's the source code if you want to play with it!

Happy coding!

notLikeUs

💖 💪 🙅 🚩
terrerox
Francis Terrero

Posted on July 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Mastering SOLID Principles in C#
programming Mastering SOLID Principles in C#

July 7, 2024