SOLID Principles cheat sheet in C#
A summary of SOLID Principles as implemented in the C# language.
Useful link: https://code-maze.com/solid-principles/
1. Single Responsibility Principle
Every class should have only one responsibility. Consider the class PersonalDetails whose responsibility is to hold details about a person:
[code language="java"]
public class PersonDetails
{
public string Name { get; set; }
public int Age { get; set; }
public string Identifier { get; set; }
public void PrintReport()
{
Console.WriteLine("Name: " + Name);
Console.WriteLine("Age: " + Age.ToString());
Console.WriteLine("Identifier: " + Identifier);
}
}
[/code]
After instantiating, we then use a public method to print out the details of that person:
[code language="java"]
PersonDetails person1 = new PersonDetails
{
Name = "Andrew",
Age = 36,
Identifier = "MDJW0031"
};
person1.PrintReport();
[/code]
The problem is that this class has more than one responsibility. It is not just holding details of a person but printing it too.
More potential complexities will ensue if it is decided we want to add other unrelated functionality such save the person to disk too.
We overcome this by separating unrelated code.
[code language="java"]
public class PersonDetails
{
public string Name { get; set; }
public int Age { get; set; }
public string Identifier { get; set; }
}
[/code]
And creating a new class to handle the responsibility of printing:
[code language="java"]
public class Printer
{
public void Print(PersonDetails personDetails)
{
Console.WriteLine("Name: " + personDetails.Name);
Console.WriteLine("Age: " + personDetails.Age.ToString());
Console.WriteLine("Identifier: " + personDetails.Identifier);
}
}
[/code]
Implemented as follows:
[code language="java"]
Printer printer = new Printer();
printer.Print(new PersonDetails
{
Name = "Andrew",
Age = 36,
Identifier = "MDJW0031"
});
[/code]
2. Open-closed Principle
This pattern is useful for lowering the chance of bugs, and means "Open for extension, closed for modification"
In other words, it should be easy to extend the functionality of classes without having to modify them.
Consider the following example which deals with the filtering of computer monitors by type and screen:
[code language="java"]
public enum MonitorType
{
OLED,
LCD,
LED
}
[/code]
[code language="java"]
public enum Screen
{
WideScreen,
CurvedScreen
}
[/code]
[code language="java"]
public class ComputerMonitor
{
public string Name { get; set; }
public MonitorType Type { get; set; }
public Screen Screen { get; set; }
}
[/code]
[code language="java"]
public class MonitorFilter
{
public List<ComputerMonitor> FilterByType(IEnumerable<ComputerMonitor> monitors,
MonitorType type) =>
monitors.Where(m => m.Type == type).ToList();
}
[/code]
[code language="java"]
var monitors = new List<ComputerMonitor>
{
new ComputerMonitor { Name = "Samsung S345", Screen = Screen.CurvedScreen, Type = MonitorType.OLED },
new ComputerMonitor { Name = "Philips P532", Screen = Screen.WideScreen, Type = MonitorType.LCD },
new ComputerMonitor { Name = "LG L888", Screen = Screen.WideScreen, Type = MonitorType.LED },
new ComputerMonitor { Name = "Samsung S999", Screen = Screen.WideScreen, Type = MonitorType.OLED },
new ComputerMonitor { Name = "Dell D2J47", Screen = Screen.CurvedScreen, Type = MonitorType.LCD }
};
var filter = new MonitorFilter();
var lcdMonitors = filter.FilterByType(monitors, MonitorType.LCD);
Console.WriteLine("All LCD monitors");
foreach (var monitor in lcdMonitors)
{
Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}
[/code]
Change MonitorFilter class:
[code language="java"]
public class MonitorFilter
{
public List<ComputerMonitor> FilterByType(IEnumerable<ComputerMonitor> monitors, MonitorType type) =>
monitors.Where(m => m.Type == type).ToList();
public List<ComputerMonitor> FilterByScreen(IEnumerable<ComputerMonitor> monitors, Screen screen) =>
monitors.Where(m => m.Screen == screen).ToList();
}
[/code]
This gives the desired result but violates the open-closed principle. The solution is to use interfaces:
[code language="java"]
public interface ISpecification<T>
{
bool isSatisfied(T item);
}
public interface IFilter<T>
{
List<T> Filter(IEnumerable<T> monitors, ISpecification<T> specification);
}
[/code]
Create a separate class for the monitor type specification to implement ISpecification:
[code language="java"]
public class MonitorTypeSpecification : ISpecification<ComputerMonitor>
{
private readonly MonitorType _type;
public MonitorTypeSpecification(MonitorType type)
{
_type = type;
}
public bool isSatisfied(ComputerMonitor item) => item.Type == _type;
}
[/code]
Modify the existing MonitorFilter class by getting it to implement the Ifilter interface:
[code language="java"]
public class MonitorFilter : IFilter<ComputerMonitor>
{
public List<ComputerMonitor> Filter(IEnumerable<ComputerMonitor> monitors, ISpecification<ComputerMonitor> specification) =>
monitors.Where(m => specification.isSatisfied(m)).ToList();
}
[/code]
On implementing this in the program, the result should be the same:
[code language="java"]
var filter = new MonitorFilter();
var lcdMonitors = filter.Filter(monitors, new MonitorTypeSpecification(MonitorType.LCD));
Console.WriteLine("All LCD monitors");
foreach (var monitor in lcdMonitors)
{
Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}
[/code]
So it is now possible to make further extensions to our MonitoFilter class without actually modifiying the class itself. If we want to filter by screen type, create a new screen specification class that implements the Ispecification interface, and use that:
[code language="java"]
public class ScreenTypeSpecification : ISpecification<ComputerMonitor>
{
private readonly Screen _screen;
public ScreenTypeSpecification(Screen screen)
{
_screen = screen;
}
public bool isSatisfied(ComputerMonitor item) => item.Screen == _screen;
}
[/code]
And make use of this in the main code:
[code language="java"]
var filter = new MonitorFilter();
var wideScreenMonitors = filter.Filter(monitors, new ScreenSpecification(Screen.WideScreen));
foreach (var monitor in wideScreenMonitors)
{
Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}
[/code]
3. Liskov Substitution Principle
A form of polymorphism. Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. Derived class objects should be able to replace base class objects without modifying it’s behaviour:
Example classes to calculate the sum of all numbers or just even numbers.
[code language="java"]
public class SumCalculator
{
protected readonly int[] _numbers;
public SumCalculator(int[] numbers)
{
_numbers = numbers;
}
public int Calculate() => _numbers.Sum();
}
[/code]
[code language="java"]
public class EvenNumbersSumCalculator : SumCalculator
{
public EvenNumbersSumCalculator(int[] numbers)
: base(numbers)
{
}
public new int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}
[/code]
[code language="java"]
var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
SumCalculator sum = new SumCalculator(numbers);
Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}");
Console.WriteLine();
EvenNumbersSumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
[/code]
Works fine. But of we try this:
[code language="java"]
var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
[/code]
We don’t get the result we desire:
We don’t get the expected result because our variable evenSum is of type SumCalculator which is the base class. This means that the Count method from the SumCalculator class will be executed, not the EvenNumbersSumCalculator class. Therefore our child class is not behaving as a substitute for the parent class, thereby violating the Liskov substitution principle.
We overcome this by using abstract base classes:
[code language="java"]
public abstract class Calculator
{
protected readonly int[] _numbers;
public Calculator(int[] numbers)
{
_numbers = numbers;
}
public abstract int Calculate();
}
[/code]
[code language="java"]
public class SumCalculator : Calculator
{
public SumCalculator(int[] numbers) : base(numbers) { }
public override int Calculate() => _numbers.Sum();
}
[/code]
[code language="java"]
public class EvenNumbersSumCalculator : Calculator
{
public EvenNumbersSumCalculator(int[] numbers) : base(numbers) {}
public override int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}
[/code]
[code language="java"]
var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
Calculator sum = new SumCalculator(numbers);
Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}");
Console.WriteLine();
Calculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
[/code]
4. Interface-segregation principle
This states that no client should be forced to depend on methods it does not need or should not use . Many client-specific interfaces can work better than one general-purpose interface.
For example, to develop a behavior for a multifunctional car , there are vehicles that we can only drive and there are vehicles that we can only fly. But there are vehicles we can both drive and fly. We would like to create a class which supports all these actions for a vehicle.
[code language="java"]
public interface IVehicle
{
void Drive();
void Fly();
}
[/code]
[code language="java"]
public class MultiFunctionalCar : IVehicle
{
public void Drive()
{
Console.WriteLine("Drive a multifunctional car");
}
public void Fly()
{
Console.WriteLine("Fly a multifunctional car");
}
}
[/code]
This covers the requirements for a multi-functional car. However if we want to develop classes for drive-only and fly-only vehicles, we see inefficiencies:
[code language="java"]
public class Car : IVehicle
{
public void Drive()
{
Console.WriteLine("Driving a car");
}
public void Fly()
{
throw new NotImplementedException();
}
}
[/code]
[code language="java"]
public class Airplane : IVehicle
{
public void Drive()
{
throw new NotImplementedException();
}
public void Fly()
{
Console.WriteLine("Flying a plane");
}
}
[/code]
Each class only contains one required declaration: the unwanted one is made to throw an exception, which is wasteful.
The solution is to split the IVehicle interface into separate Icar and IAircraft interfaces:
[code language="java"]
public interface ICar
{
void Drive();
}
[/code]
[code language="java"]
public interface IAirplane
{
void Fly();
}
[/code]
Resulting in classes that only implement the methods they need:
[code language="java"]
public class Car : ICar
{
public void Drive()
{
Console.WriteLine("Driving a car");
}
}
[/code]
[code language="java"]
public class Airplane : IAirplane
{
public void Fly()
{
Console.WriteLine("Flying a plane");
}
}
[/code]
[code language="java"]
public class MultiFunctionalCar : ICar, IAirplane
{
public void Drive()
{
Console.WriteLine("Drive a multifunctional car");
}
public void Fly()
{
Console.WriteLine("Fly a multifunctional car");
}
}
[/code]
5. Dependency Inversion Principle
These depend upon abstractions, [not] concretions
Dependency Inversion Principle is a means of creating logic for higher-level modules in such a way to be reusable and unaffected by any changes from the lower level modules in the application.
Having this idea in mind the Dependency Inversion Principle states that
The problem is that this class has more than one responsibility. It is not just holding details of a person but printing it too.
More potential complexities will ensue if it is decided we want to add other unrelated functionality such save the person to disk too.
We overcome this by separating unrelated code.
[code language="java"]
public class PersonDetails
{
public string Name { get; set; }
public int Age { get; set; }
public string Identifier { get; set; }
}
[/code]
And creating a new class to handle the responsibility of printing:
[code language="java"]
public class Printer
{
public void Print(PersonDetails personDetails)
{
Console.WriteLine("Name: " + personDetails.Name);
Console.WriteLine("Age: " + personDetails.Age.ToString());
Console.WriteLine("Identifier: " + personDetails.Identifier);
}
}
[/code]
Implemented as follows:
[code language="java"]
Printer printer = new Printer();
printer.Print(new PersonDetails
{
Name = "Andrew",
Age = 36,
Identifier = "MDJW0031"
});
[/code]
2. Open-closed Principle
This pattern is useful for lowering the chance of bugs, and means "Open for extension, closed for modification"
In other words, it should be easy to extend the functionality of classes without having to modify them.
Consider the following example which deals with the filtering of computer monitors by type and screen:
[code language="java"]
public enum MonitorType
{
OLED,
LCD,
LED
}
[/code]
[code language="java"]
public enum Screen
{
WideScreen,
CurvedScreen
}
[/code]
[code language="java"]
public class ComputerMonitor
{
public string Name { get; set; }
public MonitorType Type { get; set; }
public Screen Screen { get; set; }
}
[/code]
[code language="java"]
public class MonitorFilter
{
public List<ComputerMonitor> FilterByType(IEnumerable<ComputerMonitor> monitors,
MonitorType type) =>
monitors.Where(m => m.Type == type).ToList();
}
[/code]
[code language="java"]
var monitors = new List<ComputerMonitor>
{
new ComputerMonitor { Name = "Samsung S345", Screen = Screen.CurvedScreen, Type = MonitorType.OLED },
new ComputerMonitor { Name = "Philips P532", Screen = Screen.WideScreen, Type = MonitorType.LCD },
new ComputerMonitor { Name = "LG L888", Screen = Screen.WideScreen, Type = MonitorType.LED },
new ComputerMonitor { Name = "Samsung S999", Screen = Screen.WideScreen, Type = MonitorType.OLED },
new ComputerMonitor { Name = "Dell D2J47", Screen = Screen.CurvedScreen, Type = MonitorType.LCD }
};
var filter = new MonitorFilter();
var lcdMonitors = filter.FilterByType(monitors, MonitorType.LCD);
Console.WriteLine("All LCD monitors");
foreach (var monitor in lcdMonitors)
{
Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}
[/code]
Change MonitorFilter class:
[code language="java"]
public class MonitorFilter
{
public List<ComputerMonitor> FilterByType(IEnumerable<ComputerMonitor> monitors, MonitorType type) =>
monitors.Where(m => m.Type == type).ToList();
public List<ComputerMonitor> FilterByScreen(IEnumerable<ComputerMonitor> monitors, Screen screen) =>
monitors.Where(m => m.Screen == screen).ToList();
}
[/code]
This gives the desired result but violates the open-closed principle. The solution is to use interfaces:
[code language="java"]
public interface ISpecification<T>
{
bool isSatisfied(T item);
}
public interface IFilter<T>
{
List<T> Filter(IEnumerable<T> monitors, ISpecification<T> specification);
}
[/code]
Create a separate class for the monitor type specification to implement ISpecification:
[code language="java"]
public class MonitorTypeSpecification : ISpecification<ComputerMonitor>
{
private readonly MonitorType _type;
public MonitorTypeSpecification(MonitorType type)
{
_type = type;
}
public bool isSatisfied(ComputerMonitor item) => item.Type == _type;
}
[/code]
Modify the existing MonitorFilter class by getting it to implement the Ifilter interface:
[code language="java"]
public class MonitorFilter : IFilter<ComputerMonitor>
{
public List<ComputerMonitor> Filter(IEnumerable<ComputerMonitor> monitors, ISpecification<ComputerMonitor> specification) =>
monitors.Where(m => specification.isSatisfied(m)).ToList();
}
[/code]
On implementing this in the program, the result should be the same:
[code language="java"]
var filter = new MonitorFilter();
var lcdMonitors = filter.Filter(monitors, new MonitorTypeSpecification(MonitorType.LCD));
Console.WriteLine("All LCD monitors");
foreach (var monitor in lcdMonitors)
{
Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}
[/code]
So it is now possible to make further extensions to our MonitoFilter class without actually modifiying the class itself. If we want to filter by screen type, create a new screen specification class that implements the Ispecification interface, and use that:
[code language="java"]
public class ScreenTypeSpecification : ISpecification<ComputerMonitor>
{
private readonly Screen _screen;
public ScreenTypeSpecification(Screen screen)
{
_screen = screen;
}
public bool isSatisfied(ComputerMonitor item) => item.Screen == _screen;
}
[/code]
And make use of this in the main code:
[code language="java"]
var filter = new MonitorFilter();
var wideScreenMonitors = filter.Filter(monitors, new ScreenSpecification(Screen.WideScreen));
foreach (var monitor in wideScreenMonitors)
{
Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}
[/code]
3. Liskov Substitution Principle
A form of polymorphism. Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. Derived class objects should be able to replace base class objects without modifying it’s behaviour:
Example classes to calculate the sum of all numbers or just even numbers.
[code language="java"]
public class SumCalculator
{
protected readonly int[] _numbers;
public SumCalculator(int[] numbers)
{
_numbers = numbers;
}
public int Calculate() => _numbers.Sum();
}
[/code]
[code language="java"]
public class EvenNumbersSumCalculator : SumCalculator
{
public EvenNumbersSumCalculator(int[] numbers)
: base(numbers)
{
}
public new int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}
[/code]
[code language="java"]
var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
SumCalculator sum = new SumCalculator(numbers);
Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}");
Console.WriteLine();
EvenNumbersSumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
[/code]
Works fine. But of we try this:
[code language="java"]
var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
[/code]
We don’t get the result we desire:
We don’t get the expected result because our variable evenSum is of type SumCalculator which is the base class. This means that the Count method from the SumCalculator class will be executed, not the EvenNumbersSumCalculator class. Therefore our child class is not behaving as a substitute for the parent class, thereby violating the Liskov substitution principle.
We overcome this by using abstract base classes:
[code language="java"]
public abstract class Calculator
{
protected readonly int[] _numbers;
public Calculator(int[] numbers)
{
_numbers = numbers;
}
public abstract int Calculate();
}
[/code]
[code language="java"]
public class SumCalculator : Calculator
{
public SumCalculator(int[] numbers) : base(numbers) { }
public override int Calculate() => _numbers.Sum();
}
[/code]
[code language="java"]
public class EvenNumbersSumCalculator : Calculator
{
public EvenNumbersSumCalculator(int[] numbers) : base(numbers) {}
public override int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}
[/code]
[code language="java"]
var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
Calculator sum = new SumCalculator(numbers);
Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}");
Console.WriteLine();
Calculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
[/code]
4. Interface-segregation principle
This states that no client should be forced to depend on methods it does not need or should not use . Many client-specific interfaces can work better than one general-purpose interface.
For example, to develop a behavior for a multifunctional car , there are vehicles that we can only drive and there are vehicles that we can only fly. But there are vehicles we can both drive and fly. We would like to create a class which supports all these actions for a vehicle.
[code language="java"]
public interface IVehicle
{
void Drive();
void Fly();
}
[/code]
[code language="java"]
public class MultiFunctionalCar : IVehicle
{
public void Drive()
{
Console.WriteLine("Drive a multifunctional car");
}
public void Fly()
{
Console.WriteLine("Fly a multifunctional car");
}
}
[/code]
This covers the requirements for a multi-functional car. However if we want to develop classes for drive-only and fly-only vehicles, we see inefficiencies:
[code language="java"]
public class Car : IVehicle
{
public void Drive()
{
Console.WriteLine("Driving a car");
}
public void Fly()
{
throw new NotImplementedException();
}
}
[/code]
[code language="java"]
public class Airplane : IVehicle
{
public void Drive()
{
throw new NotImplementedException();
}
public void Fly()
{
Console.WriteLine("Flying a plane");
}
}
[/code]
Each class only contains one required declaration: the unwanted one is made to throw an exception, which is wasteful.
The solution is to split the IVehicle interface into separate Icar and IAircraft interfaces:
[code language="java"]
public interface ICar
{
void Drive();
}
[/code]
[code language="java"]
public interface IAirplane
{
void Fly();
}
[/code]
Resulting in classes that only implement the methods they need:
[code language="java"]
public class Car : ICar
{
public void Drive()
{
Console.WriteLine("Driving a car");
}
}
[/code]
[code language="java"]
public class Airplane : IAirplane
{
public void Fly()
{
Console.WriteLine("Flying a plane");
}
}
[/code]
[code language="java"]
public class MultiFunctionalCar : ICar, IAirplane
{
public void Drive()
{
Console.WriteLine("Drive a multifunctional car");
}
public void Fly()
{
Console.WriteLine("Fly a multifunctional car");
}
}
[/code]
5. Dependency Inversion Principle
These depend upon abstractions, [not] concretions
Dependency Inversion Principle is a means of creating logic for higher-level modules in such a way to be reusable and unaffected by any changes from the lower level modules in the application.
Having this idea in mind the Dependency Inversion Principle states that
- High-level modules should not depend on low-level modules, both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Comments
Post a Comment