0% found this document useful (0 votes)
10 views16 pages

SOLID Principles Using CSharp

SOLID is an acronym for five design principles that enhance code maintainability and flexibility in object-oriented programming. These principles, introduced by Robert C. Martin, include Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion, each addressing specific issues in software design. Following SOLID principles leads to better code organization, improved testability, and reduced technical debt.

Uploaded by

decodetechstack
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
10 views16 pages

SOLID Principles Using CSharp

SOLID is an acronym for five design principles that enhance code maintainability and flexibility in object-oriented programming. These principles, introduced by Robert C. Martin, include Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion, each addressing specific issues in software design. Following SOLID principles leads to better code organization, improved testability, and reduced technical debt.

Uploaded by

decodetechstack
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

WHAT ARE SOLID PRINCIPLES?

Introduction

SOLID is a mnemonic acronym for five design principles that help you create maintainable, scalable, and flexible
code.

These principles, introduced by Robert C. Martin (Uncle Bob), are essential guidelines for writing clean code in
object-oriented programming.

Why SOLID Matters?


Let's say you created an application that works perfectly today. But after 6 months, you need to add new features.
When you try to modify the code:

Problems Start Appearing:

Small change breaks something else


Adding features becomes slower

Understanding the code becomes harder


Fixing bugs introduces new bugs

Testing is complicated

That is when developers face the struggle of technical debt.

We need principles to write code that's easy to change, easy to test, and easy to understand.

THE FIVE PRINCIPLES


S - SINGLE RESPONSIBILITY PRINCIPLE (SRP)
"A class should have only one reason to change."

This means each class should have only one job or responsibility.

The Problem:

// ❌ VIOLATES SRP - Multiple Responsibilities


public class User
{
public string Username { get; set; }
public string Email { get; set; }

// Responsibility 1: User Management


public void Register()
{
// Save to database
[Link]("Saving user to database...");
}

// Responsibility 2: Email Sending


public void SendWelcomeEmail()
{
// Send email logic
[Link]("Sending welcome email...");
}

// Responsibility 3: Password Hashing


public string HashPassword(string password)
{
// Hashing logic
return "hashed_" + password;
}
}

Problems:

If registration logic changes, we modify User class


If email template changes, we modify User class
If hashing algorithm changes, we modify User class
3 reasons to change = 3 responsibilities!

The Solution:

// ✅ FOLLOWS SRP - Single Responsibility Each


public class User
{
public string Username { get; set; }
public string Email { get; set; }

// Only responsible for User data


public void Register()
{
[Link]("Registering user...");
}
}

public class EmailService


{
// Only responsible for sending emails
public void SendWelcomeEmail(User user)
{
[Link]($"Sending welcome email to {[Link]}");
}
}

public class PasswordService


{
// Only responsible for password hashing
public string HashPassword(string password)
{
return "hashed_" + password;
}
}

Benefits:
✨ Easier Testing - Test each responsibility independently

✨ Better Maintainability - Change one thing without affecting others

✨ Clearer Code - Each class has obvious purpose


✨ Reusability - Easier to reuse in different contexts

O - OPEN/CLOSED PRINCIPLE (OCP)


"Software should be open for extension, closed for modification."

This means you should add new features without modifying existing code.

The Problem:

// ❌ VIOLATES OCP - Must modify existing code for new types


public class NotificationService
{
public void SendNotification(string type, string message)
{
if (type == "Email")
{
SendEmail(message);
}
else if (type == "SMS")
{
SendSMS(message);
}
else if (type == "Push")
{
SendPushNotification(message);
}
// Add new notification type? MODIFY THIS CLASS!
}

private void SendEmail(string message) =>


[Link]($"Email: {message}");

private void SendSMS(string message) =>


[Link]($"SMS: {message}");

private void SendPushNotification(string message) =>


[Link]($"Push: {message}");
}

Problems:

Adding Slack notification requires modifying NotificationService

Adding WhatsApp requires modifying NotificationService


Every new type means changing existing code
Risk of breaking existing functionality

The Solution:

// ✅ FOLLOWS OCP - Extend without modifying


public interface INotificationChannel
{
void Send(string message);
}

public class EmailNotification : INotificationChannel


{
public void Send(string message) =>
[Link]($"Email: {message}");
}

public class SMSNotification : INotificationChannel


{
public void Send(string message) =>
[Link]($"SMS: {message}");
}

public class PushNotification : INotificationChannel


{
public void Send(string message) =>
[Link]($"Push: {message}");
}

// Add new notification WITHOUT modifying existing code


public class SlackNotification : INotificationChannel
{
public void Send(string message) =>
[Link]($"Slack: {message}");
}

public class NotificationService


{
private readonly INotificationChannel _channel;

public NotificationService(INotificationChannel channel)


{
_channel = channel;
}

public void SendNotification(string message)


{
_channel.Send(message); // Uses any notification type
}
}

// Usage
var emailNotification = new EmailNotification();
var service = new NotificationService(emailNotification);
[Link]("Hello!");

Benefits:

✨ Safe to Extend - Add features without breaking existing code

✨ Reduced Risk - Existing functionality never modified

✨ Easier Testing - Test new implementations independently

✨ Better Design - Forces use of abstractions

L - LISKOV SUBSTITUTION PRINCIPLE (LSP)

"Subtypes must be substitutable for their base types."

This means a derived class should work correctly wherever its base class is used.
The Problem:

// ❌ VIOLATES LSP - Square breaks Rectangle contract


public class Rectangle
{
public virtual double Width { get; set; }
public virtual double Height { get; set; }

public virtual double CalculateArea()


{
return Width * Height;
}
}

public class Square : Rectangle


{
// Square: all sides must be equal
private double _side;

public override double Width


{
get => _side;
set => _side = value; // Sets both width and height
}

public override double Height


{
get => _side;
set => _side = value; // Sets both width and height
}
}

// Usage - PROBLEM!
Rectangle shape = new Square { Width = 5, Height = 10 };
// We set Width=5 and Height=10, but Square makes them both 10
double area = [Link](); // 100, not 50!
// Expected 50, got 100 - BROKEN CONTRACT!

Problems:

Square doesn't behave like Rectangle

Substituting Square for Rectangle breaks code


Violates the Liskov contract

The Solution:

// ✅ FOLLOWS LSP - Proper interface design


public interface IShape
{
double CalculateArea();
}

public class Rectangle : IShape


{
public double Width { get; set; }
public double Height { get; set; }

public double CalculateArea()


{
return Width * Height;
}
}

public class Square : IShape


{
public double Side { get; set; }

public double CalculateArea()


{
return Side * Side;
}
}

// Usage - All shapes work correctly


IShape rectangle = new Rectangle { Width = 5, Height = 10 };
IShape square = new Square { Side = 5 };

[Link]([Link]()); // 50 ✓
[Link]([Link]()); // 25 ✓

// Can substitute any IShape without breaking


void PrintArea(IShape shape)
{
[Link]($"Area: {[Link]()}");
}

PrintArea(rectangle); // Works!
PrintArea(square); // Works!

Benefits:

✨ Reliable Substitution - Derived classes behave predictably

✨ Contract Honored - Base class expectations met

✨ Polymorphism Works - Can use base type safely

✨ Predictable Code - No surprises when substituting

I - INTERFACE SEGREGATION PRINCIPLE (ISP)

"Clients should not depend on interfaces they don't use."

This means create specific interfaces rather than one large interface.

The Problem:

// ❌ VIOLATES ISP - Forces unnecessary implementation


public interface IWorker
{
void Work();
void Eat();
void Sleep();
}

public class HumanWorker : IWorker


{
public void Work() => [Link]("Human working...");
public void Eat() => [Link]("Human eating...");
public void Sleep() => [Link]("Human sleeping...");
}
public class RobotWorker : IWorker
{
public void Work() => [Link]("Robot working...");

// Robot doesn't eat or sleep! But forced to implement them


public void Eat() => throw new NotImplementedException();
public void Sleep() => throw new NotImplementedException();
}

Problems:

RobotWorker forced to implement unnecessary methods


Methods throw NotImplementedException
Interface too broad for all clients

The Solution:

// ✅ FOLLOWS ISP - Segregated, specific interfaces


public interface IWorker
{
void Work();
}

public interface ILivingBeing


{
void Eat();
void Sleep();
}

public class HumanWorker : IWorker, ILivingBeing


{
public void Work() => [Link]("Human working...");
public void Eat() => [Link]("Human eating...");
public void Sleep() => [Link]("Human sleeping...");
}

public class RobotWorker : IWorker


{
public void Work() => [Link]("Robot working...");
// No unnecessary Eat() or Sleep() methods!
}

// Usage
IWorker humanWorker = new HumanWorker();
IWorker robotWorker = new RobotWorker();

[Link](); // ✓ Works
[Link](); // ✓ Works

// For living beings


ILivingBeing human = new HumanWorker();
[Link](); // ✓ Works
[Link](); // ✓ Works
Benefits:
✨ Focused Interfaces - Only what's needed

✨ No Empty Implementations - No dummy methods

✨ Flexible Design - Classes implement only relevant interfaces

✨ Better Clarity - Clear contract for each interface

D - DEPENDENCY INVERSION PRINCIPLE (DIP)

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

This means depend on abstractions (interfaces), not concrete implementations.

The Problem:

// ❌ VIOLATES DIP - Depends on concrete implementations


public class FileLogger
{
public void Log(string message)
{
// Log to file
[Link]($"File Log: {message}");
}
}

public class UserService


{
private FileLogger _logger; // Direct dependency!

public UserService()
{
_logger = new FileLogger(); // Hard-coded dependency
}

public void CreateUser(string name)


{
_logger.Log($"Creating user: {name}");
// Create user logic
}
}

Problems:

UserService tightly coupled to FileLogger


Cannot use DatabaseLogger without changing UserService

Hard to test - must use real FileLogger


Cannot swap logging implementation

The Solution:

// ✅ FOLLOWS DIP - Depends on abstractions


public interface ILogger
{
void Log(string message);
}
public class FileLogger : ILogger
{
public void Log(string message) =>
[Link]($"File Log: {message}");
}

public class DatabaseLogger : ILogger


{
public void Log(string message) =>
[Link]($"DB Log: {message}");
}

public class ConsoleLogger : ILogger


{
public void Log(string message) =>
[Link]($"Console Log: {message}");
}

public class UserService


{
private readonly ILogger _logger; // Depend on abstraction

public UserService(ILogger logger) // Injected dependency


{
_logger = logger;
}

public void CreateUser(string name)


{
_logger.Log($"Creating user: {name}");
// Create user logic
}
}

// Usage - Can swap any logger


ILogger fileLogger = new FileLogger();
ILogger dbLogger = new DatabaseLogger();
ILogger consoleLogger = new ConsoleLogger();

var service1 = new UserService(fileLogger);


var service2 = new UserService(dbLogger);
var service3 = new UserService(consoleLogger);

[Link]("John"); // Uses FileLogger


[Link]("Jane"); // Uses DatabaseLogger
[Link]("Bob"); // Uses ConsoleLogger

Benefits:

✨ Loose Coupling - Classes independent of implementations

✨ Easy to Test - Mock logger for testing

✨ Flexible Implementation - Swap implementations easily

✨ Better Maintainability - Changes isolated to specific class


SOLID PRINCIPLES SUMMARY TABLE
Principle Focus Problem Solution

SRP Single Responsibility Multiple reasons to change One responsibility per class

OCP Open/Closed Must modify existing code Extend without modification

LSP Liskov Substitution Derived class breaks contract Derived class honors base contract

ISP Interface Segregation Force unused implementations Segregated, specific interfaces

DIP Dependency Inversion Depends on concrete classes Depend on abstractions

BENEFITS OF FOLLOWING SOLID


✅ Better Code Maintainability
Clear responsibility for each class

Easy to understand code organization


Simple to locate and modify features

✅ Improved Testability
Unit test components independently

Mock dependencies easily


Test in isolation without database or external services

✅ Enhanced Flexibility
Add features without breaking existing code
Swap implementations easily

Adapt to changing requirements quickly

✅ Reduced Technical Debt

Prevent code smells by design


Avoid tight coupling from the start
Save time on refactoring later

✅ Better Team Collaboration


Clear interfaces between components
Less code conflicts when working together
Easier onboarding for new developers
Well-defined responsibilities for each person
✅ Increased Reusability
Decouple components for reuse
Use in multiple projects easily
Reduce duplicate code

COMMON VIOLATIONS & HOW TO FIX


Violation 1: God Classes
Problem: One class doing too much

// ❌ Bad - God Class


public class UserManager
{
public void Register() { }
public void Login() { }
public void SendEmail() { }
public void ProcessPayment() { }
public void LogActivity() { }
public void ValidateEmail() { }
public void UpdateProfile() { }
}

Fix: Split into focused classes

// ✅ Good - Separated Responsibilities


public class UserRegistration { }
public class UserAuthentication { }
public class EmailService { }
public class PaymentProcessor { }
public class ActivityLogger { }
public class EmailValidator { }
public class UserProfileManager { }

Violation 2: Tight Coupling


Problem: Direct dependencies on concrete classes

// ❌ Bad - Tightly Coupled


public class OrderService
{
private OrderDatabase _db = new OrderDatabase();
private EmailService _email = new EmailService();

public void CreateOrder(Order order)


{
_db.Save(order);
_email.SendConfirmation(order);
}
}

Fix: Use dependency injection with interfaces


// ✅ Good - Loosely Coupled
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _email;

public OrderService(IOrderRepository repository,


IEmailService email)
{
_repository = repository;
_email = email;
}

public void CreateOrder(Order order)


{
_repository.Save(order);
_email.SendConfirmation(order);
}
}

Violation 3: Fat Interfaces


Problem: Interfaces with methods not all clients need

// ❌ Bad - Fat Interface


public interface IDocument
{
void Open();
void Close();
void Save();
void Print();
void Scan();
void Fax();
}

public class TextFile : IDocument


{
public void Open() { }
public void Close() { }
public void Save() { }
public void Print() { }
public void Scan() { throw new NotImplementedException(); } // Unwanted!
public void Fax() { throw new NotImplementedException(); } // Unwanted!
}

Fix: Segregate into specific interfaces

// ✅ Good - Segregated Interfaces


public interface IOpenable
{
void Open();
void Close();
}

public interface ISaveable


{
void Save();
}

public interface IPrintable


{
void Print();
}

public interface IScannable


{
void Scan();
}

public class TextFile : IOpenable, ISaveable, IPrintable


{
public void Open() { }
public void Close() { }
public void Save() { }
public void Print() { }
// Only implements what it needs!
}

Violation 4: Switch/If-Else Chains

Problem: Multiple conditional branches for different types

// ❌ Bad - Switch Statement (Violates OCP)


public decimal CalculateDiscount(Customer customer)
{
switch ([Link])
{
case [Link]:
return 0.05m;
case [Link]:
return 0.15m;
case [Link]:
return 0.25m;
default:
return 0m;
}
// Adding new customer type? Must modify this method!
}

Fix: Use polymorphism

// ✅ Good - Polymorphism (Follows OCP)


public interface ICustomer
{
decimal GetDiscount();
}

public class RegularCustomer : ICustomer


{
public decimal GetDiscount() => 0.05m;
}

public class PremiumCustomer : ICustomer


{
public decimal GetDiscount() => 0.15m;
}

public class VIPCustomer : ICustomer


{
public decimal GetDiscount() => 0.25m;
}
public decimal CalculateDiscount(ICustomer customer)
{
return [Link]();
// Add new customer type? Just create new class!
}

REAL-WORLD ANALOGY
Think of a Restaurant Kitchen:

Without SOLID (Chaos):

One chef does everything: cooking, cleaning, accounting, hiring


Chef must learn all skills
If chef quits, everything stops
Hard to add new dishes

With SOLID (Well-Organized):

SRP: Head Chef → Sauce Chef → Pastry Chef (each has one job)

OCP: Add new dish without changing existing recipes


LSP: Any sauce chef can substitute for another

ISP: Each station has specific tools needed


DIP: Operations depend on job descriptions, not specific people

Result: Efficient, Scalable, Flexible Kitchen!

APPLYING SOLID IN YOUR .NET PROJECTS


Step 1: Identify Classes with Multiple Responsibilities
List what each class does

If more than one reason to change → Split it

Step 2: Create Abstractions


Define interfaces for behavior
Depend on interfaces, not concrete classes

Step 3: Use Dependency Injection

Constructor inject dependencies


Use .NET DI container
Register interfaces with implementations
Step 4: Refactor Gradually
SOLID isn't all-or-nothing
Refactor one principle at a time
Test after each change

Step 5: Code Review with SOLID in Mind

Do classes have single responsibility?


Can we extend without modification?
Can we substitute implementations?
Are interfaces focused?
Do we depend on abstractions?

TOOLS & LIBRARIES


Dependency Injection Containers
[Link] - Built-in .NET

Autofac - Advanced DI features


Ninject - Simple and powerful

StructureMap - Convention-based DI

Code Analysis Tools


SonarAnalyzer - Detect violations

FxCop - Static code analysis


StyleCop - Code style enforcement

R# (ReSharper) - IntelliSense for SOLID

Testing Frameworks

xUnit - Unit testing


NUnit - Microsoft testing
Moq - Mocking framework
FakeItEasy - Mocking and stubbing

KEY TAKEAWAYS
✨ SRP = One responsibility per class

✨ OCP = Open for extension, closed for modification

✨ LSP = Derived classes honor base contracts

✨ ISP = Specific interfaces, not fat interfaces

✨ DIP = Depend on abstractions, not concrete classes

✨ Start Simple = Apply gradually, not all at once


✨ Understand the Why = Not just the what

✨ Use Interfaces = Foundation of all SOLID principles

✨ Test Everything = SOLID code is testable

✨ Refactor Continuously = Improve over time

SO YOU WILL ALWAYS...


Write code that's easy to understand

Make changes safely without breaking things

Add features quickly without fear

Test components independently

Maintain your codebase effortlessly

Scale your application confidently

Welcome new developers with clarity

Reduce bugs through better design

SOLID Principles = Professional Code = Happy Developers!

You might also like