Agile Project Management Education | Courses and Interactive Agile Workshops

Writing Clean and Lean Code in C# (C-Sharp) with SOLID principles

Writing Clean and Lean Code in C# (C-Sharp) with SOLID principles

I usually write blog posts about Agile Project Management, covering topics like Agile leadership, Scrum Master roles, Product Ownership, coaching and mentorship. These areas have always been my passion, helping teams and individuals excel in their Agile journeys. However, recently, I’ve been thinking about broadening the scope of my writing to include some technical topics too, something that can directly support developers in their day-to-day work. Since I often engage with software development teams, I felt it would be valuable to dive into the technical side of things and share practical insights. To extend into this new direction, I chose the SOLID principles in C#, a foundation for writing clean, maintainable code that every developer can benefit from.

Lean code emphasizes simplicity, maintainability, and efficiency. In the context of C# Object-Oriented Programming (OOP), adhering to the SOLID principles is a proven approach to achieve these goals. These principles help developers create robust and flexible software by encouraging practices that reduce ‘code smells’ (any characteristic in the source code of a program that possibly indicates a deeper problem), prevent bugs, and facilitate scalability.

This article will explain the SOLID principles with practical C# examples.

What is Lean Code?

Lean code is concise and focused, avoiding redundancy while maintaining clarity. In software development, lean code:

  1. Enhances readability.
  2. Reduces complexity.
  3. Simplifies debugging and maintenance.

The Role of SOLID Principles

The SOLID principles are a set of five design guidelines that promote lean, clean, and object-oriented code. These principles are:

  • S: Single Responsibility Principle (SRP)
  • O: Open/Closed Principle (OCP)
  • L: Liskov Substitution Principle (LSP)
  • I: Interface Segregation Principle (ISP)
  • D: Dependency Inversion Principle (DIP)

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one responsibility.

Think about this: Would you ask your phone to make a call, order pizza, and do your homework all at once? Probably not! In programming, the Single Responsibility Principle (SRP) means a class should do just one thing.

// NOT SRP-compliant: One class is doing too much!
public class InvoiceManager
{
    public void ProcessInvoice() 
    {
        // Process logic 
    }
    public void SaveToDatabase() 
    { 
        // Database logic 
    }
    public void SendEmail() 
    { 
        // Email logic 
    }
}

Here, InvoiceManager has three responsibilities: processing invoices, saving and sending emails. To comply with SRP, split these into separate classes:

// SRP-compliant: Each class has a single responsibility!
public class InvoiceProcessor
{
    public void ProcessInvoice()
    { 
        // Process logic 
    }
}

public class InvoiceRepository
{
    public void SaveToDatabase() 
    { 
        // Database logic 
    }
}

public class EmailService
{
    public void SendEmail()
    { 
        // Email logic 
    }
}

2. Open/Closed Principle (OCP)

Definition: A class should be open for extension but closed for modification.

Imagine this: You’re building a house. Would you break down the walls every time you wanted to add a new room? No way! The Open/Closed Principle (OCP) says: Build your code so it’s open for extension, but closed for modification.

Example: Payment Processing

Here’s a bad example where adding a new payment method means editing existing code:

// Not OCP compliant: Adding new payment types breaks existing code. PaymentProcessor
{
    public void ProcessPayment(string paymentType)
    {
        if (paymentType == "CreditCard")
        {
            // Credit Card logic
        }
        else if (paymentType == "PayPal")
        {
            // PayPal logic
        }
    }
}

To comply with OCP, let’s fix this with polymorphism so we can add new payment methods without touching the existing ones:

// OCP-compliant: Extend functionality through inheritance
public interface IPayment
{
    void ProcessPayment();
}

public class CreditCardPayment : IPayment
{
    public void ProcessPayment()
    {
        // Credit Card logic
    }
}

public class PayPalPayment : IPayment
{
    public void ProcessPayment()
    {
        // PayPal logic
    }
}

public class PaymentProcessor
{
    public void ProcessPayment(IPayment payment)
    {
        payment.ProcessPayment();
    }
}

//Now, you can add new payment methods by just creating a new class.

3. Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.

Think about this: A square is a rectangle, but a rectangle isn’t always a square. The Liskov Substitution Principle (LSP) says that a child class should behave like its parent class, without breaking the program.

Example: Shapes

Here’s a bad example where substituting a square for a rectangle causes issues:

// Without LSP: Derived class violates base class behavior
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int Area => Width * Height;
}

public class Square : Rectangle
{
    public override int Width
    {
        set { base.Width = base.Height = value; }
    }

    public override int Height
    {
        set { base.Width = base.Height = value; }
    }
}

This violates LSP because substituting Square for Rectangle causes unexpected behavior.

Solution: Refactor using composition instead of inheritance.

public interface IShape
{
    int Area { get; }
}

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public int Area => Width * Height;
}

public class Square : IShape
{
    public int Side { get; set; }
    public int Area => Side * Side;
}

Now, both shapes behave correctly and independently.

4. Interface Segregation Principle (ISP)

Definition: A class should not be forced to implement interfaces it does not use.

Think about this: A vending machine shouldn’t force you to learn all its functions just to get a snack! The Interface Segregation Principle (ISP) says: Don’t force classes to implement interfaces they don’t use.

Example: Printers

Here’s an example where a basic printer is forced to implement unnecessary methods:

// Without ISP: Interface forces unnecessary implementation
public interface IPrinter
{
    void Print();
    void Scan();
    void Fax();
}

public class SimplePrinter : IPrinter
{
    public void Print()
    {
        // Print Logic
    }

    public void Scan()
    {
        throw new NotImplementedException();
    }

    public void Fax()
    {
        throw new NotImplementedException();
    }
}

Solution: Split the interface into smaller, more focused ones, where each class implements only what it needs.

// With ISP: Interfaces are segregated
public interface IPrint
{
    void Print();
}

public interface IScan
{
    void Scan();
}

public class SimplePrinter : IPrint
{
    public void Print()
    {
        // Print Logic
    }
}

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Imagine this: A chef doesn’t buy ingredients directly; they rely on suppliers. The Dependency Inversion Principle (DIP) says: High-level modules should depend on abstractions, not on low-level details.

Example: Logging

Here’s a bad example where the application depends directly on a logger:

// Without DIP: High-level module depends on low-level module
public class DatabaseLogger
{
    public void Log(string message)
    {
        // Log to database
    }
}

public class Application
{
    private DatabaseLogger _logger = new DatabaseLogger();

    public void Run()
    {
        _logger.Log("Application is running");
    }
}

Solution: Use dependency injection and abstraction.

// With DIP: Depend on abstractions
public interface ILogger
{
    void Log(string message);
}

public class DatabaseLogger : ILogger
{
    public void Log(string message)
    {
        // Log to database
    }
}

public class Application
{
    private readonly ILogger _logger;

    public Application(ILogger logger)
    {
        _logger = logger;
    }

    public void Run()
    {
        _logger.Log("Application is running");
    }
}

Now, the application works with any logger that implements ILogger!

By following the SOLID principles, you can write lean, scalable, and maintainable code in C#. Each principle addresses a common code design pitfall, making your codebase robust and adaptive to changes. Embracing these principles ensures that your code remains clean, efficient, and professional.

Share on

0
    0
    Your Learning Cart
    Your cart is emptyBack to Explore
    Scroll to Top