Skip to content

rashedulalam46/solid-principles

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

22 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SOLID Principales


πŸ’³ SOLID Principles in C# – Payment Processing Example

This project demonstrates how to apply the SOLID principles in a simple Payment Processing System using C#.
The goal is to show how following SOLID makes code cleaner, maintainable, and extensible.


πŸ“– Overview

The system processes orders using different payment methods and generates invoices.
It applies all five SOLID principles:

  1. Single Responsibility Principle (SRP)
    Each class has one responsibility.

    • OrderRepository β†’ Saves orders
    • PdfInvoice / EmailInvoice β†’ Generate invoices
    • CheckoutService β†’ Manages checkout process
  2. Open/Closed Principle (OCP)
    The system is open for extension but closed for modification.

    • New payment methods (e.g., CreditCardPayment, PayPalPayment, BitcoinPayment) can be added without changing existing code.
  3. Liskov Substitution Principle (LSP)
    Subclasses can replace their parent classes without breaking behavior.

    • Any IPaymentProcessor (Credit Card, PayPal, Bitcoin) can be used in CheckoutService.
  4. Interface Segregation Principle (ISP)
    Clients are not forced to implement methods they don’t use.

    • IInvoiceGenerator is separate from IPaymentProcessor.
  5. Dependency Inversion Principle (DIP)
    High-level modules depend on abstractions, not concrete implementations.

    • CheckoutService depends on IPaymentProcessor and IInvoiceGenerator interfaces.

S - Single Responsibility Principle

// Each class has only one reason to change.
public class Order
{
    public string OrderId { get; set; }
    public decimal Amount { get; set; }
}

public class OrderRepository
{
    public void Save(Order order)
    {
        Console.WriteLine($"Order {order.OrderId} saved to database.");
    }
}

O - Open/Closed Principle

// Add new payment methods without modifying existing logic.
public interface IPaymentProcessor
{
    void ProcessPayment(Order order);
}

public class CreditCardPayment : IPaymentProcessor
{
    public void ProcessPayment(Order order)
    {
        Console.WriteLine($"Processing Credit Card payment for {order.Amount}");
    }
}

public class PayPalPayment : IPaymentProcessor
{
    public void ProcessPayment(Order order)
    {
        Console.WriteLine($"Processing PayPal payment for {order.Amount}");
    }
}

L - Liskov Substitution Principle

// Subtypes (payment methods) can replace the parent type (IPaymentProcessor).
public class BitcoinPayment : IPaymentProcessor
{
    public void ProcessPayment(Order order)
    {
        Console.WriteLine($"Processing Bitcoin payment for {order.Amount}");
    }
}

I - Interface Segregation Principle

// Instead of one big interface, we separate responsibilities.

public interface IInvoiceGenerator
{
    void GenerateInvoice(Order order);
}

public class PdfInvoice : IInvoiceGenerator
{
    public void GenerateInvoice(Order order)
    {
        Console.WriteLine($"PDF invoice generated for order {order.OrderId}");
    }
}

public class EmailInvoice : IInvoiceGenerator
{
    public void GenerateInvoice(Order order)
    {
        Console.WriteLine($"Invoice emailed for order {order.OrderId}");
    }
}

D - Dependency Inversion Principle

// High-level modules depend on abstractions, not concretions.

public class CheckoutService
{
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly IInvoiceGenerator _invoiceGenerator;
    private readonly OrderRepository _orderRepository;

    public CheckoutService(IPaymentProcessor paymentProcessor, IInvoiceGenerator invoiceGenerator, OrderRepository orderRepository)
    {
        _paymentProcessor = paymentProcessor;
        _invoiceGenerator = invoiceGenerator;
        _orderRepository = orderRepository;
    }

    public void Checkout(Order order)
    {
        _paymentProcessor.ProcessPayment(order);
        _invoiceGenerator.GenerateInvoice(order);
        _orderRepository.Save(order);
    }
}

πŸš€ Example Run

Order order = new Order { OrderId = "123", Amount = 100.50m };

// Choose payment and invoice types
IPaymentProcessor payment = new PayPalPayment();
IInvoiceGenerator invoice = new PdfInvoice();

// Inject dependencies into CheckoutService
CheckoutService checkout = new CheckoutService(payment, invoice, new OrderRepository());

// Process checkout
checkout.Checkout(order);

Output

Processing PayPal payment for 100.50
PDF invoice generated for order 123
Order 123 saved to database.

βœ… How SOLID is Applied Here:

  • SRP β†’ OrderRepository only saves orders, CheckoutService only manages checkout, Invoice classes only handle invoices.

  • OCP β†’ We can add new payments (e.g., StripePayment) without changing existing classes.

  • LSP β†’ Any payment type (CreditCard, PayPal, Bitcoin) can replace IPaymentProcessor without breaking behavior.

  • ISP β†’ Invoices are split (PdfInvoice, EmailInvoice), not forced into one big interface.

  • DIP β†’ CheckoutService depends on abstractions (IPaymentProcessor, IInvoiceGenerator) instead of concrete classes.

    This way, the system is flexible, testable, and extendable.

πŸ”΄ Breaking OCP (not open for extension, but modification)

public class PaymentProcessor
{
    public void ProcessPayment(Order order, string paymentType)
    {
        if (paymentType == "CreditCard")
        {
            Console.WriteLine($"Processing Credit Card payment for {order.Amount}");
        }
        else if (paymentType == "PayPal")
        {
            Console.WriteLine($"Processing PayPal payment for {order.Amount}");
        }
        else if (paymentType == "Bitcoin")
        {
            Console.WriteLine($"Processing Bitcoin payment for {order.Amount}");
        }
        // ❌ Every time we add a new payment type (Stripe, ApplePay),
        // we must MODIFY this class β†’ violates OCP
    }
}

Problem:

  • Adding StripePayment means we must edit PaymentProcessor.
  • The class keeps growing and becomes harder to maintain.

πŸ”΄ Breaking LSP Example

High chance of introducing bugs while modifying.

public interface IPaymentProcessor
{
    void ProcessPayment(Order order);
}

Now, imagine we create a GiftCardPayment class but force it to implement ProcessPayment, even though gift cards don’t work like normal payments:

public class GiftCardPayment : IPaymentProcessor
{
    public void ProcessPayment(Order order)
    {
        // ❌ GiftCard can’t really process payments like CreditCard/PayPal
        // So we throw an exception
        throw new NotSupportedException("GiftCard cannot process payment directly.");
    }
}

public void Checkout(Order order)
{
    _paymentProcessor.ProcessPayment(order); // ❌ Will crash if GiftCardPayment is used
}

Why this breaks LSP?

  • LSP rule: Subclasses (or implementations) must be usable anywhere the base type is expected.
  • Here, if we substitute GiftCardPayment for IPaymentProcessor, it throws an exception instead of behaving properly.
  • Client code (CheckoutService) now has to know special cases β†’ violates LSP.

βœ… Fixing LSP

Instead of forcing GiftCardPayment into the wrong interface, we restructure abstractions:

public interface IPaymentProcessor
{
    void ProcessPayment(Order order);
}

public interface IGiftCardRedemption
{
    void RedeemGiftCard(Order order);
}

public class CreditCardPayment : IPaymentProcessor
{
    public void ProcessPayment(Order order)
    {
        Console.WriteLine($"Processing Credit Card payment for {order.Amount}");
    }
}

public class PayPalPayment : IPaymentProcessor
{
    public void ProcessPayment(Order order)
    {
        Console.WriteLine($"Processing PayPal payment for {order.Amount}");
    }
}

public class GiftCardRedemption : IGiftCardRedemption
{
    public void RedeemGiftCard(Order order)
    {
        Console.WriteLine($"Redeeming gift card for {order.Amount}");
    }
}

βœ… Why this respects LSP?

  • Each implementation behaves correctly without throwing exceptions.
  • GiftCardRedemption is no longer pretending to be a normal payment processor.
  • Substitution works: anywhere you expect IPaymentProcessor, you can safely use CreditCardPayment or PayPalPayment without breaking behavior.

πŸ”΄ Breaking ISP

Suppose we design one fat interface that tries to handle every possible invoice format:

public interface IInvoiceGenerator
{
    void GeneratePdfInvoice(Order order);
    void GenerateEmailInvoice(Order order);
    void GenerateExcelInvoice(Order order);
}

Now, classes are forced to implement methods they don’t need:

public class PdfInvoiceGenerator : IInvoiceGenerator
{
    public void GeneratePdfInvoice(Order order)
    {
        Console.WriteLine($"PDF invoice generated for order {order.OrderId}");
    }

    public void GenerateEmailInvoice(Order order)
    {
        // ❌ Not applicable β†’ forced to implement
        throw new NotImplementedException();
    }

    public void GenerateExcelInvoice(Order order)
    {
        // ❌ Not applicable β†’ forced to implement
        throw new NotImplementedException();
    }
}

🚨 Why this breaks ISP?

  • Classes should not be forced to depend on methods they don’t use.

  • Here, PdfInvoiceGenerator only cares about PDF, but is forced to implement email and Excel too.

  • Any change in IInvoiceGenerator impacts all implementations unnecessarily.

  • βœ… Fixing ISP

Split the interface into smaller, more specific interfaces:

public interface IPdfInvoiceGenerator
{
    void GeneratePdfInvoice(Order order);
}

public interface IEmailInvoiceGenerator
{
    void GenerateEmailInvoice(Order order);
}

public interface IExcelInvoiceGenerator
{
    void GenerateExcelInvoice(Order order);
}

Now, each implementation only does what it should:

public class PdfInvoiceGenerator : IPdfInvoiceGenerator
{
    public void GeneratePdfInvoice(Order order)
    {
        Console.WriteLine($"PDF invoice generated for order {order.OrderId}");
    }
}

public class EmailInvoiceGenerator : IEmailInvoiceGenerator
{
    public void GenerateEmailInvoice(Order order)
    {
        Console.WriteLine($"Invoice emailed for order {order.OrderId}");
    }
}

βœ… Why this respects ISP?

  • Each class depends only on what it really needs.
  • No unnecessary throw new NotImplementedException().
  • Easier to maintain and extend.

πŸ”΄ Breaking DIP

CheckoutService does depend on abstractions (IPaymentProcessor, IInvoiceGenerator), which is good (follows DIP). If it did not depend on abstractions, it would look like this (bad example ❌):

public class CheckoutService
{
    private readonly CreditCardPayment _creditCardPayment;
    private readonly PdfInvoice _pdfInvoice;
    private readonly OrderRepository _orderRepository;

    public CheckoutService()
    {
        // Directly instantiating concrete classes (bad practice)
        _creditCardPayment = new CreditCardPayment();
        _pdfInvoice = new PdfInvoice();
        _orderRepository = new OrderRepository();
    }

    public void Checkout(Order order)
    {
        _creditCardPayment.ProcessPayment(order);
        _pdfInvoice.GenerateInvoice(order);
        _orderRepository.Save(order);
    }
}

🚨 What’s wrong here?

  • Tight coupling β†’ CheckoutService is locked to CreditCardPayment and PdfInvoice.
  • No flexibility β†’ If we want to switch to PayPalPayment or EmailInvoice, we must modify CheckoutService.
  • Hard to test β†’ Can’t mock dependencies easily in unit tests.

βœ… Why this version is better:

  • Depends on interfaces (abstractions).
  • Easy to swap different implementations without modifying CheckoutService.
  • Easy to test with mocks.
public CheckoutService(IPaymentProcessor paymentProcessor, IInvoiceGenerator invoiceGenerator, OrderRepository orderRepository)

So the difference is:

  • Without DIP β†’ CheckoutService creates and depends on concrete classes.
  • With DIP β†’ CheckoutService depends on interfaces/abstractions provided from outside (injected).

About

SOLID Principles in C#

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages