🔹 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);
    }
}

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);
    }
}

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);

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);

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);

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

🔹 1. Order.cs (Model)

namespace ShippingCalculatorApp.Models;

public class Order {
    public string Destination { get; set; }
    public double Weight { get; set; }
}

🔹 2. IShippingCalculator.cs (Interface)

namespace ShippingCalculatorApp.Interfaces;

using ShippingCalculatorApp.Models;

public interface IShippingCalculator {
    decimal Calculate(Order order);
}

🔹 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);
}

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);
}

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);
}

🔹 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")
        };
    }
}

🔹 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);
    }
}

🔹 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}");
    }
}

✅ 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