Abstract
This
article is the 5th of a series of seven
articles I wrote (described later on) about The Principles of Object
Oriented Design, also known as SOLID.
This
post describes the Liskov Substitution Principle (LSP).
As
a prerequisite, please read the first 2 articles:
As
architects and solution designers we’d like to design and write robust &
clean solutions.
We’d
like our code to be stable, maintainable, testable and scalable for as long as
our product lives.
We’d
like to implement and integrate new features as smoothly as possible in our
existing (already tested and deployed) code, without any regression issues, and
by maintaining dead-lines and production deployments.
In
addition, we’d like our code to perform according to standard performance KPIs,
and eventually, to serves its purpose.
Such
desire has many obstacles, especially as our application evolves and our
product become more complex.
More
often than we’d like, we are experiencing these obstacles which could be
described as software design smells.
The
purpose of the SOLID principles is to achieve these desires and to prevent
software design smells.
In
this article series we’ll emphasize the importance of well-designed
applications.
We’ll
show how, by maintaining the SOLID principles in our code, we’ll create better
robust solutions, provide easier code maintenance, faster bug fixing and the
overall impact on the application performance, especially while adding new
features to existing design, as the application evolves with new requirements.
The complete series:
Intro:
S.O.L.I.D
Content
- Introduction
- Case Study
-
Requirements
- Implementation that does NOT
conform to the LSP
1.
BOs
implementation
2.
Builders
implementation
3.
The ServiceManager
class implementation
- Implementation that conforms to the LSP
1.
BOs
implementation
2.
Builders
implementation
3.
The ServiceManager
class implementation
- Inheritance & the LSP
-
How should we determine a proper inheritance?
- Summary
Introduction
The Liskov substitution principle
states that:
Subtypes must be
substitutable for their base types.
This means that base types could be
replaced by their corresponding derived types without manipulating or breaking
the code.
Simply put, we could use the base
type in our code to invoke all its corresponding derived types.
Seemingly, this determination seems
very implicit and simple to accomplish, we should only create a base type,
derive from it, and use it in our code when needed.
However, we’ll illustrate how a
simple inheritance, when created incorrectly, would violate the LSP, and
as a result would also violate the OCP, and eventually would cause our
application to suffer from software design smells.
Remark:
When our design does NOT conform to the LSP it usually also
violates the OCP.
Case
Study
Consider the following requirements
and different implementations:
Requirements:
1. We have 3 business
objects:
1.
Person
2.
Employee
3.
Supplier
Person
is the base class of Employee
and Supplier.
2. We need to
create a Service Manager class that invokes a service.
3.
This service receives one of our BOs and builds a
request respectively.
4. Meaning, for
each BO the request would be built differently.
Implementation
that does NOT conform to the LSP
One possible implementation that violates
the LSP and as a result also violates the OCP is the following:
1.
BOs
implementation:
using
DesignPrinciplesExamples.LSP.RequestBuilders;
namespace
DesignPrinciplesExamples.LSP.BOs
{
public class Person
{
public int Id { get; set; }
public PersonRequestBuilder PersonRequestBuilder { get; set; }
public Person()
{
PersonRequestBuilder = new PersonRequestBuilder();
}
}
}
|
using
DesignPrinciplesExamples.LSP.RequestBuilders;
namespace
DesignPrinciplesExamples.LSP.BOs
{
public class Employee : Person
{
public EmployeeRequestBuilder RequestBuilder { get; set; }
public Employee()
{
RequestBuilder = new EmployeeRequestBuilder();
}
}
}
|
using
DesignPrinciplesExamples.LSP.RequestBuilders;
namespace
DesignPrinciplesExamples.LSP.BOs
{
public class Supplier : Person
{
public SupplierRequestBuilder Builder { get; set; }
public Supplier()
{
Builder = new SupplierRequestBuilder();
}
}
}
|
Description:
-
We could notice only from the BOs implementation that
using these ‘request builders’ also violates the OCP. (as described in the ‘The
Open-Closed Principle’ article I posted)
2. Builders implementation:
namespace
DesignPrinciplesExamples.LSP.RequestBuilders
{
public class PersonRequestBuilder
{
public string BuildRequest()
{
// TODO: Need to
implement.
return null;
}
}
}
|
namespace
DesignPrinciplesExamples.LSP.RequestBuilders
{
public class EmployeeRequestBuilder
{
public string Build()
{
// TODO: Need to implement.
return null;
}
}
}
|
namespace
DesignPrinciplesExamples.LSP.RequestBuilders
{
public class SupplierRequestBuilder
{
public string BuildSupplierRequest()
{
// TODO: Need to
implement.
return null;
}
}
}
|
Description:
-
We could see that every builder was implemented in a
different manner, without uniformity or a base class, in addition, every method
was named differently.
-
It looks as if this implementation was done by different
programmers, perhaps on different times, without any code integration process.
-
Later on, when I’ll provide the proper implementation
that conforms to the LSP, I’ll also change the ‘requests builders’
implementation.
3. The ServiceManager
class implementation:
using
DesignPrinciplesExamples.LSP.BOs;
namespace
DesignPrinciplesExamples.LSP
{
public class ServiceManager
{
public void InvokeService(Person person)
{
var request = BuildRequest(person);
// TODO: Need to
implement.
//try
//{
// service.Invoke(request);
//}
//catch
(System.Exception)
//{
// throw;
//}
}
// Private methods
private string BuildRequest(Person person)
{
string request = null;
// Verify which type is
the 'person'
// and build the request
respectively.
if (person is Employee)
{
Employee employee = person as Employee;
request =
employee.RequestBuilder.Build();
}
else if (person is Supplier)
{
Supplier supplier = person as Supplier;
request =
supplier.Builder.BuildSupplierRequest();
}
else
{
request =
person.PersonRequestBuilder.BuildRequest();
}
return request;
}
}
}
|
Description:
-
The ServiceManager class contains a method to invoke
the service, and builds the request according to the objects that it receives.
-
The ‘BuildRequest()’ method violates the LSP and as a
result also violates the OCP.
-
We could see that instead of using polymorphism,
we are using an if statement to verify which BO was sent, and building the
request accordingly.
-
This, of course, would have been prevented with a proper
design that uses correct inheritance and polymorphism implementation.
-
And for each new BO we’ll add (with new requirement) we
would also have to modify the ServiceManager implementation, which obviously,
violates the OCP.
Implementation
that conforms to the LSP
There are different ways to design
these requirements with respect to the LSP and OCP principles, one possible
implementation is the following:
1. BOs implementation:
using
DesignPrinciplesExamples.LSP.RequestBuilders;
using
DesignPrinciplesExamples.LSP.RequestBuilders.Base;
namespace
DesignPrinciplesExamples.LSP.BOs
{
public class Person
{
public int Id { get; set; }
private BaseRequestBuilder _requestBuilder;
// Injecting the required 'Request Builder'
// when initializing the person class.
public Person(BaseRequestBuilder requestBuilder = null)
{
// Setting a default
'request builder'
// in case it wasn't
provided.
_requestBuilder = requestBuilder
?? SetDefaultRequestBuilder();
}
// Protected virtual methods
protected virtual BaseRequestBuilder SetDefaultRequestBuilder()
{
return new PersonRequestBuilder();
}
// Public virtual methods
public virtual string BuildRequest()
{
return
_requestBuilder.BuildRequest();
}
}
}
|
Description:
-
The Person class contains a BaseRequestBuilder object.
-
In the constructor we are injecting the desired ‘Request
Builder’ and setting a default value, in case it wasn’t provided.
-
We are using the virtual SetDefaultRequestBuilder()
method to set the ‘Request Builder’ default value, thus derived classes could
override it and implement with their own default ‘Request Builder’.
-
This design conforms to the OCP.
-
In addition, we added the virtual BuildRequest()
method which invokes the BuildRequest() method of the current ‘Request
Builder’.
-
This polymorphic design conforms to the LSP, since
derived types could use this method and won’t need to create their own
implementation.
-
In addition the BuildRequest() method was set as
virtual in case future BOs would like to modify it, e.g. add relevant log
messages.
using
DesignPrinciplesExamples.LSP.RequestBuilders;
using
DesignPrinciplesExamples.LSP.RequestBuilders.Base;
namespace DesignPrinciplesExamples.LSP.BOs
{
public class Employee : Person
{
public Employee(BaseRequestBuilder requestBuilder = null) :
base(requestBuilder)
{
}
// Overrindg the 'SetDefaultRequestBuilder()' method in
case the
// requestBuilder property wasn't provided in the constructor.
// This method is invoked in the 'Person' base class's
constructor.
protected override BaseRequestBuilder SetDefaultRequestBuilder()
{
return new EmployeeRequestBuilder();
}
}
}
|
using
DesignPrinciplesExamples.LSP.RequestBuilders;
using
DesignPrinciplesExamples.LSP.RequestBuilders.Base;
namespace
DesignPrinciplesExamples.LSP.BOs
{
public class Supplier : Person
{
public Supplier(BaseRequestBuilder requestBuilder = null) :
base(requestBuilder)
{
}
// Overrindg the 'SetDefaultRequestBuilder()' method in
case the
// requestBuilder property wasn't provided in the
constructor.
// This method is invoked in the 'Person' base class's
constructor.
protected override BaseRequestBuilder SetDefaultRequestBuilder()
{
return new SupplierRequestBuilder();
}
}
}
|
Description:
-
Both the Employee and Supplier classes derive from the Person class and implemented the SetDefaultRequestBuilder()
method with their own default ‘request builders’.
Remark: Open-Closed
Principle
As
stated above, I mentioned that this design conforms to the OCP, since we
could extend the behavior of the Person, Employee and Supplier objects, in order to use a
different builder, without modifying their code, only by implementing a new
builder that derives from BaseRequestBuilder and use it instead.
However, if we’ll deeply examine these
classes, we could notice that their ‘SetDefaultRequestBuilder()’ methods are not closed for
modification, since in case we’d like to use another ‘default builder’, we’ll
have to modify these methods.
We
could probably create another object (e.g. CreateDefaultRequestBuilder) that sets the ‘default builder’
for each BO respectively, perhaps by using configuration for each BO.
This
kind of decision should be addressed with respect to the particular
requirements, meaning, whether we expect the ‘default builder’ to consistently
stay the same, or will it be frequently changed, and the implementation
should be accordingly.
In
case we predict it to change frequently, we should support the OCP, and implement
such an object: CreateDefaultRequestBuilder.
Otherwise,
it would be over design & over engineering.
|
2. Builders implementation:
namespace
DesignPrinciplesExamples.LSP.RequestBuilders.Base
{
public abstract class BaseRequestBuilder
{
public abstract string BuildRequest();
}
}
|
using
DesignPrinciplesExamples.LSP.RequestBuilders.Base;
namespace
DesignPrinciplesExamples.LSP.RequestBuilders
{
public class PersonRequestBuilder : BaseRequestBuilder
{
public override string BuildRequest()
{
// TODO: Need to
implement.
return null;
}
}
}
|
using
DesignPrinciplesExamples.LSP.RequestBuilders.Base;
namespace
DesignPrinciplesExamples.LSP.RequestBuilders
{
public class EmployeeRequestBuilder : BaseRequestBuilder
{
public override string BuildRequest()
{
// TODO: Need to
implement.
return null;
}
}
}
|
using DesignPrinciplesExamples.LSP.RequestBuilders.Base;
namespace
DesignPrinciplesExamples.LSP.RequestBuilders
{
public class SupplierRequestBuilder : BaseRequestBuilder
{
public override string BuildRequest()
{
// TODO: Need to
implement.
return null;
}
}
}
|
Description:
-
In this implementation we created an inheritance
hierarchy between the builders, in order to create uniformity between all the
request builders.
-
In addition, this conforms to the OCP, since in case of
a change in the builders’ requirements, we would only need to extend the code,
and implement a new proper builder.
Remark:
In
case we’ll get the builder from someone else (3rd party) that didn’t
derive from our BaseRequestBuilder, we could
always write a proper wrapper or adapter to that builder class, which
would derive from our BaseRequestBuilder and thus would
conform to our design.
3. The ServiceManager
class implementation:
using
DesignPrinciplesExamples.LSP.BOs;
namespace DesignPrinciplesExamples.LSP
{
public class ServiceManager
{
public void InvokeService(Person person)
{
var request =
person.BuildRequest();
// TODO: Need to
implement.
//try
//{
// service.Invoke(request);
//}
//catch
(System.Exception)
//{
// throw;
//}
}
}
}
|
Description:
-
We could see that in this implementation we are using polymorphism
and only invoking the person’s BuildRequest() method, which invokes the
proper builder.
Inheritance
& the LSP
How
should we determine a proper inheritance?
Meaning, how do we know whether a
particular class should inherit from another base class?
Well, I would say that this question
is at the heart of designing a solution that conforms to the LSP.
When designing a class that inherits
from another base class, the derived class would inherit all the data members
and functionality of the base class (in addition to its own new data members
and functionality).
Sometimes, it’s very easy to identify
a proper inheritance, but other times, not quite.
Meaning, as young developers, when we
learned object oriented programing, we were taught that a class should inherit
from another class if it is considered the same as the base class, but with
additional functionalities.
E.g. an Employee Is A Person,
thus it should inherit all its functionality.
However,
this is not quite accurate, since some designs that rely on the Is A relationship could miss the behavior of both classes, meaning their behavior
is different, even though they are the same.
For instance, conceptually, an
Employee is the same as Person, however in our application (based on the
requirements), an Employee behaves differently, e.g. a Person uses a service to
receive all persons and the Employee class doesn’t need this behavior.
One thing that could indicate
different behaviors is that the derived class inherits data members and
functionality that it doesn’t need.
A proper inheritance that conforms to
the LSP should be considered if the derived class has the same behavior as
the base class.
This way, we could use polymorphism without
breaking the code!
Summary
We managed to illustrate a simple use
of the Liskov Substitution Principle and to understand its importance when
designed correctly in our application.
We saw how, by creating a design that does
NOT conform to the Liskov Substitution Principle, it would also violate the
Open-Closed Principle, and our application would eventually suffer from software
design smells.
Finally, we understood that in order
to create a proper inheritance between our objects, they don’t have to
be conceptually the same, rather they should have the same behavior.
To conclude, I would say that the LSP
is very important to our design, in order to use object oriented
polymorphism correctly, without breaking our code, and most importantly we
need to remember that the S.O.L.I.D design principles are well-used when they
are applied together and across the entire codebase.
---
Next in
the SOLID series is the Interface Segregation Principle, which describes how to
design small and efficient interfaces, as opposed to big and
inefficient interfaces, which would lead to undesired coupling between the
interface’s clients, and eventually to software design smells.
The End
Hope you enjoyed!
Appreciate your comments…
Yonatan Fedaeli
No comments:
Post a Comment