SOLID is an acronym that represents a set of five design principles for writing maintainable and flexible software. These principles aim to enhance the quality of object-oriented designs by promoting modular and understandable code. The SOLID principles include:
Single Responsibility Principle (SRP)
This principle states that a class should have only one reason to change, meaning it should have only one responsibility. This helps keep classes focused and makes them easier to maintain.
Example in C#:
// Bad design violating SRP
class Customer
{
public void AddToDatabase() { /* ... */ }
public void GenerateInvoice() { /* ... */ }
}
// Good design following SRP
class Customer
{
public void AddToDatabase() { /* ... */ }
}
class InvoiceGenerator
{
public void GenerateInvoice(Customer customer) { /* ... */ }
}
Open/Closed Principle (OCP)
This principle suggests that classes should be open for extension but closed for modification. In other words, you should be able to add new functionality without altering existing code.
Example in C#:
// Bad design violating OCP
class Logger
{
public void Write(string msg)
{
WriteToFile(msg);
WriteToDb(msg);
}
public void WriteToFile(string msg) { /* ... */ }
public void WriteToDB(string msg) { /* ... */ }
}
// Adding a new logging mechanism, e.g. WriteToConsole(), would require
//modifying existing Logger class
// Good design following OCP
interface ILogWriter
{
void Write(msg);
}
class FileLogWriter : ILogWriter
{
public void Write(string msg) { /* ... */ }
}
class DbLogWriter : ILogWriter
{
public void Write(string msg) { /* ... */ }
}
class Logger
{
List<ILogWriter> logWriters;
public void Write(string msg)
{
foreach(var writer in logWriters)
{
writer.Write(msg);
}
}
}
//We have a base ILogWriter interface, and every type of log writer implements it.
//The main Logger class is now no longer impacted even if a new logger is added.
// Adding a new logWriter is done by creating a new class implementing
//ILogWriter, and without modifying any of the existing classes.
Liskov Substitution Principle (LSP)
This principle states that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program.
Bad Design (Violation of LSP):
Suppose we have a class hierarchy for different types of birds. We have a base class Bird
and two subclasses Sparrow
and Ostrich
. The Bird
class has a method Fly()
which represents the flying behavior of birds.
//Bad Design (Violation of LSP)
class Bird
{
public virtual void Fly()
{
Console.WriteLine("This bird is flying.");
}
}
class Sparrow : Bird
{
public override void Fly()
{
Console.WriteLine("The sparrow is flying.");
}
}
class Ostrich : Bird
{
//Violation of Liskov Substitution Principle
public override void Fly()
{
Console.WriteLine("The ostrich is trying to fly, but it can't.");
}
}
In this design, the Ostrich
class is a subclass of Bird
, but ostriches cannot fly. So, the Ostrich
class provides an implementation for the Fly()
method that doesn't make sense.
Corrected Design (Using LSP):
To adhere to the Liskov Substitution Principle, we should rethink the design. Since ostriches cannot fly, it's better not to include them in the inheritance hierarchy of flying birds. Instead, we can have a separate hierarchy for flightless birds.
//Corrected Design (Using LSP)
//Bird no longer has Fly() method, it is moved to FlyingBird
class Bird
{
public virtual void MakeSound()
{
Console.WriteLine("This bird is making a sound.");
}
}
class FlyingBird : Bird
{
public virtual void Fly()
{
Console.WriteLine("This bird is flying.");
}
}
//Sparrow inherits from FlyingBird as it can fly
class Sparrow : FlyingBird
{
public override void Fly()
{
Console.WriteLine("The sparrow is flying.");
}
public override void MakeSound()
{
Console.WriteLine("The sparrow is chirping.");
}
}
//Ostrich inherits from Bird, so it need not implement Fly()
class Ostrich : Bird
{
public override void MakeSound()
{
Console.WriteLine("The ostrich is making a booming sound.");
}
}
In this corrected design, we have a FlyingBird
subclass that includes birds capable of flying. The Ostrich
class is not forced to provide an incorrect implementation of the Fly()
method, and instead, it focuses on its own behavior.
By adhering to the Liskov Substitution Principle, we ensure that subclass objects can be used interchangeably with superclass objects without causing unexpected behavior or violating the correctness of the program.
Another example of LSP:
//Bad Design (Violation of LSP)
class Vehicle
{
public virtual void StartEngine()
{
Console.WriteLine("This vehicle engine is starting.");
}
public virtual void Drive()
{
Console.WriteLine("Driving the vehicle.");
}
}
class Car : Vehicle
{
public override void StartEngine()
{
Console.WriteLine("This car engine is starting.");
}
public override void Drive()
{
Console.WriteLine("Driving the car.");
}
}
class Bicycle : Vehicle
{
/* Cannot start engine in a bycicle */
public override void StartEngine()
{
throw new NotImplementedException("Bicycle doesn't have an engine!")
}
public override void Drive()
{
Console.WriteLine("Pedaling the cycle.");
}
}
Obviously, a cycle doesn't have an engine, so StartEngine
cannot be implemented for it. The fix is as shown below - create a separate EngineVehicle
interface and move the StartEngine
method into it.
//Corrected Design (Using LSP)
//Vehicle no longer has StartEngine() method, it is moved to EngineVehicle
class Vehicle
{
public virtual void Drive()
{
Console.WriteLine("Driving the vehicle.");
}
}
class EngineVehicle : Vehicle
{
public virtual void StartEngine()
{
Console.WriteLine("This vehicle engine is starting.");
}
}
class Car : EngineVehicle
{
public override void StartEngine()
{
Console.WriteLine("This car engine is starting.");
}
public override void Drive()
{
Console.WriteLine("Driving the car.");
}
}
/* Bicycle inherits from Vehicle and only needs to implement Drive() */
class Bicycle : Vehicle
{
public override void Drive()
{
Console.WriteLine("Pedaling the cycle.");
}
}
Interface Segregation Principle (ISP)
This principle suggests that clients should not be forced to depend on interfaces they don't use. In other words, keep interfaces focused on specific functionality.
Example in C#:
// Bad design violating ISP
interface IWorker
{
void Work();
void Eat();
}
class Robot : IWorker
{
public void Work() { /* yes, robots work */ }
public void Eat()
{
// Robots don't eat, but we're forced to implement this method
throw new NotImplementedException("Robots don't eat.");
}
}
// Good design following ISP
interface IWorker
{
void Work();
}
interface IEater
{
void Eat();
}
// Robot only implements the relevant IWorker interface
class Robot : IWorker
{
public void Work() { /* ... */ }
}
//Programmer implements both IWorker and IEater
class Programmer: IWorker, IEater
{
public void Work() { /* ... */ }
public void Eat() { /* ... */ }
}
Dependency Inversion Principle (DIP)
This principle suggests that high-level modules should not depend on low-level modules, but both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.
In simpler terms, the principle encourages the use of abstractions (interfaces or abstract classes) to define relationships between higher-level and lower-level components, rather than directly depending on concrete implementations.
Example in C#:
// Bad design violating DIP
class LightBulb
{
public void TurnOn() { /* ... */ }
public void TurnOff() { /* ... */ }
}
class Switch
{
private LightBulb _bulb;
public Switch()
{
_bulb = new LightBulb();
}
public void Toggle()
{
if (_bulb.IsOn) _bulb.TurnOff();
else _bulb.TurnOn();
}
}
// Switch class is tightly coupled to LightBulb
// Good design following DIP
interface ISwitchable
{
void TurnOn();
void TurnOff();
}
class LightBulb : ISwitchable
{
public void TurnOn() { /* ... */ }
public void TurnOff() { /* ... */ }
}
class Switch
{
private ISwitchable _device;
public Switch(ISwitchable device)
{
_device = device;
}
public void Toggle()
{
if (_device.IsOn) _device.TurnOff();
else _device.TurnOn();
}
}
// Switch now depends on an abstraction (ISwitchable) rather
// than a concrete class
Another example:
// Bad design violating DIP
class SqlRepository
{
public void Connect() { /* ... */ }
public IEnumerable<Record> GetAll() { /* ... */ }
}
class DatabaseService
{
private SqlRepository _sql;
public DatabaseService()
{
_sql= new SqlRepository();
}
public IEnumerable<Record> ReadAllRecords()
{
_sql.Connect();
return _sql.GetAll();
}
}
// DatabaseService class is tightly coupled to SqlRepository
// Good design following DIP
interface IRepository
{
public void Connect();
public IEnumerable<Record> GetAll();
}
class SqlRepository : IRepository
{
public void Connect() { /* ... */ }
public IEnumerable<Record> GetAll() { /* ... */ }
}
class DatabaseService
{
private IRepository _repo;
public DatabaseService(IRepository repo)
{
_repo = repo;
}
public IEnumerable<Record> ReadAllRecords()
{
_repo.Connect();
return _repo.GetAll();
}
}
By adhering to the SOLID principles, developers can create software that is more modular, maintainable, and less prone to bugs when changes are made.