Skip to content
Chris's Blog
TwitterLinkedIn

SOLID Development Principles

SOLID, OOP, Design Patterns4 min read

SOLID Principles

Split files up with folders. Split by feature (core, infrastructure, UI) or type of file (Model, Controller, Interfaces)

Single responsibility principle

A class should only have a single responsibility. Only changes to one part of the software's specification should affect the specification of the class

Open-closed principle

Software entities should be open for extension but closed to modification

Liskov substitution principle

Objects in a program should be replaceable with instances of their sub-types (child classes) with the programming continuing to function correctly. I.e. the calling functions and properties shouldn't change in the extension/child classes, but the integrated functionality should.

Interface segregation principle

Many client-specific interfaces are better than one general-purpose interface

Dependency inversion principle

Components should depend upon abstractions not on concretions (concrete classes)

Single Responsibility Principle (SRP)

What is a responsibility?

Each responsibility is a different axis of change

Examples include:

  • Persistance
  • Logging
  • Validation
  • Business Logic

If two details are integrated into the same class this introduces tight coupling. Therefore, if either of these details need to change this will affect the other and the way the entire class is implemented.

Cohesion

Connection inside elements in classes

Coupling

Connection of classes to each other

Separation of Concerns

Programs should be separated into distinct sections, each addressing a separate concern, or set of information that affects the program.

Keep classes small, focused and testable

Open / Closed Principle (OCP)

Software entities (classes, modules, function, etc). Should be open for extension, but closed for modification.

It should be possible to change the behaviour of a method without editing its source code.

Open to extension

New behaviour can be added in the future

Code that is closed to extension has fixed behaviour

Closed to modification

Changes to source or binary code are not required

The only way to change the behaviour of code that is closed to extension is to change the code itself.

Why should it be closed to modification?

Less likely to introduce bugs in code we don't touch or redeploy

Less likely to break dependent code when we don't have to deploy updates

Fewer conditionals in code that is open to extension

Bug fixes are ok - exception to the principles rule

Balance abstraction and concreteness

Abstraction adds complexity

Predict where variation is needed and apply abstraction as needed (pain driven development)

"new" is glue

Should be resistant to newing up instances of objects inside classes

How can you predict future changes?

Start concrete

Modify the code the first time or two

By the third modification, consider making the code open to extension for that axis of change

Typical approaches to OCP

Examples:

1public class DoOneThing
2{
3 public void Execute()
4 {
5 Console.WriteLine("Hello, World!");
6 }
7}
  • Parameters

    1public class DoOneThing
    2{
    3 public void Execute(string message)
    4 {
    5 Console.WriteLine(message);
    6 }
    7}
  • Inheritance

    1public class DoOneThing
    2{
    3 public virtual void Execute()
    4 {
    5 Console.WriteLine("Hello, World!");
    6 }
    7}
    8public class DoAnotherThing : DoOneThing
    9{
    10 public override void Execute()
    11 {
    12 Console.WriteLine("Goodbye, World!");
    13 }
    14}
  • Composition / Injection

    1public class DoOneThing
    2{
    3 private readonly MessageService _messageService;
    4
    5 public DoOneThing (MessageService messageService)
    6 => _messageService = messageService;
    7
    8 public virtual void Execute()
    9 {
    10 Console.WriteLine(messageService.GetMessage());
    11 }
    12}

Prefer implementing new features in new classes.

Design classes from scratch to suit problem at hand

Nothing in current system depends on it (no dependencies)

Can add behaviour without touching existing code

Can follow SRP in the new class

Can be unit-tested (even if the rest of the application can't be)

Key Takeaways

  • Solve the problem first using simple, concrete code
  • Identify the kinds of changes the application is likely to continue needing
  • Modify code to be extensible along the axis of change you've identified
    • Without the need to modify its source each time

Liskov Substitution Principle (LSP)

Sub-types must be substitutable for their base types

LSP states that the IS-A relationship is insufficient and should be replaced with IS-SUBSTITUTABLE-FOR

Fixing LSP Violations

  • Follow the "Tell, Don't Ask" principle
    • Encapsulate the logic for a particular action inside of the class being called. Don't implement the logic in the calling class.
  • Minimise null checks with
    • C# features
    • Guard clauses
    • Null Object design pattern
  • Follow ISP and be sure to fully implement interfaces

IS-A - inheritance relationship

HAS-A - parameter relationship

Key Takeaways

  • Sub-types must be substitutable for their base types
  • Ensure base type invariants are enforced
  • Look for
    • Type checking
    • Null checking
    • NotImplementedException

Interface Segregation Principle (ISP)

Clients should not be forced to depend on methods they do not use.

Prefer small, cohesive interfaces to large, "fat" ones

Violating ISP results in classes that depend on things they don't need.

What does interface mean in ISP?

C# interface type/keyword

Public (or accessible) interface of a class

A type's interface in this context is whatever can be accessed by client code working with an instance of that type.

What's a client?

In this context, the client is the code that is interacting with an instance of the interface. It's the calling code.

Splitting up large interfaces

1public interface ILargeInterface : SmallInterfaceOne, SmallInterfaceTwo
2{
3}

If you originally had all of the template code in the ILargeInterface and then split it down into the two smaller interface. Implementing those two small interfaces will give the ILargeInterface the same template and will allow the code that was originally implementing ILargeInterface to continue working. It is a non-breaking change.

Fixing ISP Violations

  • Break up large interfaces into smaller ones
    • Compose fat interfaces from smaller ones for backward compatibility
  • To address large interfaces you don't control
    • Create a small, cohesive interface
    • Use the Adapter design pattern so your code can work with the Adapter.
  • Clients should own and define their interfaces

Where do interfaces live in our apps?

  • Client code should define and own interfaces it uses
  • Interfaces should be declared where both client code and implementations can access it

Key Takeaways

  • Prefer small, cohesive interfaces to large, expansive ones
  • Following ISP helps with SRP and LSP
  • Break up large interfaces by using
    • Interface inheritance
    • The Adapter design pattern

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 should depend on abstractions.

Dependencies in C#

  • References required to compile
  • References required to run

High Level

More abstract

Business rulres

Process-oriented

Furhter from input/output (i/o)

Low Level

  • Closer to I/O
  • "Plumbing" code

Interacts with specific external systems and hardware

Abstractions in C#

  • Interfaces
  • Abstract base classes
  • "Types you can't instantiate"
  • Abstractions shouldn't be couple to details
  • Abstractions describe what
    • Send a message
    • Store a customer record
  • Details specify how
    • Send an SMTP email over port 25
    • Serialise Customer to JSON and store in a text file

Hidden Direct Dependencies

  • Direct use of low level dependencies
  • Static calls and new
  • Cause pain
    • Tight coupling
    • Difficult to isolate and unit test
    • Duplication

Explicit Dependencies Principle

  • Your class shouldn't surprise clients with dependencies
  • List them up front, in the constructor
  • Think of them as ingredients in a cooking recipe.

Key Takeaways

  • Most classes should depend on abstractions, not implementation details
  • Abstractions shouldn't leak details
  • Classes should be explicit about their dependencies
  • Client should inject dependencies when they create other classes
  • Structure your solutions to leverage dependency inversion