History:
In our previous posts we learned ‘What is S.O.L.I.D. Programing Principles’ and a detailed explanation with code of Single Responsibility Principle and Open/closed Principle.
S.O.L.I.D. is an acronym introduced by Michael Feathers as:
1. S for SRP: Single responsibility principle
2. O for OCP: Open/closed principle
3. L for LSP: Liskov substitution principle
4. I for ISP: Interface segregation principle
5. D for DIP: Dependency inversion principle
- Single Responsibility Principle says, class should have single responsibility. In reference to this I would say “A class should have single responsibility”.
Lets dive into ocean – can we read this like “a class should not design to do multiple activities”. - Open/Closed Principle says, “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”.
Learning S.O.L.I.D is very vast topic and can’t possible to explore in one-shot. I divided this into following parts:
Introduction
In this whole article, we will learn Liskov Substitution Principle in details with example.
Here is definition from wiki :
“if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T)
without altering any of the desirable properties of that program (correctness, task performed, etc.)”
Learning Liskov substitution principle (LSP)
I understood above definition like this: parent should easily replace the child object.
Lets explore this with an example:Go back to code discussed during our previous session and suppose we have several databases, some of these should not be migrated
Take a look into following snippet:
public class DataBase
{
public virtual bool IsValid(ServerData data, SourceServerData sourceData)
{
return new Validator(new List<IValidator>()).Validate(data, sourceData);
}
public virtual void Save()
{
}
}
public class ProdDB : DataBase
{
public override bool IsValid(ServerData data, SourceServerData sourceData)
{
return base.IsValid(data, sourceData);
}
public override void Save()
{
base.Save();
}
}
public class QADB : DataBase
{
public override bool IsValid(ServerData data, SourceServerData sourceData)
{
return base.IsValid(data, sourceData);
}
public override void Save()
{
base.Save();
}
}
public class LocalDB : DataBase
{
public override bool IsValid(ServerData data, SourceServerData sourceData)
{
return base.IsValid(data, sourceData);
}
public override void Save()
{
throw new Exception("Local Data should not be saved!");
}
}
Recall, inheritance and you can visualize that DataBase is a parent class of ProdDB, QADB and LocalDB.
Lets think polymorphism for a while and we can write as:
DataBase pDataBase = new ProdDB();
DataBase qDataBase = new QADB();
DataBase lDataBase = new LocalDB();
var dataBases = new List<DataBase> {new ProdDB(), new QADB(), new LocalDB()};
Isn’t it easy to save my object using this:
var dataBases = new List<DataBase> {new ProdDB(), new QADB(), new LocalDB()};
foreach (var dataBase in dataBases)
{
if (dataBase.IsValid(data, sourceData))
dataBase.Save();
}
Wait, wait…
Whats wrong in above, NOTHING?
Yes, you are absolutely correct there is nothing wrong with above code, the only thing is, its execution. When above code execute, it will also invoke save method
of LocalDB object. In this case we received an exception as our LocalDB object is not supposed to save data.
A big question is “why this happened?”
In simple words LocalDB is actually not an entity of DataBase or we can say DataBase is not an actual parent of LocalDB.
Another question in mind “Why LocalDB is not an entity of DataBase, when it inherits DataBase“.
Hold on, go back to LocalDB class and check this is not meant to implement Save() method, here this makes LocalDB as a separate entity.
In simple words, LISCOV says parent should easily replace its child.
How to implement LISCOV principle?
We know LocalDB is not supposed to save data but others are. Lets consider following snippet:
public interface IRule
{
bool IsValid(ServerData data, SourceServerData sourceData);
}
public interface IRepository
{
void Save();
}
Now, we have two interfaces, with their own methods. IRule: to validate data and IRepository: to save/persist data.
Lets make changes to our LocalDB class, as:
public class LocalDB : IRule
{
public bool IsValid(ServerData data, SourceServerData sourceData)
{
return new Validator(new List<IValidator>()).Validate(data, sourceData);
}
}
Why we implement IRule?
For LocalDB, we only want to check whether data is valid or not. We do not want to persist data in any scenario.
Now, we can’t write this:
DataBase lDataBase = new LocalDB();
Our DataBase class, should be like this:
public class DataBase : IRule, IRepository
{
public virtual bool IsValid(ServerData data, SourceServerData sourceData)
{
return new Validator(new List<IValidator>()).Validate(data, sourceData);
}
public virtual void Save()
{
}
}
Other classes will remain unchanged.
Our execution code goes as:
public void Execute(ServerData data, SourceServerData sourceData)
{
var dataBases = new List<IRepository> { new ProdDB(), new QADB() };
foreach (var dataBase in dataBases.Where(dataBase => ((IRule)dataBase).IsValid(data, sourceData)))
{
dataBase.Save();
}
}
Now, our code is looks too easier to handle
How to download source-code?
You can download complete souce code of examples used in this article from GitHub: Learning Solid.