SOLID Principles Notes
Overview
The SOLID principles are a set of software design principles intended to make software designs more understandable, flexible, and maintainable.
Single Responsibility Principle
Every software should have only one reason to change.
A class should only have one job. A popular analogy is the Swiss knife which has can perform many different functions and with more and more functions added, it becomes a nightmare to use/maintain.
// Violates the SRP and has hidden dependencies
class MessageService
{
private Email _email;
private Sms _sms;
public void SendEmail()
{
Send(_email);
}
public SendSms()
{
Send(_sms);
}
}
// Refactored
//
interface IEmailSender
{
public void SendEmail(Email email);
}
interface ISmsSender
{
public void SendSms(Sms sms);
}
// Now you can create separate services of Email and SMS
// And you can inject them into the MessageService class
Open Closed Principle
Open for extension, closed to modification.
A class should be designed in a way such that new functionality should only be added when there are new requirements - this can be achieved with inheritance. An existing class should be closed for modification and it shouldn't be necessary to modify it to add new functionality.
Liskov Substitution Principle
Derived classes must be substitutable for their base classes.
A class should be able to use any derived class instead of a parent class and have it behave in the same manner without modification.
Interface Segregation Principle
Make fine grained interfaces that are client specific.
Basically clients shouldn't be forced to implement interfaces that they don't use. Many small, focused interfaces is preferable over a fat interface.
Smells
A class only implements a subset of an interface which then usually indicates a LSP violation as well.
Many
NotImplementedException
Dependency Inversion Principle
Depend on abstractions, not on concretions. Details depend on abstractions.
High level modules/classes shouldn't depend on low-level modules/classes.
Both should depend on abstractions
Abstractions should not depend upon details
Details should depend on abstractions
Traditional programming
Business logic depends on the infrastructure e.g. database
Static methods are used for convenience
Class instantiations are scattered throughout the application
Violates the SRP & LSR
Class dependencies
The idea is that classes should be honest with their dependencies so that they are explicit. Having hidden or implicit dependencies makes it very hard to unit test and change/extended functionality.
Inversion of Control
Don't call us, we'll call you. See Martin Fowler Blog
Simply put, it is when the control is inverted to what it was previously. This often comes up in the design of Frameworks.
From Martin Fowler's blog:
Let's consider a simple example. Imagine I'm writing a program to get some information from a user and I'm using a command line enquiry. I might do it something like this
#ruby puts 'What is your name?' name = gets process_name(name) puts 'What is your quest?' quest = gets process_quest(quest)
In this interaction, my code is in control: it decides when to ask questions, when to read responses, and when to process those results.
However if I were were to use a windowing system to do something like this, I would do it by configuring a window.
require 'tk' root = TkRoot.new() name_label = TkLabel.new() {text "What is Your Name?"} name_label.pack name = TkEntry.new(root).pack name.bind("FocusOut") {process_name(name)} quest_label = TkLabel.new() {text "What is Your Quest?"} quest_label.pack quest = TkEntry.new(root).pack quest.bind("FocusOut") {process_quest(quest)} Tk.mainloop()
There's a big difference now in the flow of control between these programs - in particular the control of when the
process_name
andprocess_quest
methods are called. In the command line form I control when these methods are called, but in the window example I don't.Instead I hand control over to the windowing system (with the
Tk.mainloop
command). It then decides when to call my methods, based on the bindings I made when creating the form. The control is inverted - it calls me rather me calling the framework. This phenomenon is Inversion of Control.
This can still be a little confusing and it doesn't help when it is often used interchangeably with Dependency Injection/Inversion!
Dependency Injection
Is a set of software design principles and patterns that enable us to develop loosely coupled code.
In its general form, it would be described as above. Designing loosely coupled code tends to adhere to other SOLID principles naturally; for example, it often does not violate the DRY and O/C principles.
It is a technique in which an object receives the object it depends on instead of it building/finding the service it needs.
For .NET Core for example, it injects or resolves the dependencies a class explicitly needs through its IoC (Inversion of Control) Container - adding your service implementations is as simple as adding it to the service collection with a method extension.
Explicit Dependencies Principle
Methods and classes should explicitly require any collaborating objects they need in order to function correctly.
Constructor Injection
Pros
Classes self-document what they need to perform
Works well with/without a container
Classes are always in a valid state once constructed
Cons
Can have many dependencies (design smell)
Some features need default constructors e.g. serialization
Some methods in the class may not require some of the injected dependencies other methods require (design smell)
Property Injection
Also known as "Setter Injection" by setting properties.
Pro
Dependencies can be changed at any time during its lifetime
Very flexible
Cons
Dependencies may be in an invalid state between constructions and setting of dependencies
Less intuitive - hard to document, what if order of setting matters?
Parameter Injection
Dependencies are passed in via a method parameter. Consider if one method has the dependency, otherwise prefer constructor injection.
Pros
Most granular
Very flexible
Requires no change for the rest of the class
Cons
Breaks method signature
Can result in many parameters (design smell)
Service Scope
With dependency injection frameworks in .NET being a best practice - it's important to understand the lifetime of the services when registering them with the IoC Container.
Transient
Transient objects are always different; a new instance is provided to every controller and every service.
A nice example from Stack Overflow with examples helps a log visually.
Scoped
Scoped objects are the same within a request, but different across different requests.
Singleton
Singleton objects are the same for every object and every request.
There are some interesting situations where if you have a parent service that has transient dependencies; the transient dependencies will not be different. A better example is detailed here.
Miscellaneous Encyclopedia
Don't Repeat Yourself Principle
Repetition in logic calls for abstraction. Repetition in process calls for automation.
You Aren't Going To Need It
YAGNI - build what you need today and not tomorrow.
Domain Objects
Objects from the business specific area that represent something meaningful to the domain expert. Domain objects are mostly represented by entities and value objects. Generally speaking, most objects that live in domain layer contribute to the model and are domain objects.
For example, in most cases, domain objects do not include things like logging, formatting, serialization, encryption etc - unless you are specifically building a product to log, serialize, format or encrypt respectively.
POCO
Plain Old Class Object - it just stores data in memory generally using auto-properties and is used with ORM or as a Data Transfer Object.
Entity
An object fundamentally defined not by its attributes, but by a thread of continuity and identity. (Meaning it must have Id)
Variance
Read this MS blog first before trying to understand...
Covariance
If it preserves the ordering of types (≤), which orders types from more specific to more generic.
For example, List
would be covariant if S
is a subtype of T
iff List[S]
is a subtype of List[T]
Contravariance
If it does not preserve the ordering of types (≤), which orders types from more specific to more generic.
For example, (x ≤ y) = (N(y) ≤ N(x))
is always true and you can see that the ordering is reversed!
Anemic
Objects that have state but lack behavior e.g. DTOs
Closure
Essentially a block of code which can be executed at a later time but also maintains the environment in which it was first created; even after the method has finished executing.
That is; it can still use local variables of the method which created it due to the C# compiler creating a temporary class for the delegate and disposing it when no more references exist.
Last updated