6 min read

SOLID is an acronym for the first five object-oriented design (OOD) principles by Robert C. Martin

What is SOLID?

SOLID is an acronym for the 5 basic principles of good software architecture:

Letter Abbr Name Basic Description
S SRP Single Responsibility Principle A class should have only a single responsibility, that is, it only has 1 specific task to fulfill.
O OCP Open / Closed Principle A class that needs a new behavior should be extended and not modified.
L LSP Liskov Substitution Principle A class should be replaceable by a subclass that can change how a task is completed but it doesn’t damage the operation of the dependent classes. In other words, a parent class should be able to refer to child objects seamlessly during runtime via polymorphism.
I ISP Interface Segregation Principle Instead of one huge multi-purpose interface one should have multiple specific interfaces. A client should not be forced to code for an interface they do not need.
D DIP Dependency Inversion Principle Implementation should depend on abstraction rather than some concrete solution. High level objects should not have to depend on low level objects; they only need to know the feature is implemented by something.

Polymorphism allows the expression of some sort of contract, with potentially many types implementing that contract (whether through class inheritance or not) in different ways, each according to their own purpose. Code using that contract should not have to care about which implementation is involved, only that the contract will be obeyed.

Single Responsibility Principle

SRP keeps classes simple, easy to maintain and minifies the effects of any minor changes. A class should only have one responsibility and not multiple. This can sound confusing at first; someone will likely bring up something like a view model or controller but that, in itself, has a single responsibility to manage other classes that themselves have a very simple single responsibility.

Broken - Writing to logfile is not job of Aggregator class. ___

class Aggregator
{
  public void Add(Entity entity)
  {
    try
    {
      // Add entity to aggregator.
    }
    catch (Exception ex)
    {
       System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
    }
  }
}

Fixed ___

class Aggregator
{
  private Logger log = new Logger();
  public void Add(Entity entity)
  {
    try
    {
      // Add entity to aggregator.
    }
    catch (Exception ex)
    {
      log.Error(ex.ToString());
    }
  }
}

class Logger
{
  public void Error(string error)
  {
    System.IO.File.WriteAllText(@"c:\Error.txt", error);
  }
}

Open / Closed Principle

Again, OCP keeps classes simple, easy to maintain and minifies the impacts of changes to the code. OCP means that the class should not be modified (i.e. it is closed) but it can be extended (i.e. it is open). A developer will often say, “Hey, I need to add this new capability to this class for this specific case.” That modification can be reckless. One should keep the original class intact so the code relying on that original class isn’t broken. To add that new functionality the class basically has a new task and thus needs to change; that change should be an extension via inheritance.

Broken ___

class Drone
{
  public int DroneType {get; set; }
  public DateTime ArrivalTime(double kilometers)
  {
    if (DroneType == 0)
    {
      return DateTime.Now.AddHours(kilometers);
    }
    else if (DroneType == 1)
    {
      return DateTime.Now.AddHours(kilometers / 2.0);
    }
    else if (DroneType == 2)
    {
      return DateTime.Now.AddHours(kilometers / 32.0);
    }
  }
}

Fixed (Polymorphism Inheritance) ___

class Drone
{
  public virtual DateTime ArrivalTime(double kilometers)
  {
    return DateTime.Now.AddHours(kilometers);
  }
}

class SpyDrone : Drone
{
  public override DateTime ArrivalTime(double kilometers)
  {
    return base.ArrivalTime(kilometers / 2.0);
  }
}

class AttackDrone : Drone
{
  public override DateTime ArrivalTime(double kilometers)
  {
    return base.ArrivalTime(kilometers / 32.0);
  }
}

Liskov Substitution Principle

LSP keeps the overall program working as it was designed though a backing class might have been swapped out for another implementation. A good example of LSP in use is the Mock object often used in Unit Testing. The LSP object can be based off of interfaces or an abstract class as long as the object instance doesn’t break the calling code.

Example - Substituting a logger class doesn’t break Aggregator ___

class Aggregator
{
  // private ILogger log = new FileLogger();
  private ILogger log = new ConsoleLogger();
  public void Add(Entity entity)
  {
    try
    {
      // Add entity to aggregator.
    }
    catch (Exception ex)
    {
      log.Error(ex.ToString());
    }
  }
}

interface ILogger
{
  void Error(string message);
}

class FileLogger : ILogger
{
  public void Error(string error)
  {
    System.IO.File.WriteAllText(@"c:\Error.txt", error);
  }
}

class ConsoleLogger : ILogger
{
  public void Error(string error)
  {
    System.Console.Error.WriteLine(error);
  }
}

Interface Segregation Principle

ISP helps protect clients from developers damaging interfaces which are normally “contracts” with other clients. Interfaces used by other clients, including actual people or classes throughout your code repositories, should not change because that change could break the client’s software implementation. There is also the tendency to put too much stuff into an interface which can almost be seen as the same as violating the Single Responsibility Principle though from an interface perspective. Requirements matter and sometimes it is a good idea to break up a proposed interface to provide more flexibility.

Original ___

interface IDatabase
{
  void Add(Entity entity);
}

class Client: IDatabase
{
  public void Add(Entity entity)
  {
    // Add entity to database.
  }
}

Broken ___

interface IDatabase
{
  void Add(Entity entity);
  Entity Read();
}

// Broken!
class Client: IDatabase
{
  public void Add(Entity entity)
  {
    // Add entity to database.
  }
}

Fixed ___

interface IDatabase
{
  void Add(Entity entity);
}

interface IDatabaseV1 : IDatabase
{
  // Has Add Method.
  Entity Read();
}

class ClientWithRead: IDatabaseV1, IDatabase
{
  public void Add(Entity entity)
  {
    Client obj = new Client();
    obj.Add(entity);
  }

  public Entity Read()
  {
    // Read from database.
  }
}

class Client: IDatabase
{
  public void Add(Entity entity)
  {
    // Add entity to database.
  }
}

// Results...
IDatabase a = new Client(); // Previous clients happy!
a.Add(entity);
IDatabaseV1 b = new ClientWithRead(); // New client requirements.
b.Add(entity);
entity = b.Read();

Dependency Inversion Principle

Instead of hard coding a class to determine what component should be used internally flexibility should be built in. With DIP we want to invert, or delegate, the responsibility of that swappable component used internally. As an example, instead of using a bunch of IF statements to choose your logging mechanism (that’s a no), or some flag in your logging class (that’s a no) or just becoming inflexible and not allowing for this kind of change one should use Dependency Inversion.

Broke 1 ___

class Aggregator
{
  private Logger log = new Logger();
  public void Add(Entity entity, bool logToFile = true)
  {
    try
    {
      // Add entity to aggregator.
    }
    catch (Exception ex)
    {
      log.LogToFile = logToFile;
      log.Error(ex.ToString());
    }
  }
}

class Logger
{
  public bool LogToFile { get; set; } = true;
  public void Error(string error)
  {
    if (LogToFile)
    {
      System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
    else
    {
      System.Console.Error.WriteLine(error);
    }
  }
}

Broke 2 ___

class Aggregator
{
  private ILogger log1 = new FileLogger();
  private ILogger log2 = new ConsoleLogger();
  public void Add(Entity entity, bool logToFile = true)
  {
    try
    {
      // Add entity to aggregator.
    }
    catch (Exception ex)
    {
      if (logToFile)
        log1.Error(ex.ToString());
      else
        log2.Error(ex.ToString());
    }
  }
}

interface ILogger
{
  void Error(string message);
}

class FileLogger : ILogger
{
  public void Error(string error)
  {
    System.IO.File.WriteAllText(@"c:\Error.txt", error);
  }
}

class ConsoleLogger : ILogger
{
  public void Error(string error)
  {
    System.Console.Error.WriteLine(error);
  }
}

Fixed ___

class Aggregator
{
  private ILogger log;
  public Aggregator(ILogger logger)
  {
    log = logger;
  }

  public void Add(Entity entity, bool logToFile = true)
  {
    try
    {
      // Add entity to aggregator.
    }
    catch (Exception ex)
    {
      log?.Error(ex.ToString());
    }
  }
}

interface ILogger
{
  void Error(string message);
}

class FileLogger : ILogger
{
  public void Error(string error)
  {
    System.IO.File.WriteAllText(@"c:\Error.txt", error);
  }
}

class ConsoleLogger : ILogger
{
  public void Error(string error)
  {
    System.Console.Error.WriteLine(error);
  }
}

// Usage:
Aggregator agg1 = new Aggregator(new FileLogger());
Aggregator agg2 = new Aggregator(new ConsoleLogger());