DEV Community

Cover image for C#: Interfaces and Design Patterns
Taki
Taki

Posted on • Edited on

C#: Interfaces and Design Patterns

πŸ”Ή Interfaces and Design Patterns

🎯 Objective

  • To explore how interfaces can decouple code
  • To demonstrate design patterns that leverage interfaces: Strategy and Factory
  • To promote substitutability, a core idea behind SOLID principles (especially the Open/Closed Principle and Dependency Inversion Principle)

πŸ”Έ 1. Why Use Interfaces?

Interfaces define behavioral contracts without specifying implementation.

❌ Bad Practice (Tightly Coupled)

public class OrderProcessor {
    private readonly CreditCardPaymentProcessor _paymentProcessor = new CreditCardPaymentProcessor();

    public void Process(Order order) {
        _paymentProcessor.Process(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

This class depends on a concrete implementation, which makes it:

  • Hard to test
  • Hard to extend (e.g., support PayPal, Stripe, etc.)

βœ… Good Practice (Decoupled via Interface)

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

public class CreditCardPaymentProcessor : IPaymentProcessor {
    public void Process(Order order) {
        // Payment logic
    }
}

public class OrderProcessor {
    private readonly IPaymentProcessor _paymentProcessor;

    public OrderProcessor(IPaymentProcessor paymentProcessor) {
        _paymentProcessor = paymentProcessor;
    }

    public void Process(Order order) {
        _paymentProcessor.Process(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now:

  • OrderProcessor is reusable and extensible
  • You can inject different payment processors (Strategy Pattern)
  • Unit testing becomes easy with mock IPaymentProcessor

πŸ”Έ 2. Strategy Pattern

Definition: Allows you to change an algorithm's behavior at runtime by injecting different strategy implementations.

Used here to inject different behaviors for IPaymentProcessor.

public class PayPalPaymentProcessor : IPaymentProcessor {
    public void Process(Order order) {
        // PayPal logic
    }
}

// Inject the strategy at runtime
var orderProcessor = new OrderProcessor(new PayPalPaymentProcessor());
orderProcessor.Process(order);
Enter fullscreen mode Exit fullscreen mode

Key Idea: Swap behaviors without changing OrderProcessor.


πŸ”Έ 3. Factory Pattern

Definition: Encapsulates object creation logic. Useful when:

  • You need to decide which implementation to instantiate
  • You want to hide complex creation logic from the consumer
public class PaymentProcessorFactory {
    public IPaymentProcessor GetProcessor(string paymentMethod) {
        switch (paymentMethod) {
            case "creditcard": return new CreditCardPaymentProcessor();
            case "paypal": return new PayPalPaymentProcessor();
            default: throw new NotSupportedException();
        }
    }
}

// Usage
var factory = new PaymentProcessorFactory();
var processor = factory.GetProcessor("creditcard");
var orderProcessor = new OrderProcessor(processor);
Enter fullscreen mode Exit fullscreen mode

This separates object creation from business logic, keeping things clean and extendable.


πŸ”Έ 4. Dependency Inversion Principle (D from SOLID)

This principle is emphasized in Chapter 3:

  • High-level modules (like OrderProcessor) should not depend on low-level modules (like CreditCardPaymentProcessor).
  • Both should depend on abstractions (IPaymentProcessor).

This makes systems:

  • Easier to extend (add new payment methods)
  • Easier to test (mock interfaces)
  • More maintainable

πŸ”Έ 5. Interface vs Abstract Class in C

Feature Interface Abstract Class
Multiple inheritance βœ… Yes ❌ No
Constructors ❌ Not allowed βœ… Allowed
Default implementation βœ… (C# 8+) βœ…
Use case Behavioral contract Shared implementation

Gary recommends favoring interfaces when:

  • You’re defining pluggable behavior
  • You want to support multiple inheritance
  • You want to use Strategy, Factory, or other behavioral patterns

πŸ’‘ Key Takeaways

  • Interfaces enable polymorphism and are fundamental to Agile architecture
  • You should program to abstractions, not implementations
  • Patterns like Strategy and Factory are powerful when combined with interfaces
  • Avoid directly instantiating dependencies (use DI + Factories)
  • Design code to be closed for modification, but open for extension

βœ… Example Refactor (Factory + Strategy + Interface)

// Interface
public interface IShippingCalculator {
    decimal CalculateShipping(Order order);
}

// Concrete Strategies
public class StandardShipping : IShippingCalculator {
    public decimal CalculateShipping(Order order) => 5;
}

public class ExpressShipping : IShippingCalculator {
    public decimal CalculateShipping(Order order) => 15;
}

// Factory
public class ShippingCalculatorFactory {
    public IShippingCalculator Create(string type) {
        return type switch {
            "standard" => new StandardShipping(),
            "express" => new ExpressShipping(),
            _ => throw new NotSupportedException()
        };
    }
}

// Usage
var factory = new ShippingCalculatorFactory();
var calculator = factory.Create("express");
var shippingCost = calculator.CalculateShipping(order);
Enter fullscreen mode Exit fullscreen mode

Awesome! Here's a mini C# project based on Chapter 3 of Adaptive Code via C#, showcasing:

  • βœ… Interfaces
  • βœ… Strategy Pattern
  • βœ… Factory Pattern
  • βœ… Dependency Injection
  • βœ… Testable & SOLID-compliant architecture

πŸ“¦ Project: Shipping Cost Calculator

We'll build a console app that calculates shipping costs based on the shipping type selected (Standard, Express, or Overnight).


πŸ“ Folder Structure

/ShippingCalculatorApp
β”‚
β”œβ”€β”€ Program.cs
β”œβ”€β”€ Models/
β”‚   └── Order.cs
β”œβ”€β”€ Interfaces/
β”‚   └── IShippingCalculator.cs
β”œβ”€β”€ Strategies/
β”‚   β”œβ”€β”€ StandardShipping.cs
β”‚   β”œβ”€β”€ ExpressShipping.cs
β”‚   └── OvernightShipping.cs
β”œβ”€β”€ Factories/
β”‚   └── ShippingCalculatorFactory.cs
β”œβ”€β”€ Services/
β”‚   └── OrderService.cs
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 1. Order.cs (Model)

namespace ShippingCalculatorApp.Models;

public class Order {
    public string Destination { get; set; }
    public double Weight { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 2. IShippingCalculator.cs (Interface)

namespace ShippingCalculatorApp.Interfaces;

using ShippingCalculatorApp.Models;

public interface IShippingCalculator {
    decimal Calculate(Order order);
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 3. Strategy Implementations

StandardShipping.cs

namespace ShippingCalculatorApp.Strategies;

using ShippingCalculatorApp.Interfaces;
using ShippingCalculatorApp.Models;

public class StandardShipping : IShippingCalculator {
    public decimal Calculate(Order order) => (decimal)(order.Weight * 1.2);
}
Enter fullscreen mode Exit fullscreen mode

ExpressShipping.cs

namespace ShippingCalculatorApp.Strategies;

using ShippingCalculatorApp.Interfaces;
using ShippingCalculatorApp.Models;

public class ExpressShipping : IShippingCalculator {
    public decimal Calculate(Order order) => (decimal)(order.Weight * 2.5 + 5);
}
Enter fullscreen mode Exit fullscreen mode

OvernightShipping.cs

namespace ShippingCalculatorApp.Strategies;

using ShippingCalculatorApp.Interfaces;
using ShippingCalculatorApp.Models;

public class OvernightShipping : IShippingCalculator {
    public decimal Calculate(Order order) => (decimal)(order.Weight * 4 + 15);
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 4. ShippingCalculatorFactory.cs (Factory Pattern)

namespace ShippingCalculatorApp.Factories;

using ShippingCalculatorApp.Interfaces;
using ShippingCalculatorApp.Strategies;

public class ShippingCalculatorFactory {
    public IShippingCalculator Create(string type) {
        return type.ToLower() switch {
            "standard" => new StandardShipping(),
            "express" => new ExpressShipping(),
            "overnight" => new OvernightShipping(),
            _ => throw new ArgumentException("Unsupported shipping type")
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 5. OrderService.cs

namespace ShippingCalculatorApp.Services;

using ShippingCalculatorApp.Interfaces;
using ShippingCalculatorApp.Models;

public class OrderService {
    private readonly IShippingCalculator _calculator;

    public OrderService(IShippingCalculator calculator) {
        _calculator = calculator;
    }

    public decimal CalculateShipping(Order order) {
        return _calculator.Calculate(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 6. Program.cs (App Entry Point)

using ShippingCalculatorApp.Models;
using ShippingCalculatorApp.Factories;
using ShippingCalculatorApp.Services;

class Program {
    static void Main() {
        var order = new Order { Destination = "USA", Weight = 10.5 };

        Console.WriteLine("Select shipping type: standard | express | overnight");
        var type = Console.ReadLine();

        var factory = new ShippingCalculatorFactory();
        var calculator = factory.Create(type!);

        var service = new OrderService(calculator);
        var cost = service.CalculateShipping(order);

        Console.WriteLine($"Shipping cost: ${cost:F2}");
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… How It All Ties Together

  • OrderService is decoupled from shipping logic via IShippingCalculator
  • ShippingCalculatorFactory injects behavior using the Strategy pattern
  • Code is testable, maintainable, and extensible

Top comments (0)