🔹 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 (likeCreditCardPaymentProcessor
). - 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 viaIShippingCalculator
-
ShippingCalculatorFactory
injects behavior using the Strategy pattern - Code is testable, maintainable, and extensible