writing maintainable software using solid principles
TRANSCRIPT
Have you ever played Jenga?
From <http://www.codemag.com/article/1001061>
https://lostechies.com/derickbailey/2009/02/11/solid-development-
principles-in-motivational-pictures/
MVC and Adding the Rails
MVC popularized by Ruby on Rails
If you consider a train on rails, the train goes where the rails take
it. - https://www.ruby-forum.com/topic/135143
by defining a convention and sticking to that you will find that your applications
are easy to generate and quick to get up and running. -
https://stackoverflow.com/questions/183462/what-does-it-mean-for-a-
programming-language-to-be-on-rails
Principles, Patterns, and Practices
What’re we talking about?
Adding the rails with SOLID Principles and DRY
Shown using a particular methodology
Some patterns to help
Dependency Injection
…and how this leads to maintainable software
The DRY Principle
DRY - Don’t Repeat Yourself!
“Every piece of knowledge must have a single, unambiguous, authoritative
representation within a system”
As opposed to WET
- The Pragmatic Programmer
Copy/Paste Programmingpublic UserDetails GetUserDetails(int id)
{UserDetails userDetails;string cs = Startup.Configuration.GetSection("ConnectionStrings:DefaultConnection").Value;var connection = new SqlConnection(cs);connection.Open();try{
userDetails = connection.Query<UserDetails>("select BusinessEntityID,EmailAddress from [Person].[EmailAddress] where BusinessEntityID = @Id",new { Id = id }).FirstOrDefault();
}finally{
connection.Dispose();}return userDetails;
}public UserContact GetUserContact(int id){
UserContact user = null;string cs = Startup.Configuration.GetSection("ConnectionStrings:DefaultConnection").Value;var connection = new SqlConnection(cs);connection.Open();try{
user = connection.Query<UserContact>(@"SELECT p.FirstName, bea.BusinessEntityID,ea.EmailAddress,bea.AddressIDFROM Person.Person p INNER JOIN Person.EmailAddress ea ON p.BusinessEntityID = ea.BusinessEntityIDINNER JOIN Person.BusinessEntityAddress bea ON ea.BusinessEntityID = bea.BusinessEntityIDwhere ea.BusinessEntityID = @Id", new { Id = userId }).FirstOrDefault();
}finally{
connection.Dispose();}return user;
}
store.deviq.com/products/software-craftsmanship-
calendars-2017-digital-image-pack
SOLID principles?
Principles of Object Oriented Design
Created by Robert C. Martin (known as Uncle Bob)
Also co-creator of the Agile Manifesto
Series of articles for The C++ Report (1996)
In his book Agile Software Development Principles, Patterns, and Practices
The SOLID Principlesas in Robert C. Martin’s Agile Software Development book
SRP: Single Responsibility Principle
OCP: Open-Closed Principle
LSP: The Liskov Substitution Principle
DIP: The Dependency Inversion Principle
ISP: The Interface-Segregation Principle
The SOLID Principlesas rearranged by Michael Feathers
SRP: Single Responsibility Principle
OCP: Open-Closed Principle
LSP: The Liskov Substitution Principle
ISP: The Interface-Segregation Principle
DIP: The Dependency Inversion Principle
The SOLID Principlesrearranged by importance
SRP: Single Responsibility Principle
DIP: The Dependency Inversion Principle
OCP: Open-Closed Principle
LSP: The Liskov Substitution Principle
ISP: The Interface-Segregation Principle
- per Uncle Bob on Hanselminutes podcast
…and they’re about Managing Dependencies!
SRP: Single Responsibility Principle
A class should have only one reason to change
SOLID
https://lostechies.com/derickbailey/2009/02/11/solid-development-
principles-in-motivational-pictures/ SOLID
public string SendUserEmail(int userId){
#region GetUserDataUserContact user = null;string cs = Startup.Configuration.GetSection("ConnectionStrings:DefaultConnection").Value;var connection = new SqlConnection(cs);connection.Open();try{
user = connection.Query<UserContact>(@"SELECT p.FirstName, bea.BusinessEntityID,ea.EmailAddress,bea.AddressIDFROM Person.Person p INNER JOIN Person.EmailAddress ea ON p.BusinessEntityID = ea.BusinessEntityIDINNER JOIN Person.BusinessEntityAddress bea ON ea.BusinessEntityID = bea.BusinessEntityIDwhere ea.BusinessEntityID = @Id", new { Id = userId }).FirstOrDefault();
}finally{
connection.Dispose();}#endregion#region GetAdditionalInfoOnUservar httpClient = new HttpClient();string userCoordinatesJson = httpClient.GetStringAsync(
$"http://localhost:62032/api/geolocator/locateaddressid/{user.AddressId}").Result;var userCoordinates = JsonConvert.DeserializeObject<GeocodeCoordinates>(userCoordinatesJson);#endregion#region Business Logicif (IsNearby(userCoordinates)){
return "User is nearby. Do not send the scam email!";}#endregion#region BuildAndSendUserEmailSmtpClient client = new SmtpClient(Startup.Configuration.GetSection("Smtp:Host").Value);client.UseDefaultCredentials = false;client.Credentials = new NetworkCredential{
UserName = Startup.Configuration.GetSection("Smtp:UserName").Value,Password = Startup.Configuration.GetSection("Smtp:Password").Value
};MailMessage mailMessage = new MailMessage{
From = new MailAddress(Startup.Configuration.GetSection("Smtp:FromAddress").Value),
Subject = $"Congratulations, {user.FirstName}, you just won!",Body = "You won 1.3 million dollars! " +"There are just some taxes you need to pay on those winnings first. " +"Please send payment to [email protected].",
};mailMessage.To.Add(user.EmailAddress);client.Send(mailMessage);#endregionreturn "Email Sent! Just wait for the riches to come pouring in...";
} SOLID
What could change?
Could have a SQL Server instance dns change [mitigated]
SQL table structure could change
Api url could change (and actually WILL, since pointing to localhost)
Api interface could change
Business rules could change
Now only send to users in countries with weak extradition laws
Email SMTP server could change, from address, user/pass [mitigated]
Subject and body of email could change
SOLID
In other words, the following could
change…
Configuration data – server names, usernames, passwords
The way we get user data
The way we get user coordinates
The way we determine user eligibility
The way we contact users
SOLID
public string SendUserEmail(int id){
var userRepository = new UserSqlRepository();var user = userRepository.GetUserContactById(id);#region GetAdditionalInfoOnUservar httpClient = new HttpClient();string userCoordinatesJson = httpClient.GetStringAsync(
$"http://localhost:62032/api/geolocator/locateaddressid/{user.AddressId}").Result;…
Refactor to UserRepository
public class UserSqlRepository{
private string _connectionString = Startup.Configuration.GetSection("ConnectionStrings:DefaultConnection").Value;
public UserContact GetUserContactById(int userId){
UserContact user;using (var connection = GetOpenConnection()){
user = connection.Query<UserContact>(@"SELECT p.FirstName, bea.BusinessEntityID,ea.EmailAddress,bea.AddressIDFROM Person.Person p INNER JOIN Person.EmailAddress ea ON p.BusinessEntityID = ea.BusinessEntityIDINNER JOIN Person.BusinessEntityAddress bea ON ea.BusinessEntityID = bea.BusinessEntityIDwhere ea.BusinessEntityID = @Id", new { Id = userId }).FirstOrDefault();
}return user;
}
SOLID
After fully refactoring to adhere to SRP
public string SendUserEmail(int userId){
var userRepository = new UserSqlRepository();UserContact user = userRepository.GetUserContactById(userId);var userServiceClient = new UserServiceClient();GeocodeCoordinates userCoordinates =
userServiceClient.GetUserCoordinates(user.AddressId);var userScamEligibility = new NearbyUserScamEligibility();if (userScamEligibility.IsUserScamEligible(user, userCoordinates)){
return "Too risky. User not eligible for our scam.";}var messageSender = new SmtpUserMessageSender();messageSender.SendUserMessage(user);return "Email Sent! Just wait for the riches to come pouring in...";
}
SOLID
DIP: Dependency Inversion Principle(not the DI: Dependency Injection)
A. High-level modules should not depend on low-level modules. Both should
depend on abstractions.
B. Abstractions should not depend on details. Details should depend on
abstractions.
- Uncle Bob
Separate implementations from abstractions
Forces us to code to abstractions/interfaces
Separate construction from use
Allow for much easier testing
Allow for much easier changes and therefore maintainability
SOLID
store.deviq.com/products/software-craftsmanship-
calendars-2017-digital-image-pack SOLID
Added the abstraction (interface)public interface IUserMessageSender
{void SendUserMessage(UserContact user);
}
------- SEPARATE FILE, ideally separate dll/package -------
public class SmtpUserMessageSender : IUserMessageSender{
private readonly string _smtpUserName = Startup.Configuration.GetSection("Smtp:UserName").Value;private readonly string _smtpPassword = Startup.Configuration.GetSection("Smtp:Password").Value;private readonly string _smtpFromAddress = Startup.Configuration.GetSection("Smtp:FromAddress").Value;private readonly string _smtpHost = Startup.Configuration.GetSection("Smtp:Host").Value;public void SendUserMessage(UserContact user){
SmtpClient client = new SmtpClient(_smtpHost);client.UseDefaultCredentials = false;client.Credentials = new NetworkCredential{
UserName = _smtpUserName,Password = _smtpPassword
};MailMessage mailMessage = new MailMessage{
From = new MailAddress(_smtpFromAddress),Subject = $"Congratulations, {user.FirstName}, you just won!",Body = "You won 1.3 million dollars! " +"There are just some taxes you need to pay on those winnings first. " +"Please send payment to [email protected].",
};mailMessage.To.Add(user.EmailAddress);client.Send(mailMessage);
}
SOLID
Inverted the dependencies
Expose dependencies via constructor
public class SmtpUserMessageSender : IUserMessageSender{
private readonly ISmtpUserMessageSenderConfig _config;public SmtpUserMessageSender(ISmtpUserMessageSenderConfig config){
_config = config;}public void SendUserMessage(UserContact user){
SmtpClient client = new SmtpClient(_config.Host);client.UseDefaultCredentials = false;client.Credentials = new NetworkCredential{
UserName = _config.UserName,Password = _config.Password
};MailMessage mailMessage = new MailMessage{
From = new MailAddress(_config.FromAddress),Subject = $"Congratulations, {user.FirstName}, you just won!",Body = "You won 1.3 million dollars! " +"There are just some taxes you need to pay on those winnings first. " +"Please send payment to [email protected].",
};mailMessage.To.Add(user.EmailAddress);client.Send(mailMessage);
}
SOLID
Chuck Norris, Jon Skeet, and
Immutability (readonly keyword in C#)
Chuck Norris doesn’t read books…
Jon Skeet is immutable…
SOLID
…using a config DTO class
public class SmtpMessageSenderConfig{
public string UserName { get; set; }public string Password { get; set; }public string Host { get; set; }public string FromAddress { get; set; }
}
SOLID
Or if you want to be hardcore…
public class SmtpUserMessageSenderConfig : ISmtpUserMessageSenderConfig{
public string UserName { get; set; }public string Password { get; set; }public string Host { get; set; }public string FromAddress { get; set; }
}public interface ISmtpUserMessageSenderConfig{
string UserName { get; }string Password { get; }string Host { get; }string FromAddress { get; }
}
SOLID
Client usage injecting smtp dependencies
public string SendUserEmail(int userId){
var userRepository = new UserSqlRepository();UserContact user = userRepository.GetUserContactById(userId);var userServiceClient = new UserServiceClient();GeocodeCoordinates userCoordinates =
userServiceClient.GetUserCoordinates(user.AddressId);var userScamEligibility = new NearbyUserScamEligibility();if (userScamEligibility.IsUserScamEligible(user, userCoordinates)){
return "Too risky. User not eligible for our scam.";}IUserMessageSender userMessageSender = new SmtpUserMessageSender(
new SmtpUserMessageSenderConfig{
Host = Startup.Configuration.GetSection("Smtp:Host").Value,UserName = Startup.Configuration.GetSection("Smtp:UserName").Value,Password = Startup.Configuration.GetSection("Smtp:Password").Value,FromAddress =
Startup.Configuration.GetSection("Smtp:FromAddress").Value});
userMessageSender.SendUserMessage(user);return "Scam Email Sent! Just wait for the riches to come pouring in...";
} SOLID
Inject them all!
public string SendUserEmail(int userId){
var userRepository = new UserSqlRepository(new UserSqlRepositoryConfig{
ConnectionString = Startup.Configuration.GetSection("ConnectionStrings:DefaultConnection").Value
});UserContact user = userRepository.GetUserContactById(userId);var userServiceClient = new UserServiceClient(new UserServiceClientConfig{
UserCoordinatesUrl = Startup.Configuration.GetSection("UserApi:UserCoordinatesUrl").Value},new HttpClientHandler());GeocodeCoordinates userCoordinates = userServiceClient.GetUserCoordinates(user.AddressId);var userScamEligibility = new NearbyUserScamEligibility();if (userScamEligibility.IsUserScamEligible(user, userCoordinates)){
return "Too risky. User not eligible for our scam.";}IUserMessageSender userMessageSender = new SmtpUserMessageSender(
new SmtpUserMessageSenderConfig{
Host = Startup.Configuration.GetSection("Smtp:Host").Value,UserName = Startup.Configuration.GetSection("Smtp:UserName").Value,Password = Startup.Configuration.GetSection("Smtp:Password").Value,FromAddress = Startup.Configuration.GetSection("Smtp:FromAddress").Value
});userMessageSender.SendUserMessage(user);return "Scam Email Sent! Just wait for the riches to come pouring in...";
} SOLID
Strategy pattern in UserProcessor
public class UserProcessor : IUserProcessor{
private readonly IUserRepository _userRepository;private readonly IUserScamEligibility _userScamEligibility;private readonly IUserMessageSender _userMessageSender;public UserProcessor(IUserRepository userRepository, IUserScamEligibility
userScamEligibility, IUserMessageSender userMessageSender){
this._userRepository = userRepository;this._userScamEligibility = userScamEligibility;_userMessageSender = userMessageSender;
}public bool SendUserMessageByUserId(int userId){
var user = _userRepository.GetUserContactById(userId);if (!_userScamEligibility.IsUserScamEligible(user)){
return false;}_userMessageSender.SendUserMessage(user);return true;
}} SOLID
The Strategy PatternDefine a family of algorithms, encapsulate each one, and make them interchangeable.
Strategy lets the algorithm vary independently from clients that use it. – GOF
This is achieved via composition and not via inheritance.
SOLIDUML Diagrams - https://yuml.me/diagram/scruffy/class/draw
Principles of reusable object-oriented design
Program to an interface, not an implementation.
Favor object composition over class inheritance.
Object composition has another effect on system design. Favoring object composition over class inheritance helps you keep each class encapsulated and focused on one task. Your classes and class hierarchies will remain small and will be less likely to grow into unmanageable monsters……our experience is that designers overuse inheritance as a reuse technique, and designs are often made more reusable (and simpler) by depending more on object composition.
GOF - Design Patterns: Elements of Reusable Object-Oriented Software (pp. 19-20). Pearson Education (1995).
SOLID
Favor interface inheritance over
implementation inheritance
James Gosling, creator of Java, stated in 2001 interview that he was wrestling
with the idea of removing class inheritance:
“Rather than subclassing, just use pure interfaces. It's not so much that class
inheritance is particularly bad. It just has problems.”
https://www.javaworld.com/article/2073649/core-java/why-extends-is-
evil.html
[James Gosling on what he'd change in Java] explained that the real problem
wasn't classes per se, but rather implementation inheritance (the extends
relationship). Interface inheritance (the implements relationship) is preferable.
You should avoid implementation inheritance whenever possible.
- Allen Holub 2003
http://www.artima.com/intv/gosling34.html
SOLID
Controller acting as Composition Root
public string SendUserEmail(int userId){
IUserRepository userRepository = new UserSqlRepository(new UserSqlRepositoryConfig{
ConnectionString = Startup.Configuration.GetSection("ConnectionStrings:DefaultConnection").Value
});IUserServiceClient userServiceClient = new UserServiceClient(new UserServiceClientConfig{
UserCoordinatesUrl = Startup.Configuration.GetSection("UserApi:UserCoordinatesUrl").Value
},new HttpClientHandler());IUserScamEligibility userScamEligibility = new
NearbyUserScamEligibility(userServiceClient);IUserMessageSender userMessageSender = new SmtpUserMessageSender(
new SmtpUserMessageSenderConfig{
Host = Startup.Configuration.GetSection("Smtp:Host").Value,UserName = Startup.Configuration.GetSection("Smtp:UserName").Value,Password = Startup.Configuration.GetSection("Smtp:Password").Value,FromAddress = Startup.Configuration.GetSection("Smtp:FromAddress").Value
});IUserProcessorNoFactory userProcessor = new
UserProcessorNoFactory(userRepository,userScamEligibility,userMessageSender);userProcessor.SendUserMessageByUserId(userId);return "Scam Email Sent! Just wait for the riches to come pouring in...";
} SOLID
Composition Root?
The main function in an application should have a concrete non-volatile side.
– Uncle Bob
The Composition Root is the place where we create and compose the objects
resulting in an object-graph that constitutes the application. This place should be
as close as possible to the entry point of the application.
From <http://www.dotnetcurry.com/patterns-practices/1285/clean-composition-roots-dependency-injection>
SOLID
THE Composition Root
Dependency Injection via Startup.cs filepublic void ConfigureServices(IServiceCollection services)
{services.AddMvc();services.AddSingleton<IUserProcessor, UserProcessor>();services.AddSingleton<IUserRepository, UserRepository>();services.AddSingleton<IUserScamEligibility, UserScamEligibility>();services.AddSingleton<IUserServiceClient, UserServiceClient>();services.AddSingleton<IUserMessageSender, SmtpUserMessageSender>();services.AddSingleton<UserRepositoryConfig>(provider => new UserRepositoryConfig{
ConnectionString = _configuration.GetSection("ConnectionStrings:DefaultConnection").Value
});services.AddSingleton<UserServiceClientConfig>(provider => new UserServiceClientConfig{
UserDetailsUrl = _configuration.GetSection("UserApi:UserDetailsUrl").Value});services.AddSingleton<SmtpUserMessageSenderConfig>(provider => new
SmtpUserMessageSenderConfig{
Host = _configuration.GetSection("Smtp:Host").Value,UserName = _configuration.GetSection("Smtp:UserName").Value,Password = _configuration.GetSection("Smtp:Password").Value,FromAddress = _configuration.GetSection("Smtp:FromAddress").Value
});} SOLID
Singletons
SOLID
SingletonsEnsure exactly 1 instance
public sealed class Singleton{
private static readonly Singleton instance = new Singleton();
// Explicit static constructor to tell C# compiler// not to mark type as beforefieldinitstatic Singleton(){}
private Singleton(){}
public static Singleton Instance{
get{
return instance;}
}}
Jon Skeet - http://csharpindepth.com/articles/general/singleton.aspxSOLID
Avoid Static Cling
A static member belongs to the type (class) and not to the instance.
It is a concrete implementation only and we cannot reference via abstraction.
Static Cling is a code smell used to describe the undesirable coupling
introduced by accessing static (global) functionality, either as variables or
methods. This coupling can make it difficult to test or modify the behavior of
software systems. - http://deviq.com/static-cling/
I tend to care when… I can’t unit test!
static includes external dependency.
MyNamespace.MyRepo.InsertInDb(…)
File.ReadAllText above
Static method includes non-deterministic behavior
DateTime.UtcNow();
System.IO.File.ReadAllText("SomeFile.txt");
SOLID
Controller after DI Framework configured
[Route("api/[controller]")]public class ProcessorController : Controller{
private readonly IUserProcessor _userProcessor;public ProcessorController(IUserProcessor userProcessor){
_userProcessor = userProcessor;}// GET api/ProcessorController/sendusermessage/5[HttpGet("sendusermessage/{userId}")]public string SendUserMessage(int userId){
try{
if (_userProcessor.SendUserMessageByUserId(userId)){
return "Scam Message Sent! Just wait for the riches to come pouring in...";}return "Too risky. User not eligible for our scam.";
}catch(Exception ex){
return "Error occurred sending message. Exception message: " + ex.Message;}
}
SOLID
Dependency Injection Frameworks
…also known as Inversion of Control (IoC)
C#
.NET Core Dependency Injection
Built into ASP.NET Core
A quick NuGet package away otherwise for NetStandard 2.0
Microsoft.Extensions.DependencyInjection
SimpleInjector
My preference for anything complicated or on .NET Framework
Highly opinionated
Ninject
Complex DI made easy
Not opinionated: will help you get it done regardless of pattern you choose
Java
Spring
SOLID
DIP, DI, IoC…what’s that?
Dependency Inversion Principle (DIP)
A. High-level modules should not depend on low-level modules. Both should
depend on abstractions.
B. Abstractions should not depend on details. Details should depend on
abstractions.
Dependency Injection (DI)
Inversion of Control (IoC)
Hollywood Principle – “Don't call us, we'll call you”
Template Method
Events/Observable
IoC Container and DI Framework
Same thing!
SOLIDhttps://martinfowler.com/bliki/InversionOfControl.html
OCP: The Open-Closed Principle
Software entities (classes, modules, functions, etc…) should be open for
extension, but closed for modification
We can extend client classes that use interfaces, but will need to change
code.
SOLID
https://lostechies.com/derickbailey/2009/02/11/solid-development-
principles-in-motivational-pictures/ SOLID
This cannot be extended
public class DIPMessageProcessorController: Controller{
[HttpGet("sendusermessage/{userId}")]public string SendUserEmail(int userId){
IUserRepository userRepository = new UserSqlRepository(new UserSqlRepositoryConfig{
ConnectionString = Startup.Configuration.GetSection("ConnectionStrings:DefaultConnection").Value
});IUserServiceClient userServiceClient = new UserServiceClient(new UserServiceClientConfig{
UserCoordinatesUrl = Startup.Configuration.GetSection("UserApi:UserCoordinatesUrl").Value},new HttpClientHandler());IUserScamEligibility userScamEligibility = new NearbyUserScamEligibility(userServiceClient);IUserMessageSender userMessageSender = new SmtpUserMessageSender(
new SmtpUserMessageSenderConfig{
Host = Startup.Configuration.GetSection("Smtp:Host").Value,UserName = Startup.Configuration.GetSection("Smtp:UserName").Value,Password = Startup.Configuration.GetSection("Smtp:Password").Value,FromAddress = Startup.Configuration.GetSection("Smtp:FromAddress").Value
});IUserProcessorNoFactory userProcessor = new
UserProcessorNoFactory(userRepository,userScamEligibility,userMessageSender);userProcessor.SendUserMessageByUserId(userId);return "Scam Email Sent! Just wait for the riches to come pouring in...";
}
SOLID
New is Glue
Any time you use the new keyword, you are gluing your code to a particular
implementation. You are permanently (short of editing, recompiling, and
redeploying) hard-coding your application to work with a particular class’s
implementation. - Steve Smith, https://ardalis.com/new-is-glue
SOLID
This can be extended!
public class UserProcessor : IUserProcessor{
private readonly IUserRepository _userRepository;private readonly IUserScamEligibility _userScamEligibility;private readonly IUserMessageSender _userMessageSender;public UserProcessor(IUserRepository userRepository, IUserScamEligibility
userScamEligibility, IUserMessageSender userMessageSender){
this._userRepository = userRepository;this._userScamEligibility = userScamEligibility;_userMessageSender = userMessageSender;
}public bool SendUserMessageByUserId(int userId){
var user = _userRepository.GetUserContactById(userId);if (!_userScamEligibility.IsUserScamEligible(user)){
return false;}_userMessageSender.SendUserMessage(user);return true;
}}
SOLID
Create another strategy for business rules
public class ExtraditionUserScamEligibility: IUserScamEligibility{
public bool IsUserScamEligible(UserContact user){
return !CountryLikelyToSeekExtradition(user.Country);}
SOLID
Change strategy for UserMessageSender
public class TextUserMessageSender : IUserMessageSender{
public void SendUserMessage(UserContact user){
if (!IsMobilePhone(user.PhoneNumber)){
throw new SolidException("Cannot send text to non-mobile phone");}SendTextMessage(user.PhoneNumber);
}
SOLID
Remember that Strategy Pattern…Define a family of algorithms, encapsulate each one, and make them interchangeable.
Strategy lets the algorithm vary independently from clients that use it. – GOF
This is achieved via composition and not via inheritance.
SOLIDUML Diagrams - https://yuml.me/diagram/scruffy/class/draw
Just change 2 lines of code…
public void ConfigureServices(IServiceCollection services){
services.AddMvc();services.AddSingleton<IUserScamEligibility, ExtraditionUserScamEligibility>();services.AddSingleton<IUserMessageSender, TextUserMessageSender>();
…
SOLID
This has been EXTENDED
public class UserProcessor : IUserProcessor{
private readonly IUserRepository _userRepository;private readonly IUserScamEligibility _userScamEligibility;private readonly IUserMessageSender _userMessageSender;public UserProcessor(IUserRepository userRepository, IUserScamEligibility
userScamEligibility, IUserMessageSender userMessageSender){
this._userRepository = userRepository;this._userScamEligibility = userScamEligibility;_userMessageSender = userMessageSender;
}public bool SendUserMessageByUserId(int userId){
var user = _userRepository.GetUserContactById(userId);if (!_userScamEligibility.IsUserScamEligible(user)){
return false;}_userMessageSender.SendUserMessage(user);return true;
}}
SOLID
Polymorphism
Changing the underlying implementation of the type to give it different
behavior
Different types of Polymorphism
Subtype Polymorphism (inheritance based), which can be from a supertype that is
Abstract class
Concrete class
Interface
Duck Typing
Dynamic languages like JavaScript and Ruby
If it has the Quack() function, I can call it regardless of the type
Parametric Polymorphism
Generics with parameter polymorphism
List<T> open type with closed type as List<string>
Ad hoc Polymorphism
Method overloading
SOLID
The UPS as a Decorator
Mark Seemann. Dependency Injection in .NET
Extend a strategy with a Decorator
public class CacheDecoratorUserRepository : IUserRepository{
private readonly IUserRepository _userRepository;private readonly TimeSpan _timeToLive;private readonly ConnectionMultiplexer _redis;public CacheDecoratorUserRepository(IUserRepository userRepository,
CacheDecoratorUserRepositoryConfig config){
_userRepository = userRepository;_timeToLive = config.TimeToLive;_redis = ConnectionMultiplexer.Connect(config.ConfigurationString);
}public UserContact GetUserContactById(int userId){
//this would have multiple try/catch blocksIDatabase db = _redis.GetDatabase();string key = $"SolidCore:GetUserContactById:{userId}";string redisValue = db.StringGet(key);if (redisValue != null){
return JsonConvert.DeserializeObject<UserContact>(redisValue);}var user = _userRepository.GetUserContactById(userId);if (user == null){
return null;}string serializedValue = JsonConvert.SerializeObject(user);db.StringSet(key, serializedValue,_timeToLive);return user;
}} SOLID
The Decorator Pattern
Attach additional responsibilities to an object dynamically. Decorators provide a
flexible alternative to subclassing for extending functionality. - GoF
SOLIDUML Diagrams - https://yuml.me/diagram/scruffy/class/draw
.NET Core DI doesn’t directly support Decorator
services.AddSingleton<UserSqlRepository>();services.AddSingleton<CacheDecoratorUserRepositoryConfig>(provider =>
new CacheDecoratorUserRepositoryConfig{
ConfigurationString = _configuration.GetSection("Redis:ConfigurationString").Value,
TimeToLive = TimeSpan.Parse(_configuration.GetSection("Redis:TimeToLive").Value)
});services.AddSingleton<IUserRepository>(provider =>{
var userRepository = provider.GetService<UserSqlRepository>();var config = provider.GetService<CacheDecoratorUserRepositoryConfig>();return new CacheDecoratorUserRepository(userRepository, config);
});
SOLID
…you now have new functionality here!
public class UserProcessorNoFactory : IUserProcessorNoFactory{
private readonly IUserRepository _userRepository;private readonly IUserScamEligibility _userScamEligibility;private readonly IUserMessageSender _userMessageSender;public UserProcessorNoFactory(IUserRepository userRepository,
IUserScamEligibility userScamEligibility, IUserMessageSender userMessageSender){
this._userRepository = userRepository;this._userScamEligibility = userScamEligibility;_userMessageSender = userMessageSender;
}public bool SendUserMessageByUserId(int userId){
var user = _userRepository.GetUserContactById(userId);if (!_userScamEligibility.IsUserScamEligible(user)){
return false;}_userMessageSender.SendUserMessage(user);return true;
}}
SOLID
Dependency with short lifetime
public class SqlDbConnectionFactory : IDbConnectionFactory{
private readonly SqlDbConnectionFactoryConfig _config;
public SqlDbConnectionFactory(SqlDbConnectionFactoryConfig config){
_config = config;}public IDbConnection GetOpenConnection(){
var connection = new SqlConnection(_config.ConnectionString);connection.Open();return connection;
}}
SOLID
Client of DBConnectionFactory
public class UserSqlRepository : IUserRepository{
private readonly IDbConnectionFactory _dbConnectionFactory;
public UserSqlRepository(IDbConnectionFactory dbConnectionFactory){
_dbConnectionFactory = dbConnectionFactory;}public UserContact GetUserContactById(int userId){
UserContact userContact;using (var connection = _dbConnectionFactory.GetOpenConnection()){
userContact = connection.Query<UserContact>(@"SELECT p.FirstName, bea.BusinessEntityID,ea.EmailAddress,bea.AddressIDFROM Person.Person p INNER JOIN Person.EmailAddress ea ON
p.BusinessEntityID = ea.BusinessEntityIDINNER JOIN Person.BusinessEntityAddress bea ON ea.BusinessEntityID =
bea.BusinessEntityIDwhere ea.BusinessEntityID = @Id", new { Id = userId }).FirstOrDefault();
}return userContact;
}
New instance every call
using Abstract Factory
public class Sha512HashAlgorithmFactory : IHashAlgorithmFactory{
public HashAlgorithm GetInstance(){
return System.Security.Cryptography.SHA512.Create();}
}
SOLID
Abstract Factory to choose strategy
public class UserMessageSenderFactory : IUserMessageSenderFactory{
private readonly IEnumerable<IUserMessageSender> _userMessageSenders;
public UserMessageSenderFactory(IEnumerable<IUserMessageSender> userMessageSenders){
_userMessageSenders = userMessageSenders;}
public IUserMessageSender GetInstance(string messageType){
var userMessageSender = _userMessageSenders.FirstOrDefault(x =>x.MessageType.Equals(messageType, StringComparison.OrdinalIgnoreCase));
if (userMessageSender == null){
throw new SolidException("Invalid Message Type");}return userMessageSender;
}}
SOLID
DI just needed additional registrations…
services.AddSingleton<IUserMessageSender,SmtpUserMessageSender>();services.AddSingleton<IUserMessageSender, TextUserMessageSender>();services.AddSingleton<IUserMessageSenderFactory, UserMessageSenderFactory>();
SOLID
Change strategies based on input
public class UserProcessor : IUserProcessor{
private readonly IUserRepository _userRepository;private readonly IUserScamEligibility _userScamEligibility;private readonly IUserMessageSenderFactory _userMessageSenderFactory;public UserProcessor(IUserRepository userRepository, IUserScamEligibility
userScamEligibility, IUserMessageSenderFactory userMessageSenderFactory){
this._userRepository = userRepository;this._userScamEligibility = userScamEligibility;this._userMessageSenderFactory = userMessageSenderFactory;
}public bool SendUserMessageByUserId(int userId, string messageType){
var userMessageSender = _userMessageSenderFactory.GetInstance(messageType);var user = _userRepository.GetUserContactById(userId);if (!_userScamEligibility.IsUserScamEligible(user)){
return false;}userMessageSender.SendUserMessage(user);return true;
}}
SOLID
Abstract Factory
Provide an interface for creating families of related or dependent objects without
specifying their concrete classes. – GoF
It offers a good alternative to the complete transfer of control that’s involved in full
INVERSION OF CONTROL, because it partially allows the consumer to control the
lifetime of the DEPENDENCIES created by the factory; the factory still controls what is
being created and how creation happens.
- Mark Seemann. Dependency Injection in .NET (Kindle Locations 3628-3630). Manning
Publications.
SOLID
Don’t go overboard
Strategically choose what changes to close design against.
Resisting premature abstraction is as important as abstraction itself.
Over-conformance to the principles leads to the design smell of Needless
Complexity.
No significant program can be 100% closed.
- Uncle Bob, taken from books, articles, podcasts
I had a problem, so I tried to solve it with Java…
SOLID
LSP: Liskov Substitution Principle
The LSP can be paraphrased as follows:
Subtypes must be substitutable for their base types
Can’t add unexpected exceptions
Contravariance of method arguments in the subtype
Covariance of return types in the subtype
Preconditions cannot be strengthened in a subtype
Postconditions cannot be weakened in a subtype.
Invariants of the supertype must be preserved in a subtype.
History constraint
…which we’ll use to also mean implementations of interfaces must be
substitutable for other implementations
SOLID
store.deviq.com/products/software-craftsmanship-
calendars-2017-digital-image-pack SOLID
I have this Serializer
public interface ISerializer{
T DeserializeObject<T>(string value);string SerializeObject(object value);
}
public class JsonSerializer : ISerializer{
public T DeserializeObject<T>(string value){
return JsonConvert.DeserializeObject<T>(value);}public string SerializeObject(object value){
return JsonConvert.SerializeObject(value);}
}
SOLID
but I want to Serialize Binary
public class BinarySerializer : ISerializer{
public string SerializeObject(object value){
using (var stream = new MemoryStream()){
var formatter = new BinaryFormatter();formatter.Serialize(stream, value);stream.Flush();stream.Position = 0;return Convert.ToBase64String(stream.ToArray());
}}public T DeserializeObject<T>(string value){
throw new NotImplementedException();}
}SOLID
My LSP violation caused OCP violation!
public class DeserializeMaybe{
private readonly ISerializer _serializer;
public DeserializeMaybe(ISerializer serializer){
_serializer = serializer;}public User.User DeserializeUser(string serializedUser){
if(_serializer is JsonSerializer){
return _serializer.DeserializeObject<User.User>(serializedUser);}return null;
}}
SOLID
…and when I try to serialize my User…
another LSP violation
private string SerializeUser(User user){
return _serializer.SerializeObject(user);}
SOLID
ISP: Interface Segregation Principle
The ISP:
Clients should not be forced to depend on methods that they do not use.
Clients should not know about objects as a single class with a noncohesive interface.
SOLID
store.deviq.com/products/software-craftsmanship-
calendars-2017-digital-image-pack SOLID
Interface with multiple roles
public interface IMultiRoleUserRepository{
UserContact GetUserContactById(int userId);void InsertUser(User user);void UpdateEmailAddress(int userId, string emailAddress);
}
SOLID
Split the interface to single roles
public interface IUserContactReaderRepository{
UserContact GetUserContactById(int userId);}
public interface IUserWriterRepository{
void InsertUser(User user);void UpdateEmailAddress(int userId, string emailAddress);
}
SOLID
Repository implementing multiple roles
public class MultiRoleUserSqlRepository : IUserContactReaderRepository, IUserWriterRepository
{private readonly IDbConnectionFactory _dbConnectionFactory;public MultiRoleUserSqlRepository(IDbConnectionFactory dbConnectionFactory){
_dbConnectionFactory = dbConnectionFactory;}public UserContact GetUserContactById(int userId){
UserContact userContact;using (var connection = _dbConnectionFactory.GetOpenConnection()){
userContact = ...}return userContact;
}public void InsertUser(User user){
...}public void UpdateEmailAddress(int userId, string emailAddress){
...}
SOLID
Pure Functions – the ideal
The function always evaluates the same result value given the same argument
value(s). The function result value cannot depend on any hidden information
or state that may change while program execution proceeds or between
different executions of the program, nor can it depend on any external input
from I/O devices (usually—see below).
Evaluation of the result does not cause any semantically observable side
effect or output, such as mutation of mutable objects or output to I/O
devices (usually—see below).
https://en.wikipedia.org/wiki/Pure_function
Agile Manifesto
Individuals and interactions over processes and tools
Working software over comprehensive documentation
Customer collaboration over contract negotiation
Responding to change over following a plan
That is, while there is value in the items on the right, we value the items on the
left more.
A SOLID Manifesto
Composition over inheritance
Interface inheritance over implementation inheritance
Classes with state OR behavior over classes with state AND behavior
Singletons over multiple instances or statics
Volatile state scoped to functions over volatile state scoped to classes
That is, while there is value in the items on the right, I value the items on the
left more.
So what’s the downside?
Class and Interface Explosion
Complexity (increase in one kind of complexity)
I have an instance of the IService interface. What implementation am I using?
Constructor injection spreads like a virus
…but you now have maintainable
software using SOLID principles!
Testable!
What parts should you test?
What is Michael Feather’s definition of legacy code?
Easy to change
Particularly at the abstractions (the “seams” - Seemann)
Code can be reused
Design easy to preserve
Forces cognizance of dependencies
Requires much smaller mental model
Small functions with method injection isolated from outside world requires MUCH
SMALLER mental model of code. Complexity greatly reduced. Instead of having to
understand system, you can just understand THAT function. - Mark Seemann on
.NET Rocks podcast
https://lostechies.com/derickbailey/2009/02/11/solid-development-
principles-in-motivational-pictures/
We’ve turned our Jenga game…
…into a well built structure that’s easy
to maintain
Appendix The Pragmatic Programmer – Andrew Hunt and David Thomas
SOLID Jenga reference - http://www.codemag.com/article/1001061
Images noted from Steve Smith’s Software Craftsmanship Calendars - store.deviq.com/products/software-
craftsmanship-calendars-2017-digital-image-pack – used with express written permission
Composition root definition - http://www.dotnetcurry.com/patterns-practices/1285/clean-composition-roots-
dependency-injection
SOLID Motivational Pictures from Los Techies - https://lostechies.com/derickbailey/2009/02/11/solid-development-
principles-in-motivational-pictures/
Microsoft .NET Application Architecture - Architecting Modern Web Applications with ASP.NET Core and Azure
Mark Seemann. Dependency Injection in .NET
Feathers, Michael - Working Effectively With Legacy Code
UML Diagrams - https://yuml.me/diagram/scruffy/class/draw
IoC Definition - https://martinfowler.com/bliki/InversionOfControl.html
Pure Function - https://en.wikipedia.org/wiki/Pure_function
IoC Explained - https://martinfowler.com/bliki/InversionOfControl.html
New is Glue - https://ardalis.com/new-is-glue
Static Cling - http://deviq.com/static-cling/
6 ways to make a singleton - http://csharpindepth.com/articles/general/singleton.aspx
GoF - Design Patterns: Elements of Reusable Object-Oriented Software
Project layout