Monday, April 10, 2017

SOLID Cheat Sheet

Full disclosure: I blatantly stole the code examples below from the terrific examples on the BlackWasp website.

Single Responsibility Principle (SRP)

  • A class should have only one reason to change.
  • Wrap all functionality in classes. Objects are classes, but also functions are classes.

Bad

This class has at least three distinct responsibilities.
public class OxygenMeter
{
    public double OxygenSaturation { get; set; }
    public void ReadOxygenLevel()
    {
        using (MeterStream ms = new MeterStream("O2"))
        {
            int raw = ms.ReadByte();
            OxygenSaturation = (double)raw / 255 * 100;
        }
    }
    public bool OxygenLow()
    {
        return OxygenSaturation <= 75;
    }
    public void ShowLowOxygenAlert()
    {
        Console.WriteLine("Oxygen low ({0:F1}%)", OxygenSaturation);
    }
}

Good

The refactored code is broken into one class per responsibility.
public class OxygenMeter
{
    public double OxygenSaturation { get; set; }
    public void ReadOxygenLevel()
    {
        using (MeterStream ms = new MeterStream("O2"))
        {
            int raw = ms.ReadByte();
            OxygenSaturation = (double)raw / 255 * 100;
        }
    }
}
 
public class OxygenSaturationChecker
{
    public bool OxygenLow(OxygenMeter meter)
    {
        return meter.OxygenSaturation <= 75;
    }
}
   
public class OxygenAlerter
{
    public void ShowLowOxygenAlert(OxygenMeter meter)
    {
        Console.WriteLine("Oxygen low ({0:F1}%)", meter.OxygenSaturation);
    }
}

My Thoughts

It feels like there is a tipping point here. It really depends on how you interpret the word responsibility. If you get too granular, you run the risk of having a zillion classes to maintain.

Open/Closed Principle (OCP)

  • A class should be open for extension but closed for modification.
  • Use interfaces and make polymorphic calls.
  • Instead of changing class, inherit from it and override methods.
  • Avoid switch statements that check an enumeration.

Bad

The Log() method in the class will need to be modified every time a type of log is added.
public class Logger
{
    public void Log(string message, LogType logType)
    {
        switch (logType) //BAD!!!
        {
            case LogType.Console:
                Console.WriteLine(message);
                break;
 
            case LogType.File:
                // Code to send message to printer
                break;
        }
    }
}

public enum LogType
{
    Console,
    File
}

Good

Instead of switching on an enumeration, the refactored code allows us to inject the log type into the class’s constructor and make polymorphic calls.
public class Logger
{
    IMessageLogger _messageLogger;
    public Logger(IMessageLogger messageLogger)
    {
        _messageLogger = messageLogger;
    }
 
    public void Log(string message)
    {
        _messageLogger.Log(message);
    }
}
 
public interface IMessageLogger
{
    void Log(string message);
}
 
public class ConsoleLogger : IMessageLogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}
 
public class PrinterLogger : IMessageLogger
{
    public void Log(string message)
    {
        // Code to send message to printer
    }
}

My Thoughts

By replacing a messy switch statement with polymorphism, we were able to future-proof the Log() method. The result is much cleaner.
The Open/Closed Principle (OCP) is similar to the Interface Segregation Principle (ISP) and Dependency Inversion Principle (DIP) in that it encourages operating on interfaces instead of classes. This puts less restriction on how we choose to modify our model class hierarchies and allows our functional classes to become more useful.

Liskov Substitution Principle (LSP)

  • Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
  • In other words, functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
  • You cannot substitute Square for Rectangle, even though it seems like it would be. This is because the dimensions of a square cannot (or, at least, should not) be modified independently.
  • Rules:
    • Contravariance of method arguments in subtype.
    • Covariance of return types in subtype.
    • Preconditions cannot be strengthened in a subtype.
    • Postconditions cannot be weakened in a subtype.
    • Cannot introduce new exception types.
    • You cannot make an immutable class mutable.

Bad

ReadOnlyFile inherits from ProjectFile, but throws a new Exception type. If it were truly replaceable, the conditional check in SaveAllFiles() wouldn’t be necessary. It also violates the post-condition that the file was actually saved to disk.
public class Project
{
    public Collection<ProjectFile> ProjectFiles { get; set; }
    public void LoadAllFiles()
    {
        foreach (ProjectFile file in ProjectFiles)
        {
            file.LoadFileData();
        }
    }
    public void SaveAllFiles()
    {
        foreach (ProjectFile file in ProjectFiles)
        {
            if (file as ReadOnlyFile == null) //BAD!!!
                file.SaveFileData();
        }
    }
}
 
public class ProjectFile
{
    public string FilePath { get; set; }
    public byte[] FileData { get; set; }
    public void LoadFileData() { ... }
    public virtual void SaveFileData() { ... }
}
 
public class ReadOnlyFile : ProjectFile
{
    public override void SaveFileData()
    {
        throw new InvalidOperationException();
    }
}

Good

The refactored code modifies the ProjectFile inheritance hierarchy. Additionally, the Project class maintains separate lists of files to operate on.
public class Project
{
    public Collection<ProjectFile> AllFiles { get; set; }
    public Collection<WriteableFile> WriteableFiles { get; set; }
    public void LoadAllFiles()
    {
        foreach (ProjectFile file in AllFiles)
        {
            file.LoadFileData();
        }
    }
    public void SaveAllWriteableFiles()
    {
        foreach (WriteableFile file in WriteableFiles)
        {
            file.SaveFileData();
        }
    }
}
 
public class ProjectFile
{
    public string FilePath { get; set; }
    public byte[] FileData { get; set; }
    public void LoadFileData() { ... }
}
 
public class WriteableFile : ProjectFile
{
    public void SaveFileData() { ... }
}

My Thoughts

Maintaining duplicate lists of data here is a bit awkward, but at least we were able to eliminate the ugly conditional check in SaveAllFiles().

Interface Segregation Principle (ISP)

  • Clients should not be forced to depend upon interfaces that they don’t use.

Bad

In the code below, the Emailer and Dialler classes can only operate on Contact, which contains members not necessary to their functionality.
public class Contact
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string EmailAddress { get; set; }
    public string Telephone { get; set; }
}
 
public class Emailer
{
    public void SendMessage(Contact contact, string subject, string body)
    {
        // Code to send email, using contact's email address and name
    }
}
     
public class Dialler
{
    public void MakeCall(Contact contact)
    {
        // Code to dial telephone number of contact
    }
}

Good

The refactored code modifies the Emailer and Dialler classes to operate on interfaces that are only concerned with the necessary members to perform the function.
public interface IEmailable
{
    string Name { get; set; }
    string EmailAddress { get; set; }
}
 
public interface IDiallable
{
    string Telephone { get; set; }
}
 
public class Contact : IEmailable, IDiallable
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string EmailAddress { get; set; }
    public string Telephone { get; set; }
}
 
public class MobileEngineer : IDiallable
{
    public string Name { get; set; }
    public string Vehicle { get; set; }
    public string Telephone { get; set; }
}
 
public class Emailer
{
    public void SendMessage(IEmailable target, string subject, string body)
    {
        // Code to send email, using target's email address and name
    }
}
 
public class Dialler
{
    public void MakeCall(IDiallable target)
    {
        // Code to dial telephone number of target
    }
}

My Thoughts

The Emailer and Dialler classes are now more useful to us, as they can operate on classes other than Contact. This type of refactoring could present us with a number of opportunities to prevent duplicated effort.
The Interface Segregation Principle (ISP) is similar to the Open/Closed Principle (OCP) and Dependency Inversion Principle (DIP) in that it encourages operating on interfaces instead of classes. This puts less restriction on how we choose to modify our model class hierarchies and allows our functional classes to become more useful.

Dependency Inversion Principal (DIP)

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend upon details. Details should depend upon abstractions.
  • Write against an interface instead of a concrete class.

Bad

The TransferManager can operate only on BankAccounts. If other Account types are added later, they will have to be subclasses of BankAccount.
public class BankAccount
{
    public string AccountNumber { get; set; }
    public decimal Balance { get; set; }
    public void AddFunds(decimal value)
    {
        Balance += value;
    }
    public void RemoveFunds(decimal value)
    {
        Balance -= value;
    }
}
 
public class TransferManager
{
    public BankAccount Source { get; set; }
    public BankAccount Destination { get; set; }
    public decimal Value { get; set; }
    public void Transfer()
    {
        Source.RemoveFunds(Value);
        Destination.AddFunds(Value);
    }
}

Good

The TransferManager class now operates on interfaces, which frees us up to transfer money between any type of account we happen to create in the future.
public interface ITransferSource
{
    void RemoveFunds(decimal value);
}
 
public interface ITransferDestination
{
    void AddFunds(decimal value);
}
 
public class BankAccount : ITransferSource, ITransferDestination
{
    public string AccountNumber { get; set; }
    public decimal Balance { get; set; }
    public void AddFunds(decimal value)
    {
        Balance += value;
    }
    public void RemoveFunds(decimal value)
    {
        Balance -= value;
    }
}
 
public class TransferManager
{
    public ITransferSource Source { get; set; }
    public ITransferDestination Destination { get; set; }
    public decimal Value { get; set; }
    public void Transfer()
    {
        Source.RemoveFunds(Value);
        Destination.AddFunds(Value);
    }
}

My Thoughts

The Dependency Inversion Principle (DIP) is similar to the Open/Closed Principle (OCP) and Interface Segregation Principle (ISP) in that it encourages operating on interfaces instead of classes. This puts less restriction on how we choose to modify our model class hierarchies and allows our functional classes to become more useful.

Conclusion

The five SOLID principles build on each other in interesting ways.
  • The SRP encourages us to have functional classes that operate on object classes within our model hierarchy.
  • The OCP, ISP, and DIP then encourage us to have our functional classes operate on interfaces rather than concrete classes, thus freeing us to structure our model hierarchies as we see fit.
  • The LSP encourages us to write our classes in a way that functionality in classes is not broken in subclasses.