Inheritance, Composition, Aggregation, and Association with Liskov Substitution Principle (SOLID)
How Object-Oriented Relationships Align with SOLID Principles
Object-oriented programming (OOP) relies heavily on the relationships between classes and objects to build scalable, maintainable software. Among these relationships, inheritance, composition, aggregation, and association play pivotal roles. But how do they connect to one of the fundamental SOLID principles — the Liskov Substitution Principle (LSP)?
In this blog, we’ll explore these concepts, unravel their differences, and see how they align with the LSP to create robust, reusable software systems. Let’s dive in!
📌Explore more at: https://dotnet-fullstack-dev.blogspot.com/
🌟 Clapping would be appreciated! 🚀
The Four Pillars of Class Relationships
Before linking them to the Liskov Substitution Principle, let’s understand the foundational class relationships:
1. Inheritance 🧬
Inheritance is when one class (child) derives from another (parent), inheriting its behaviour and potentially overriding or extending it.
Example:
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal sound");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Bark");
}
}
Use Case: Sharing common functionality across related classes.
Key : Overusing inheritance can lead to tight coupling and violates LSP if the derived class doesn’t behave as expected.
2. Composition ⚙️
Composition involves using an object of one class within another class. This relationship models a “has-a” relationship.
Example:
public class Engine
{
public void Start() => Console.WriteLine("Engine started");
}
public class Car
{
private readonly Engine _engine = new Engine();
public void StartCar()
{
_engine.Start();
Console.WriteLine("Car is running");
}
}
Use Case: Building classes by combining multiple smaller components.
Benefit: Encourages flexibility and avoids the rigidity of inheritance.
3. Aggregation 🧩
Aggregation is a specialized form of association where one object is part of another but can exist independently. It’s often described as a “whole-part” relationship.
Example:
public class Department
{
public string Name { get; set; }
}
public class Company
{
public List<Department> Departments { get; set; } = new List<Department>();
}
Use Case: Modeling loosely coupled relationships between objects.
4. Association 🔗
Association is a general term for a relationship between two objects. This relationship can be one-to-one, one-to-many, or many-to-many.
Example:
public class Student
{
public string Name { get; set; }
}
public class Course
{
public string Title { get; set; }
public List<Student> EnrolledStudents { get; set; } = new List<Student>();
}
Use Case: Establishing links between entities without strict ownership or dependency.
Enter Liskov Substitution Principle (LSP) 🎯
The Liskov Substitution Principle (the “L” in SOLID) states:
“Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.”
In simpler terms:
- Subtypes must behave in ways their base types expect.
- Clients using a base type should not need to know which subclass they’re working with.
Violating LSP often happens in inheritance when:
- A derived class overrides behaviour in ways that are incompatible with the base class.
- Clients relying on the base class’s contract are broken by a subclass’s implementation.
How Relationships Align with LSP
Inheritance and LSP 🧬⚠️
- Challenge: Inheritance can violate LSP if a subclass changes or removes functionality expected by the parent class.
- Solution: Ensure derived classes adhere to the behaviour's promised by the base class.
Example of LSP Violation:
public class Bird
{
public virtual void Fly() => Console.WriteLine("Flying");
}
public class Ostrich : Bird
{
public override void Fly()
{
throw new NotImplementedException("Ostriches can't fly");
}
}
// Violation: Ostrich can't replace Bird for clients expecting Fly()
Fix it with Composition:
public interface IFlight
{
void Fly();
}
public class Bird
{
public string Name { get; set; }
}
public class Sparrow : Bird, IFlight
{
public void Fly() => Console.WriteLine("Flying");
}
public class Ostrich : Bird
{
// No Fly implementation; adheres to its behavior
}
Composition and LSP ⚙️✅
- Advantage: Composition avoids violating LSP because behaviours are explicitly added or removed through components, rather than relying on inheritance.
Example:
public interface IEngine
{
void Start();
}
public class ElectricEngine : IEngine
{
public void Start() => Console.WriteLine("Electric engine started");
}
public class GasolineEngine : IEngine
{
public void Start() => Console.WriteLine("Gasoline engine started");
}
public class Car
{
private readonly IEngine _engine;
public Car(IEngine engine) => _engine = engine;
public void StartCar()
{
_engine.Start();
Console.WriteLine("Car is running");
}
}
// Use cases
var electricCar = new Car(new ElectricEngine());
electricCar.StartCar();
var gasCar = new Car(new GasolineEngine());
gasCar.StartCar();
Why it works: You can replace IEngine
implementations without breaking the Car
class. This adheres to LSP.
Aggregation and LSP 🧩✅
Aggregation supports LSP by loosely coupling parts. Changes in aggregated objects don’t usually break their container.
Example:
A Department
can exist independently of a Company
. Adding or removing a department doesn’t violate the expected behaviour of the Company
.
public class Department
{
public string Name { get; set; }
}
public class Company
{
public List<Department> Departments { get; } = new List<Department>();
public void AddDepartment(Department department)
{
Departments.Add(department);
}
}
// No LSP violation; departments are independent entities
Association and LSP 🔗✅
Associations rarely conflict with LSP since they model relationships without ownership. As long as the associated objects behave as expected, LSP is preserved.
Example:
public class Teacher
{
public string Name { get; set; }
}
public class Classroom
{
public Teacher AssignedTeacher { get; set; }
}
// Replacing Teacher with a subclass does not break LSP
When to Choose What?
Wrapping It Up 🎉
The Liskov Substitution Principle is a cornerstone of creating flexible and maintainable object-oriented systems. By carefully choosing between inheritance, composition, aggregation, and association, you can build systems that align with LSP while ensuring scalability and reusability.
- Use inheritance sparingly and responsibly.
- Favour composition for flexibility and adherence to LSP.
- Leverage aggregation and association for loosely coupled designs.
Design smarter, code better, and let relationships in your code work for you, not against you! 😊 Happy coding! 🚀