SOLID Principles are the most important principles of writing maintainable, easy-to-read, easy-to-write clean code. This presentation attempts to give a basic overview of these principles with some examples of violations and ways to correct them.
3. SOLID
Five principles of agile object-oriented software design
Robert C. Martin: Agile Software Development: Principles, Patterns, and
Practices (2002)
Robert C. Martin: Agile Software Development: Principles, Patterns, and
Practices in C# (2003)
Robert C. Martin: Clean Code (2008)
Robert C. Martin: Clean Architecture (2017)
5. SOLID - SRP
The least well understood principle of SOLID.
It is not about the module (method/class/component) doing exactly one
thing.
Correct phrasing:
“A module should be responsible to one, and only one, user or stakeholder – and
thus have one, and only one, reason to change.”
This, in fact, does nevertheless contain the above definition of a module
doing exactly one thing.
6. Cohesion & Coupling
We will discuss these in more detail when discussing Static Code Analysis
Metrics
Coupling: interdependency between modules
Cohesion: relatedness of functionality within one module
Low Coupling and High Cohesion should be the goal
SRP is basically stating that “Cohesion should be as high as possible”
7. SOLID - SRP
“A class should have only one reason to change”
As an example, consider a module that compiles and prints a report.
Such a module can be changed for two reasons.
The content of the report could change.
The format of the report could change.
These two things change for very different causes; one substantive, and one
cosmetic.
The single responsibility principle says that these two aspects of the problem
are really two separate responsibilities, and should therefore be in separate
classes or modules. It would be a bad design to couple two things that change
for different reasons at different times.
8. Another Example: The Modem
public interface Modem
{
public void Dial(string phoneNumber);
public void Hangup();
public void Send(char c);
public char Receive();
}
9. Modem Example, Part 2
<<I>> Data Channel
+ Send(char)
+ Receive : char
<<I>> Connection
+ Dial(string)
+ Hangup()
Modem
// with two responsibilities
10. SOLID - SRP
One “thing” should do exactly one “thing”
What is a “thing”?
Usually a class
Sometimes a function
Trivially a variable
Would you write code like this?
bool isLowerCase = string.Equals(input, input.ToLower());
if (isLowerCase) {
isLowerCase = input.Length < 50;
// why declare another bool, we no longer use the previous one, lets repurpose!
}
11. SOLID - SRP
Why would you then do something like this?
public void DoStuff(bool flag) {
if (flag) {
// do something
} else {
// do something else
}
}
The above is only correct, if DoStuff() is a wrapper function, that you use to
dynamically invoke either DoSomething() or DoSomethingElse()
12. SOLID - SRP
The same holds true for classes
One class should only be responsible for one single thing
So, increase cohesion!
Use small classes
Reduce the number of instance variables!
Move functionality on different level of abstraction to a separate class
Methods that do not manipulate instance variables might not be part of the class (static
methods)
Use composition
15. SOLID – O/CP
Classes should be open for extension but closed for modification
Changes should be localizable – a single change should not cascade down and
require dependent modules to change as well
Changes should be achieved by adding new code instead of modified existing
code
16. SOLID – O/CP
Open for extension: This means that the behavior of the module can be
extended. As the requirements of the application change, we can extend the
module with new behaviors that satisfy those changes. In other words, we are
able to change what the module does.
Closed for modification: Extending the behavior of a module does not result
in changes to the source, or binary, code of the module. The binary
executable version of the module whether in a linkable library, a DLL, or a
.EXE file remains untouched.
Is it possible to satisfy both criteria at once?
17. SOLID – O/CP
Use abstraction!
An interface or an abstract base class can be fixed, thus closed for
modification. However, the implementation is an extension point, so the
abstract module remains open for extension.
Some common design patterns to satisfy OCP are:
Strategy
Template Method
Visitor
19. SOLID – LSP
Derived classes can be used instead of the classes they derive of
Animal
Bird Dog
Move
Fly Walk
20. SOLID – LSP
Bad solution
public class Bird : IAnimal {
public void Move() {
throw
NotImplementedException
(“Use fly()!”);
}
public void Fly() {
FlapWings();
}
}
Good solution
public class Bird : IAnimal {
public void Move() {
Fly();
}
public void Fly() {
FlapWings();
}
}
21. SOLID – LSP
public class Bird : IAnimal {
public void Move() {
Fly();
}
}
public class Dog : IAnimal {
public void Move() {
Walk();
}
}
public void MoveAllAnimals(IEnumerable<IAnimal> animals) {
foreach (animal in Animals) {
animal.Move();
}
}
22. SOLID – LSP
Common example of breaking the LSP: the Square/Rectangle problem
A square is (by mathematic definition) a rectangle.
public class Rectangle {
public virtual int Height { get; set; }
public virtual int Width { get; set; }
}
public class Square : Rectangle {
private int side;
public override int Height {
get { return side; }
set { side = value; }
}
public override int Width {
get { return side; }
set { width = side; }
}
}
23. SOLID – LSP
Nah, that’s okay, where’s the issue?
Well…
Rectangle r = RectangleFactory.Create();
r.Height = 2;
r.Width = 5;
r.Area.Should().Be(10);
works just fine if the RectangleFactory returns a Rectangle, but fails, if it returns a
Square – which is a valid return value, as a square is a rectangle in our
implementation.
25. SOLID – ISP
Use multiple small interfaces
Split interfaces by functional boundaries
When a method only requires something related to the interface, use the
interface as the type of the input parameter
Why?
So that your client won’t do anything stupid.
So that your client doesn’t depend on things it does not need.
26. SOLID – ISP
Bad solution
A solution I’ve worked on: IInstance.
428 lines of code
100+ methods
8 regions:
Configuration and Tracing
LoginManager
WMSManager
ProductionManager
…
Good solution
IInstance : ILoginManager,
IWmsManager, IProductionManager, …
Usage:
ProductionEngineWrapper
(IProductionManager
productionManager) {
// can only call
// production related code
// from this method
// but not WMS-related
}
28. Another Example, cont’d
static void Main()
{
MyGraph graph = new MyGraph();
graph.Load(@"C:settings.txt);
PrintAGraphInA4(graph);
}
static void PrintAGraphInA4(MyGraph
graph)
{
// Note:
// the code only needed access
// to the Print Method
// but was exposed to
// Load/Save/
// MathModel/GraphTitle/etc.
Printer printer = new Printer();
printer.Paper =
Printer.PaperType.A4;
graph.Print(printer);
}
29. Another Example, corrected
class Printer
{
public enum PaperType { A3, A4, Letter };
public PaperType Paper { get; set; }
}
interface IPrintable
{
void Print(Printer printerDevice);
}
class MyGraph : IPrintable
{
// details
}
30. Corrected example, continued
static void Main()
{
MyGraph graph = new MyGraph();
graph.Load(@"C:settings.txt”);
// because inheritance is used for
// MyGraph : Iprintable
// we can simply pass the graph
// to automatically cast
// to Iprintable
PrintInA4(graph);
}
static void PrintInA4(IPrintable
printable)
{
// we do not care
// what we are printing.
Printer printer = new Printer();
printer.Paper =
Printer.PaperType.A4_PAPER;
printable.Print(printer);
}
32. SOLID - DIP
Rely on abstractions and interfaces, not concrete implementations
High level modules should not depend on low level modules.
Why?
See O/C principle! Implementations are subject to change, abstractions are usually
not!
The level of abstraction is broken. Your object should not care about the lifetime
of the objects it uses.
Objects should be used
Not created and destroyed
33. SOLID - DIP
Bad solution
public class MainViewModel {
public IInstructionViewModel
InstructionViewModel
{ get; set; }
public IProductionViewModel
ProductionViewModel
{ get; set; }
public MainViewModel() {
InstructionViewModel = new()
ProductionViewModel = new()
}
}
Good solution
public class MainViewModel {
public IInstructionViewModel
InstructionViewModel
{ get; set; }
public IProductionViewModel
ProductionViewModel
{ get; set; }
public MainViewModel(
IInstructionViewModel ivm,
IProductionViewModel pvm) {
this.InstructionViewModel = ivm
ProductionViewModel = pvm
}
}