Understanding Liskov Substitution Principle with Real Construction Examples
Understanding Liskov Substitution
Principle with Real Construction Examples
In software development, especially
when building systems like construction expense tracking or invoice management
apps, it is crucial to ensure your object-oriented design is maintainable and
safe. One of the SOLID principles that guarantees reliability in inheritance is
the Liskov Substitution Principle (LSP).
The Liskov Substitution Principle
states that objects of a subclass should be replaceable for objects of the base
class without changing the correctness of the program. In practical terms,
wherever you use a base class, you should be able to plug in any of its derived
classes without unexpected behavior.
Let’s explore how LSP applies in
real-world construction scenarios like invoice processing, expense tracking,
and payment handling.
Key Principle:
Behavior
Preservation
A
subclass should behave like the base class in all scenarios.
No
Contradictions
Subclass
should not override methods with conflicting or unexpected behavior.
No Side
Effects
Using
subclass in place of base class must not cause errors or break logic.
Contract
Adherence
Subclass
must honor the base class's method preconditions and postconditions.
Safe
Polymorphism
You
should be able to use a derived class object anywhere a base class object is
expected.
Behavior Preservation
In a construction billing system,
we might have a base class called PaymentMethod that defines how a
payment is processed. Any subclass, such as BankTransfer, must preserve
the behavior of the base class.
class PaymentMethod
{
public
virtual string ProcessPayment(decimal amount) => $"Paid {amount}";
}
class BankTransfer :
PaymentMethod
{
public
override string ProcessPayment(decimal amount) => $"Bank Transfer:
{amount} successful";
}
void
PayContractor(PaymentMethod method, decimal amount)
{
Console.WriteLine(method.ProcessPayment(amount));
}
When BankTransfer is
used in place of PaymentMethod, it behaves exactly as expected. The
behavior is preserved, and LSP is satisfied.
No Contradictions
Now imagine we introduce a new
type of payment — credit payments. We implement it like this:
class CreditPayment :
PaymentMethod
{
public
override string ProcessPayment(decimal amount)
{
throw
new InvalidOperationException("Credit payment not allowed");
}
}
This would break the client code
that expects every PaymentMethod to process payments. Instead of
throwing an error from a subclass, we can redesign the hierarchy to avoid contradiction.
A better solution is to introduce
interfaces that group related behavior:
interface IPaymentMethod
{
string
ProcessPayment(decimal amount);
}
class CreditPayment
{
public
string RequestApproval(decimal amount) => $"Credit approval required
for {amount}";
}
This approach avoids using
inheritance where it doesn’t make sense and preserves expected behavior without
contradiction.
No Side Effects
Consider a base class Expense that
tracks costs in a construction project. If we extend it to create a MaterialExpense and
introduce side effects in property setters, we risk breaking LSP.
class Expense
{
public
virtual decimal Amount { get; set; }
}
class MaterialExpense :
Expense
{
public
override decimal Amount
{
get
=> base.Amount;
set
{
base.Amount
= value;
SendNotification();
// Unexpected side effect
}
}
private
void SendNotification()
{
Console.WriteLine("Email
sent for material expense.");
}
}
To fix this, the notification
logic should be moved outside the property setter. A service or separate method
should trigger it after the amount is set, ensuring the subclass does not
behave unexpectedly when used as an Expense.
class MaterialExpense : Expense
{
public
void SetAmountAndNotify(decimal amount)
{
Amount
= amount;
SendNotification();
}
private
void SendNotification()
{
Console.WriteLine("Email
sent for material expense.");
}
}
This preserves the purity of the
property setter and respects LSP.
Contract Adherence
Let’s look at invoice submission.
Suppose the base class Invoice allows submission for any amount
greater than zero. A derived class OnlineInvoice might restrict
submission to amounts above 1000, violating the contract.
class Invoice
{
public
virtual void Submit(decimal amount)
{
if
(amount <= 0)
throw
new ArgumentException("Amount must be positive");
Console.WriteLine("Invoice
submitted");
}
}
class OnlineInvoice :
Invoice
{
public
override void Submit(decimal amount)
{
if
(amount < 1000)
throw
new ArgumentException("Online invoice must be >= 1000");
base.Submit(amount);
}
}
This change introduces a stricter
precondition than the base class, breaking LSP. To fix this, the subclass must
honor or relax, but not tighten, the preconditions.
A correct version would validate
such business rules externally, not in the subclass.
class OnlineInvoice :
Invoice
{
public
override void Submit(decimal amount)
{
base.Submit(amount);
Console.WriteLine("Online
confirmation email sent");
}
}
Now, OnlineInvoice adheres
to the contract defined by Invoice.
Safe Polymorphism
A classic example of safe
polymorphism is using different types of expenses across your construction
system. Suppose we define a base class Expense, and extend it into LabourExpense and TransportExpense.
class Expense
{
public
virtual string GetDetails() => "Generic expense";
}
class LabourExpense :
Expense
{
public
override string GetDetails() => "Labour charges for site work";
}
class TransportExpense :
Expense
{
public
override string GetDetails() => "Transport of construction
materials";
}
void
PrintExpenseDetails(Expense expense)
{
Console.WriteLine(expense.GetDetails());
}
You can safely pass any subclass
of Expense to PrintExpenseDetails, and it will behave
consistently. This demonstrates LSP in action — subclasses are substitutable
without breaking behavior.
Final Thoughts
LSP is more than just a
theoretical concept. It helps you build robust, reliable, and maintainable code
in real-world applications such as construction invoice systems, expense
trackers, and contractor payment modules.
Whenever you’re designing with
inheritance, ask yourself: Can I replace the base class with this
subclass without any issues? If the answer is no, it’s time to
refactor.
Respecting LSP leads to better abstractions, clearer responsibilities, and code that's easier to test and extend.
Comments
Post a Comment