~/src/www.mokhan.ca/xlgmokha [main]
cat software-design-patterns-architecture-guide.md
software-design-patterns-architecture-guide.md 237629 bytes | 2007-01-01 00:00
symlink: /opt/dotnet/software-design-patterns-architecture-guide.md

Software Design Patterns & Architecture Guide

A comprehensive guide to software design patterns, architectural principles, and best practices for building maintainable, extensible, and robust software systems.

Table of Contents

  1. SOLID Principles
  2. Gang of Four Design Patterns
  3. Enterprise Application Patterns
  4. GRASP Patterns
  5. Architectural Principles
  6. Interface-Based Programming
  7. Domain-Driven Design
  8. Refactoring Techniques
  9. Modern Architecture Patterns
  10. Best Practices and Guidelines

SOLID Principles

The SOLID principles form the foundation of object-oriented design and help create maintainable, flexible software systems.

Single Responsibility Principle (SRP)

Definition: Each class should have a single responsibility, and therefore only a single reason to change.

Problem: Classes that violate SRP are difficult to maintain and test.

// Violation: Multiple responsibilities
public class BadDataAccess
{
  public IEnumerable<DataRow> ExecuteQuery(string sql)
  {
    // Responsibility 1: Configuration management
    string connectionString = ConfigurationManager
      .ConnectionStrings[ConfigurationManager.AppSettings["active.connection"]]
      .ConnectionString;

    // Responsibility 2: Connection management
    using(var connection = new SqlConnection(connectionString))
    {
      // Responsibility 3: Command execution
      var command = connection.CreateCommand();
      command.CommandText = sql;
      command.CommandType = CommandType.Text;

      // Responsibility 4: Data mapping
      var table = new DataTable();
      table.Load(command.ExecuteReader());
      return table.Rows.Cast<DataRow>();
    }
  }
}

Solution: Separate concerns into focused classes.

// SRP Compliant: Single responsibility per class
public class DatabaseGateway
{
  private readonly IConnectionFactory _connectionFactory;
  private readonly IMapper<IDataReader, IEnumerable<DataRow>> _mapper;

  public DatabaseGateway(
    IConnectionFactory connectionFactory, 
    IMapper<IDataReader, IEnumerable<DataRow>> mapper)
  {
    _connectionFactory = connectionFactory;
    _mapper = mapper;
  }

  public IEnumerable<DataRow> Execute(string sql)
  {
    using(var connection = _connectionFactory.OpenConnection())
    {
      var command = connection.CreateCommand();
      command.CommandText = sql;
      command.CommandType = CommandType.Text;
      return _mapper.MapFrom(command.ExecuteReader());
    }
  }
}

public interface IConnectionFactory
{
  IDbConnection OpenConnection();
}

public interface IMapper<TInput, TOutput>
{
  TOutput MapFrom(TInput input);
}

Benefits:

  • Reduced coupling between responsibilities
  • Easier testing and maintenance
  • Better reusability
  • Isolated impact of changes

Open-Closed Principle (OCP)

Definition: Classes should be open for extension but closed for modification.

Problem: Modifying existing classes increases the risk of introducing bugs.

// Violation: Must modify class to add new query types
public class DatabaseGateway
{
  public void Execute(string sql, QueryType type)
  {
    using(var connection = _connectionFactory.OpenConnection())
    {
      var command = connection.CreateCommand();
      
      // Must modify this method for each new query type
      switch(type)
      {
        case QueryType.RawSql:
          command.CommandText = sql;
          command.CommandType = CommandType.Text;
          break;
        case QueryType.StoredProcedure:
          command.CommandText = sql;
          command.CommandType = CommandType.StoredProcedure;
          break;
        // Adding new types requires modification
      }
      
      command.ExecuteReader();
    }
  }
}

Solution: Use the Strategy pattern to allow extension without modification.

// OCP Compliant: Extensible without modification
public class DatabaseGateway
{
  private readonly IConnectionFactory _connectionFactory;

  public DatabaseGateway(IConnectionFactory connectionFactory)
  {
    _connectionFactory = connectionFactory;
  }

  public void Execute(IQuery query)
  {
    using(var connection = _connectionFactory.OpenConnection())
    {
      query.ExecuteUsing(connection.CreateCommand());
    }
  }
}

public interface IQuery
{
  void ExecuteUsing(IDbCommand command);
}

// Extensions don't require modifying DatabaseGateway
public class RawSqlQuery : IQuery
{
  private readonly string _sql;
  
  public RawSqlQuery(string sql)
  {
    _sql = sql;
    Result = new DataTable();
  }

  public DataTable Result { get; private set; }

  public void ExecuteUsing(IDbCommand command)
  {
    command.CommandText = _sql;
    command.CommandType = CommandType.Text;
    Result.Load(command.ExecuteReader());
  }
}

public class StoredProcedureQuery : IQuery
{
  private readonly string _procedureName;
  private readonly Dictionary<string, object> _parameters;
  
  public StoredProcedureQuery(string procedureName, Dictionary<string, object> parameters = null)
  {
    _procedureName = procedureName;
    _parameters = parameters ?? new Dictionary<string, object>();
    Result = new DataTable();
  }

  public DataTable Result { get; private set; }

  public void ExecuteUsing(IDbCommand command)
  {
    command.CommandText = _procedureName;
    command.CommandType = CommandType.StoredProcedure;
    
    foreach(var param in _parameters)
    {
      var dbParam = command.CreateParameter();
      dbParam.ParameterName = param.Key;
      dbParam.Value = param.Value;
      command.Parameters.Add(dbParam);
    }
    
    Result.Load(command.ExecuteReader());
  }
}

Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types.

Key Points:

  • Derived classes should be able to replace base classes without altering program correctness
  • Preconditions cannot be strengthened in derived classes
  • Postconditions cannot be weakened in derived classes
// LSP Compliant: Proper inheritance hierarchy
public abstract class Shape
{
  public abstract double CalculateArea();
}

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

  public override double CalculateArea()
  {
    return Width * Height;
  }
}

public class Square : Shape  // Not inheriting from Rectangle!
{
  public double Side { get; set; }

  public override double CalculateArea()
  {
    return Side * Side;
  }
}

// Usage - all shapes are substitutable
public void ProcessShapes(List<Shape> shapes)
{
  foreach(Shape shape in shapes)
  {
    Console.WriteLine($"Area: {shape.CalculateArea()}");
  }
}

Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they don’t use.

// ISP Violation: Fat interface
public interface IWorker
{
  void Work();
  void Eat();
  void Sleep();
}

// ISP Compliant: Segregated interfaces
public interface IWorkable
{
  void Work();
}

public interface IFeedable
{
  void Eat();
}

public interface IRestable
{
  void Sleep();
}

public class Human : IWorkable, IFeedable, IRestable
{
  public void Work() { /* implementation */ }
  public void Eat() { /* implementation */ }
  public void Sleep() { /* implementation */ }
}

public class Robot : IWorkable  // Robots don't eat or sleep
{
  public void Work() { /* implementation */ }
}

Dependency Inversion Principle (DIP)

Definition:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.
// DIP Violation: High-level depends on low-level
public class OrderService
{
  private SqlOrderRepository _repository = new SqlOrderRepository(); // Direct dependency
  
  public void ProcessOrder(Order order)
  {
    _repository.Save(order);
  }
}

// DIP Compliant: Depend on abstractions
public class OrderService
{
  private readonly IOrderRepository _repository;
  
  public OrderService(IOrderRepository repository)
  {
    _repository = repository;
  }
  
  public void ProcessOrder(Order order)
  {
    _repository.Save(order);
  }
}

public interface IOrderRepository
{
  void Save(Order order);
  Order GetById(int id);
}

public class SqlOrderRepository : IOrderRepository
{
  public void Save(Order order) { /* SQL implementation */ }
  public Order GetById(int id) { /* SQL implementation */ }
}

Gang of Four Design Patterns

Visitor Pattern

Intent: Separate an algorithm from the structure it operates on.

Use Cases:

  • Adding new operations to object structures without modifying them
  • Operations that need to work across different types of objects
  • Adhering to the Open-Closed Principle
// Core interfaces
public interface IVisitable<T>
{
  void Accept(IVisitor<T> visitor);
}

public interface IVisitor<T>
{
  void Visit(T item);
}

// Structure implementation
public class Table : IVisitable<Row>
{
  private readonly List<Row> _rows = new List<Row>();
  
  public void AddRow(Row row)
  {
    _rows.Add(row);
  }
  
  public void Accept(IVisitor<Row> visitor)
  {
    foreach(var row in _rows)
    {
      visitor.Visit(row);
    }
  }
}

public class Row : IVisitable<ICell>
{
  private readonly List<ICell> _cells = new List<ICell>();
  
  public void AddCell(ICell cell)
  {
    _cells.Add(cell);
  }
  
  public void Accept(IVisitor<ICell> visitor)
  {
    foreach(var cell in _cells)
    {
      visitor.Visit(cell);
    }
  }
}

public interface ICell
{
  object Value { get; }
  IColumn Column { get; }
}

public class Cell<T> : ICell
{
  public Cell(IColumn column, T value)
  {
    Column = column;
    Value = value;
  }
  
  public IColumn Column { get; }
  public object Value { get; private set; }
  
  public void ChangeValueTo(T value)
  {
    Value = value;
  }
}

// Visitor implementations
public class TotalRowsVisitor : IVisitor<Row>
{
  public int Total { get; private set; }
  
  public void Visit(Row item)
  {
    Total++;
  }
}

public class CellValuesPrinterVisitor : IVisitor<ICell>
{
  public void Visit(ICell item)
  {
    Console.WriteLine($"{item.Column.Name}: {item.Value}");
  }
}

// Usage
var table = new Table();
var row = new Row();
row.AddCell(new Cell<string>(new Column("Name"), "John"));
row.AddCell(new Cell<int>(new Column("Age"), 30));
table.AddRow(row);

var rowCountVisitor = new TotalRowsVisitor();
table.Accept(rowCountVisitor);
Console.WriteLine($"Total rows: {rowCountVisitor.Total}");

var cellPrinterVisitor = new CellValuesPrinterVisitor();
row.Accept(cellPrinterVisitor);

Strategy Pattern

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.

// Strategy interface
public interface ISortStrategy<T>
{
  void Sort(List<T> items);
}

// Concrete strategies
public class QuickSortStrategy<T> : ISortStrategy<T> where T : IComparable<T>
{
  public void Sort(List<T> items)
  {
    QuickSort(items, 0, items.Count - 1);
  }
  
  private void QuickSort(List<T> items, int low, int high)
  {
    if (low < high)
    {
      int pivot = Partition(items, low, high);
      QuickSort(items, low, pivot - 1);
      QuickSort(items, pivot + 1, high);
    }
  }
  
  private int Partition(List<T> items, int low, int high)
  {
    // QuickSort partition logic
    return 0; // Simplified
  }
}

public class MergeSortStrategy<T> : ISortStrategy<T> where T : IComparable<T>
{
  public void Sort(List<T> items)
  {
    MergeSort(items, 0, items.Count - 1);
  }
  
  private void MergeSort(List<T> items, int left, int right)
  {
    // MergeSort implementation
  }
}

// Context
public class Sorter<T>
{
  private ISortStrategy<T> _strategy;
  
  public Sorter(ISortStrategy<T> strategy)
  {
    _strategy = strategy;
  }
  
  public void SetStrategy(ISortStrategy<T> strategy)
  {
    _strategy = strategy;
  }
  
  public void Sort(List<T> items)
  {
    _strategy.Sort(items);
  }
}

// Usage
var numbers = new List<int> { 64, 34, 25, 12, 22, 11, 90 };

var sorter = new Sorter<int>(new QuickSortStrategy<int>());
sorter.Sort(numbers);

// Switch strategy at runtime
sorter.SetStrategy(new MergeSortStrategy<int>());
sorter.Sort(numbers);

Observer Pattern

Intent: Define a one-to-many dependency between objects so that when one object changes state, all dependents are notified.

// Subject interface
public interface ISubject<T>
{
  void Attach(IObserver<T> observer);
  void Detach(IObserver<T> observer);
  void Notify(T data);
}

// Observer interface
public interface IObserver<T>
{
  void Update(T data);
}

// Concrete subject
public class WeatherStation : ISubject<WeatherData>
{
  private readonly List<IObserver<WeatherData>> _observers = new List<IObserver<WeatherData>>();
  private WeatherData _currentWeather;
  
  public void Attach(IObserver<WeatherData> observer)
  {
    _observers.Add(observer);
  }
  
  public void Detach(IObserver<WeatherData> observer)
  {
    _observers.Remove(observer);
  }
  
  public void Notify(WeatherData data)
  {
    foreach(var observer in _observers)
    {
      observer.Update(data);
    }
  }
  
  public void SetWeatherData(WeatherData weather)
  {
    _currentWeather = weather;
    Notify(_currentWeather);
  }
}

// Concrete observers
public class MobileApp : IObserver<WeatherData>
{
  public void Update(WeatherData data)
  {
    Console.WriteLine($"Mobile App: Temperature is {data.Temperature}°C");
  }
}

public class WebDisplay : IObserver<WeatherData>
{
  public void Update(WeatherData data)
  {
    Console.WriteLine($"Web Display: Weather update - {data.Temperature}°C, {data.Humidity}% humidity");
  }
}

public class WeatherData
{
  public double Temperature { get; set; }
  public double Humidity { get; set; }
  public double Pressure { get; set; }
}

// Usage
var station = new WeatherStation();
var mobileApp = new MobileApp();
var webDisplay = new WebDisplay();

station.Attach(mobileApp);
station.Attach(webDisplay);

station.SetWeatherData(new WeatherData { Temperature = 25, Humidity = 60, Pressure = 1013 });

Factory Pattern

Intent: Create objects without specifying their exact classes.

// Product hierarchy
public abstract class Document
{
  public abstract void Print();
}

public class PdfDocument : Document
{
  public override void Print()
  {
    Console.WriteLine("Printing PDF document");
  }
}

public class WordDocument : Document
{
  public override void Print()
  {
    Console.WriteLine("Printing Word document");
  }
}

// Factory interface
public interface IDocumentFactory
{
  Document CreateDocument(string type, string content);
}

// Concrete factory
public class DocumentFactory : IDocumentFactory
{
  public Document CreateDocument(string type, string content)
  {
    switch(type.ToLower())
    {
      case "pdf":
        return new PdfDocument();
      case "word":
        return new WordDocument();
      default:
        throw new ArgumentException($"Unknown document type: {type}");
    }
  }
}

// Usage
IDocumentFactory factory = new DocumentFactory();
Document pdfDoc = factory.CreateDocument("pdf", "Sample content");
Document wordDoc = factory.CreateDocument("word", "Sample content");

pdfDoc.Print();
wordDoc.Print();

Enterprise Application Patterns

Event Aggregator

Intent: Channel events from multiple objects into a single object to simplify registration for clients.

// Event infrastructure
public interface IEvent
{
  string Name { get; }
  void Add(EventHandler handler);
  void Raise(object sender, EventArgs data);
}

public interface IEventAggregator
{
  void Register(IEvent eventToAdd);
  void AddHandler(string eventName, EventHandler handler);
  void RaiseEvent(string eventName, object sender = null, EventArgs data = null);
}

// Event implementation
internal class EventRaiser : IEvent
{
  private readonly string _name;
  private readonly List<EventHandler> _handlers;

  public EventRaiser(string name)
  {
    _name = name;
    _handlers = new List<EventHandler>();
  }

  public string Name => _name;

  public void Add(EventHandler handler)
  {
    _handlers.Add(handler);
  }

  public void Raise(object sender, EventArgs data)
  {
    foreach(EventHandler handler in _handlers)
    {
      handler?.Invoke(sender, data);
    }
  }
}

// Event aggregator implementation
public class EventAggregator : IEventAggregator
{
  private readonly Dictionary<string, IEvent> _events;

  public EventAggregator()
  {
    _events = new Dictionary<string, IEvent>();
  }

  public void Register(IEvent eventToAdd)
  {
    if(_events.ContainsKey(eventToAdd.Name))
    {
      throw new ArgumentException($"Event {eventToAdd.Name} already registered");
    }
    
    _events.Add(eventToAdd.Name, eventToAdd);
  }

  public void AddHandler(string eventName, EventHandler handler)
  {
    if(!_events.ContainsKey(eventName))
    {
      throw new ArgumentException($"Event {eventName} not registered");
    }
    
    _events[eventName].Add(handler);
  }

  public void RaiseEvent(string eventName, object sender = null, EventArgs data = null)
  {
    if(!_events.ContainsKey(eventName))
    {
      throw new ArgumentException($"Event {eventName} not registered");
    }
    
    _events[eventName].Raise(sender, data ?? EventArgs.Empty);
  }
}

// Application events facade
public static class ApplicationEvents
{
  private static readonly IEventAggregator _aggregator;
  public static readonly IEvent Save = new EventRaiser("Save");
  public static readonly IEvent Loading = new EventRaiser("Loading");
  public static readonly IEvent Shutdown = new EventRaiser("Shutdown");

  static ApplicationEvents()
  {
    _aggregator = new EventAggregator();
    _aggregator.Register(Save);
    _aggregator.Register(Loading);
    _aggregator.Register(Shutdown);
  }

  public static void Raise(string eventName, object sender = null, EventArgs data = null)
  {
    _aggregator.RaiseEvent(eventName, sender, data);
  }

  public static void SubscribeTo(string eventName, EventHandler handler)
  {
    _aggregator.AddHandler(eventName, handler);
  }
}

// Usage
ApplicationEvents.SubscribeTo("Save", (sender, args) => {
  Console.WriteLine("Document is being saved");
});

ApplicationEvents.SubscribeTo("Loading", (sender, args) => {
  Console.WriteLine("Application is loading");
});

ApplicationEvents.Raise("Save");
ApplicationEvents.Raise("Loading");

Identity Map

Intent: Ensure that each object gets loaded only once by keeping every loaded object in a map.

// Domain layer supertype
public interface IDomainObject
{
  long Id { get; }
}

// Identity map interface
public interface IIdentityMap<T> where T : IDomainObject
{
  void Add(T domainObject);
  T FindObjectWithId(long id);
  bool ContainsObjectWithId(long id);
  void Remove(long id);
}

// Identity map implementation
public class IdentityMap<T> : IIdentityMap<T> where T : IDomainObject
{
  private readonly Dictionary<long, T> _items;

  public IdentityMap()
  {
    _items = new Dictionary<long, T>();
  }

  public void Add(T domainObject)
  {
    if(ContainsObjectWithId(domainObject.Id))
    {
      throw new InvalidOperationException($"Object with ID {domainObject.Id} already exists in map");
    }
    
    _items.Add(domainObject.Id, domainObject);
  }

  public T FindObjectWithId(long id)
  {
    return _items.TryGetValue(id, out T item) ? item : default(T);
  }

  public bool ContainsObjectWithId(long id)
  {
    return _items.ContainsKey(id);
  }

  public void Remove(long id)
  {
    _items.Remove(id);
  }
}

// Example domain object
public class Customer : IDomainObject
{
  public long Id { get; set; }
  public string Name { get; set; }
  public string Email { get; set; }
}

// Repository with identity map
public class CustomerRepository
{
  private readonly IIdentityMap<Customer> _identityMap;
  private readonly IDataMapper<Customer> _dataMapper;

  public CustomerRepository(IIdentityMap<Customer> identityMap, IDataMapper<Customer> dataMapper)
  {
    _identityMap = identityMap;
    _dataMapper = dataMapper;
  }

  public Customer FindById(long id)
  {
    // Check identity map first
    var customer = _identityMap.FindObjectWithId(id);
    if(customer != null)
    {
      return customer;
    }

    // Load from database
    customer = _dataMapper.FindById(id);
    if(customer != null)
    {
      _identityMap.Add(customer);
    }

    return customer;
  }

  public void Save(Customer customer)
  {
    _dataMapper.Save(customer);
    
    if(!_identityMap.ContainsObjectWithId(customer.Id))
    {
      _identityMap.Add(customer);
    }
  }
}

public interface IDataMapper<T>
{
  T FindById(long id);
  void Save(T entity);
}

Data Mapper

Intent: A layer of mappers that moves data between objects and a database while keeping them independent of each other.

// Data mapper interface
public interface IDataMapper<T>
{
  T FindById(long id);
  List<T> FindAll();
  void Insert(T entity);
  void Update(T entity);
  void Delete(long id);
}

// Customer data mapper
public class CustomerDataMapper : IDataMapper<Customer>
{
  private readonly string _connectionString;

  public CustomerDataMapper(string connectionString)
  {
    _connectionString = connectionString;
  }

  public Customer FindById(long id)
  {
    using(var connection = new SqlConnection(_connectionString))
    {
      connection.Open();
      var command = new SqlCommand("SELECT * FROM Customers WHERE Id = @id", connection);
      command.Parameters.AddWithValue("@id", id);
      
      using(var reader = command.ExecuteReader())
      {
        if(reader.Read())
        {
          return MapFromReader(reader);
        }
      }
    }
    
    return null;
  }

  public List<Customer> FindAll()
  {
    var customers = new List<Customer>();
    
    using(var connection = new SqlConnection(_connectionString))
    {
      connection.Open();
      var command = new SqlCommand("SELECT * FROM Customers", connection);
      
      using(var reader = command.ExecuteReader())
      {
        while(reader.Read())
        {
          customers.Add(MapFromReader(reader));
        }
      }
    }
    
    return customers;
  }

  public void Insert(Customer customer)
  {
    using(var connection = new SqlConnection(_connectionString))
    {
      connection.Open();
      var command = new SqlCommand(
        "INSERT INTO Customers (Name, Email) VALUES (@name, @email); SELECT SCOPE_IDENTITY();",
        connection);
      
      command.Parameters.AddWithValue("@name", customer.Name);
      command.Parameters.AddWithValue("@email", customer.Email);
      
      customer.Id = Convert.ToInt64(command.ExecuteScalar());
    }
  }

  public void Update(Customer customer)
  {
    using(var connection = new SqlConnection(_connectionString))
    {
      connection.Open();
      var command = new SqlCommand(
        "UPDATE Customers SET Name = @name, Email = @email WHERE Id = @id",
        connection);
      
      command.Parameters.AddWithValue("@id", customer.Id);
      command.Parameters.AddWithValue("@name", customer.Name);
      command.Parameters.AddWithValue("@email", customer.Email);
      
      command.ExecuteNonQuery();
    }
  }

  public void Delete(long id)
  {
    using(var connection = new SqlConnection(_connectionString))
    {
      connection.Open();
      var command = new SqlCommand("DELETE FROM Customers WHERE Id = @id", connection);
      command.Parameters.AddWithValue("@id", id);
      command.ExecuteNonQuery();
    }
  }

  private Customer MapFromReader(SqlDataReader reader)
  {
    return new Customer
    {
      Id = reader.GetInt64("Id"),
      Name = reader.GetString("Name"),
      Email = reader.GetString("Email")
    };
  }
}

GRASP Patterns

GRASP (General Responsibility Assignment Software Patterns) provides guidelines for assigning responsibilities to classes and objects.

Information Expert

Principle: Assign responsibility to the class that has the information needed to fulfill it.

// Good: Order has the information to calculate its total
public class Order
{
  private readonly List<OrderLine> _orderLines = new List<OrderLine>();

  public decimal CalculateTotal()
  {
    return _orderLines.Sum(line => line.GetSubtotal());
  }

  public void AddOrderLine(Product product, int quantity)
  {
    _orderLines.Add(new OrderLine(product, quantity));
  }
}

public class OrderLine
{
  private readonly Product _product;
  private readonly int _quantity;

  public OrderLine(Product product, int quantity)
  {
    _product = product;
    _quantity = quantity;
  }

  public decimal GetSubtotal()
  {
    return _product.Price * _quantity;
  }
}

public class Product
{
  public string Name { get; set; }
  public decimal Price { get; set; }
}

Creator

Principle: Assign class B the responsibility to create instance of class A if:

  • B aggregates A objects
  • B contains A objects
  • B records instances of A objects
  • B closely uses A objects
  • B has initializing data for A
// Good: Order creates OrderLines since it aggregates them
public class Order
{
  private readonly List<OrderLine> _orderLines = new List<OrderLine>();
  private readonly Customer _customer;

  public Order(Customer customer)
  {
    _customer = customer;
  }

  public void AddProduct(Product product, int quantity)
  {
    // Order creates OrderLine since it aggregates them
    var orderLine = new OrderLine(product, quantity);
    _orderLines.Add(orderLine);
  }
}

// Good: OrderFactory creates Orders since it has the initialization data
public class OrderFactory
{
  private readonly ICustomerRepository _customerRepository;

  public OrderFactory(ICustomerRepository customerRepository)
  {
    _customerRepository = customerRepository;
  }

  public Order CreateOrder(long customerId)
  {
    var customer = _customerRepository.FindById(customerId);
    return new Order(customer); // Factory creates Order with proper data
  }
}

High Cohesion

Principle: Keep related responsibilities together and unrelated responsibilities separate.

// Good: High cohesion - all methods related to customer management
public class Customer
{
  public string Name { get; private set; }
  public string Email { get; private set; }
  public CustomerStatus Status { get; private set; }

  public void UpdateContactInfo(string name, string email)
  {
    Name = name;
    Email = email;
  }

  public void Activate()
  {
    Status = CustomerStatus.Active;
  }

  public void Deactivate()
  {
    Status = CustomerStatus.Inactive;
  }

  public bool IsActive()
  {
    return Status == CustomerStatus.Active;
  }
}

// Bad: Low cohesion - mixing customer data with unrelated functionality
public class CustomerBad
{
  public string Name { get; set; }
  public string Email { get; set; }
  
  // Unrelated responsibility - should be in a separate class
  public void SendEmail(string subject, string body) { }
  
  // Unrelated responsibility - should be in a repository
  public void SaveToDatabase() { }
  
  // Unrelated responsibility - should be in a logging service
  public void LogActivity(string activity) { }
}

Low Coupling

Principle: Minimize dependencies between classes.

// Good: Low coupling - depends on abstractions
public class OrderService
{
  private readonly IOrderRepository _orderRepository;
  private readonly IPaymentProcessor _paymentProcessor;
  private readonly INotificationService _notificationService;

  public OrderService(
    IOrderRepository orderRepository,
    IPaymentProcessor paymentProcessor,
    INotificationService notificationService)
  {
    _orderRepository = orderRepository;
    _paymentProcessor = paymentProcessor;
    _notificationService = notificationService;
  }

  public async Task ProcessOrderAsync(Order order)
  {
    await _orderRepository.SaveAsync(order);
    await _paymentProcessor.ProcessPaymentAsync(order.PaymentInfo);
    await _notificationService.SendOrderConfirmationAsync(order);
  }
}

// Bad: High coupling - depends on concrete classes
public class OrderServiceBad
{
  private SqlOrderRepository _orderRepository = new SqlOrderRepository();
  private PayPalPaymentProcessor _paymentProcessor = new PayPalPaymentProcessor();
  private SmtpEmailService _emailService = new SmtpEmailService();

  // Hard to test, inflexible, tightly coupled
}

Controller

Principle: Assign responsibility for handling system events to a controller class.

// Application controller
public class OrderController
{
  private readonly IOrderService _orderService;
  private readonly ILogger _logger;

  public OrderController(IOrderService orderService, ILogger logger)
  {
    _orderService = orderService;
    _logger = logger;
  }

  public async Task<ActionResult> CreateOrder(CreateOrderRequest request)
  {
    try
    {
      var order = new Order(request.CustomerId);
      
      foreach(var item in request.Items)
      {
        order.AddProduct(item.Product, item.Quantity);
      }

      await _orderService.ProcessOrderAsync(order);
      
      return Ok(new { OrderId = order.Id, Status = "Created" });
    }
    catch(Exception ex)
    {
      _logger.LogError(ex, "Failed to create order");
      return BadRequest("Failed to create order");
    }
  }
}

public class CreateOrderRequest
{
  public long CustomerId { get; set; }
  public List<OrderItem> Items { get; set; }
}

public class OrderItem
{
  public Product Product { get; set; }
  public int Quantity { get; set; }
}

Architectural Principles

Encapsulate What Varies

Identify aspects of your application that vary and separate them from what stays the same.

// Problem: Discount calculation mixed with order logic
public class Order
{
  public decimal CalculateTotal()
  {
    decimal total = _orderLines.Sum(line => line.Subtotal);
    
    // Discount logic that varies
    if(_customer.Type == CustomerType.Premium)
    {
      total *= 0.9m; // 10% discount
    }
    else if(_customer.Type == CustomerType.VIP)
    {
      total *= 0.8m; // 20% discount
    }
    
    return total;
  }
}

// Solution: Encapsulate varying discount calculation
public interface IDiscountStrategy
{
  decimal CalculateDiscount(decimal total, Customer customer);
}

public class PremiumDiscountStrategy : IDiscountStrategy
{
  public decimal CalculateDiscount(decimal total, Customer customer)
  {
    return total * 0.1m; // 10% discount
  }
}

public class VipDiscountStrategy : IDiscountStrategy
{
  public decimal CalculateDiscount(decimal total, Customer customer)
  {
    return total * 0.2m; // 20% discount
  }
}

public class Order
{
  private readonly IDiscountStrategy _discountStrategy;
  
  public Order(IDiscountStrategy discountStrategy)
  {
    _discountStrategy = discountStrategy;
  }
  
  public decimal CalculateTotal()
  {
    decimal subtotal = _orderLines.Sum(line => line.Subtotal);
    decimal discount = _discountStrategy.CalculateDiscount(subtotal, _customer);
    return subtotal - discount;
  }
}

Favor Composition Over Inheritance

Use composition to achieve code reuse instead of inheritance when possible.

// Problem: Inheritance hierarchy becomes complex
public abstract class Employee
{
  public abstract decimal CalculatePay();
}

public class FullTimeEmployee : Employee
{
  public override decimal CalculatePay() { /* implementation */ }
}

public class PartTimeEmployee : Employee
{
  public override decimal CalculatePay() { /* implementation */ }
}

public class ContractEmployee : Employee
{
  public override decimal CalculatePay() { /* implementation */ }
}

// What about FullTimeManagerEmployee? PartTimeManagerEmployee?
// Inheritance explosion!

// Solution: Composition
public interface IPayCalculator
{
  decimal CalculatePay(Employee employee);
}

public class SalaryPayCalculator : IPayCalculator
{
  public decimal CalculatePay(Employee employee)
  {
    return employee.AnnualSalary / 12;
  }
}

public class HourlyPayCalculator : IPayCalculator
{
  public decimal CalculatePay(Employee employee)
  {
    return employee.HoursWorked * employee.HourlyRate;
  }
}

public class Employee
{
  private readonly IPayCalculator _payCalculator;
  
  public Employee(IPayCalculator payCalculator)
  {
    _payCalculator = payCalculator;
  }
  
  public decimal CalculatePay()
  {
    return _payCalculator.CalculatePay(this);
  }
  
  public decimal AnnualSalary { get; set; }
  public decimal HourlyRate { get; set; }
  public int HoursWorked { get; set; }
}

Program to Interfaces, Not Implementations

Depend on abstractions rather than concrete classes.

// Good: Programming to interfaces
public class DocumentProcessor
{
  private readonly IDocumentParser _parser;
  private readonly IDocumentValidator _validator;
  private readonly IDocumentStorage _storage;

  public DocumentProcessor(
    IDocumentParser parser,
    IDocumentValidator validator,
    IDocumentStorage storage)
  {
    _parser = parser;
    _validator = validator;
    _storage = storage;
  }

  public async Task ProcessDocumentAsync(Stream documentStream)
  {
    var document = await _parser.ParseAsync(documentStream);
    
    if(await _validator.ValidateAsync(document))
    {
      await _storage.StoreAsync(document);
    }
  }
}

// Interfaces define contracts
public interface IDocumentParser
{
  Task<Document> ParseAsync(Stream stream);
}

public interface IDocumentValidator
{
  Task<bool> ValidateAsync(Document document);
}

public interface IDocumentStorage
{
  Task StoreAsync(Document document);
}

// Concrete implementations can vary
public class PdfDocumentParser : IDocumentParser
{
  public async Task<Document> ParseAsync(Stream stream)
  {
    // PDF-specific parsing logic
    return new Document();
  }
}

public class XmlDocumentParser : IDocumentParser
{
  public async Task<Document> ParseAsync(Stream stream)
  {
    // XML-specific parsing logic
    return new Document();
  }
}

Dependency Inversion in Practice

// High-level policy
public class OrderProcessingService
{
  private readonly IOrderRepository _orderRepository;
  private readonly IPaymentGateway _paymentGateway;
  private readonly IInventoryService _inventoryService;
  private readonly INotificationService _notificationService;

  public OrderProcessingService(
    IOrderRepository orderRepository,
    IPaymentGateway paymentGateway,
    IInventoryService inventoryService,
    INotificationService notificationService)
  {
    _orderRepository = orderRepository;
    _paymentGateway = paymentGateway;
    _inventoryService = inventoryService;
    _notificationService = notificationService;
  }

  public async Task<OrderResult> ProcessOrderAsync(Order order)
  {
    // High-level algorithm that doesn't depend on low-level details
    
    // 1. Validate inventory
    var hasInventory = await _inventoryService.CheckAvailabilityAsync(order.Items);
    if(!hasInventory)
    {
      return OrderResult.Failed("Insufficient inventory");
    }

    // 2. Process payment
    var paymentResult = await _paymentGateway.ProcessPaymentAsync(order.PaymentInfo);
    if(!paymentResult.IsSuccess)
    {
      return OrderResult.Failed("Payment failed");
    }

    // 3. Save order
    await _orderRepository.SaveAsync(order);

    // 4. Update inventory
    await _inventoryService.ReserveItemsAsync(order.Items);

    // 5. Send confirmation
    await _notificationService.SendOrderConfirmationAsync(order);

    return OrderResult.Success(order.Id);
  }
}

// Low-level details implement abstractions
public class SqlOrderRepository : IOrderRepository
{
  public async Task SaveAsync(Order order)
  {
    // SQL-specific implementation
  }
}

public class StripePaymentGateway : IPaymentGateway
{
  public async Task<PaymentResult> ProcessPaymentAsync(PaymentInfo paymentInfo)
  {
    // Stripe-specific implementation
  }
}

Interface-Based Programming

Benefits of Interface-Based Programming

  1. Flexibility: Easy to change implementations
  2. Testability: Easy to create test doubles
  3. Maintainability: Reduced coupling between components
  4. Extensibility: New implementations can be added without changing existing code

Best Practices

Use the Most General Interface Possible

// Instead of specific collection types
public void ProcessStudents(List<Student> students) { }

// Use general interfaces
public void ProcessStudents(IEnumerable<Student> students) { }

// Even better - be specific about what you need
public void DisplayStudents(IEnumerable<Student> students) { } // Read-only
public void ModifyStudents(ICollection<Student> students) { } // Add/Remove
public void AccessStudents(IList<Student> students) { }      // Index access

Interface Hierarchy Understanding

// Understanding the hierarchy helps choose the right abstraction
public interface IEnumerable<T>
{
  IEnumerator<T> GetEnumerator();
}

public interface ICollection<T> : IEnumerable<T>
{
  int Count { get; }
  bool IsReadOnly { get; }
  void Add(T item);
  void Clear();
  bool Contains(T item);
  void CopyTo(T[] array, int arrayIndex);
  bool Remove(T item);
}

public interface IList<T> : ICollection<T>
{
  T this[int index] { get; set; }
  int IndexOf(T item);
  void Insert(int index, T item);
  void RemoveAt(int index);
}

// Choose based on what you actually need
public class StudentService
{
  // Only need to iterate - use most general
  public void PrintStudents(IEnumerable<Student> students)
  {
    foreach(var student in students)
    {
      Console.WriteLine(student.Name);
    }
  }

  // Need to add/remove - use ICollection
  public void FilterStudents(ICollection<Student> students, Func<Student, bool> predicate)
  {
    var toRemove = students.Where(s => !predicate(s)).ToList();
    foreach(var student in toRemove)
    {
      students.Remove(student);
    }
  }

  // Need indexing - use IList
  public void SwapStudents(IList<Student> students, int index1, int index2)
  {
    var temp = students[index1];
    students[index1] = students[index2];
    students[index2] = temp;
  }
}

Domain-Driven Design

Value Objects vs Entities

Entities: Objects with identity that persist over time.

public class Customer
{
  public CustomerId Id { get; private set; }
  public string Name { get; private set; }
  public Email Email { get; private set; }
  public Address Address { get; private set; }

  public Customer(CustomerId id, string name, Email email)
  {
    Id = id;
    Name = name;
    Email = email;
  }

  public void ChangeAddress(Address newAddress)
  {
    Address = newAddress;
  }

  // Identity-based equality
  public override bool Equals(object obj)
  {
    return obj is Customer other && Id.Equals(other.Id);
  }

  public override int GetHashCode()
  {
    return Id.GetHashCode();
  }
}

Value Objects: Objects without identity, defined by their attributes.

public class Address
{
  public string Street { get; }
  public string City { get; }
  public string State { get; }
  public string ZipCode { get; }

  public Address(string street, string city, string state, string zipCode)
  {
    Street = street ?? throw new ArgumentNullException(nameof(street));
    City = city ?? throw new ArgumentNullException(nameof(city));
    State = state ?? throw new ArgumentNullException(nameof(state));
    ZipCode = zipCode ?? throw new ArgumentNullException(nameof(zipCode));
  }

  // Value-based equality
  public override bool Equals(object obj)
  {
    return obj is Address other &&
           Street == other.Street &&
           City == other.City &&
           State == other.State &&
           ZipCode == other.ZipCode;
  }

  public override int GetHashCode()
  {
    return HashCode.Combine(Street, City, State, ZipCode);
  }
}

public class Email
{
  public string Value { get; }

  public Email(string value)
  {
    if(string.IsNullOrWhiteSpace(value))
      throw new ArgumentException("Email cannot be empty");
    
    if(!IsValidEmail(value))
      throw new ArgumentException("Invalid email format");
    
    Value = value;
  }

  private bool IsValidEmail(string email)
  {
    // Email validation logic
    return email.Contains("@");
  }

  public override string ToString() => Value;

  public override bool Equals(object obj)
  {
    return obj is Email other && Value == other.Value;
  }

  public override int GetHashCode()
  {
    return Value.GetHashCode();
  }
}

Aggregate Roots

public class Order
{
  private readonly List<OrderLine> _orderLines = new List<OrderLine>();

  public OrderId Id { get; private set; }
  public CustomerId CustomerId { get; private set; }
  public OrderStatus Status { get; private set; }
  public DateTime OrderDate { get; private set; }

  public IReadOnlyList<OrderLine> OrderLines => _orderLines.AsReadOnly();

  public Order(OrderId id, CustomerId customerId)
  {
    Id = id;
    CustomerId = customerId;
    Status = OrderStatus.Pending;
    OrderDate = DateTime.UtcNow;
  }

  // Aggregate root controls access to child entities
  public void AddOrderLine(ProductId productId, int quantity, decimal unitPrice)
  {
    if(Status != OrderStatus.Pending)
      throw new InvalidOperationException("Cannot modify confirmed order");

    var existingLine = _orderLines.FirstOrDefault(l => l.ProductId == productId);
    if(existingLine != null)
    {
      existingLine.UpdateQuantity(existingLine.Quantity + quantity);
    }
    else
    {
      _orderLines.Add(new OrderLine(productId, quantity, unitPrice));
    }
  }

  public void RemoveOrderLine(ProductId productId)
  {
    if(Status != OrderStatus.Pending)
      throw new InvalidOperationException("Cannot modify confirmed order");

    var line = _orderLines.FirstOrDefault(l => l.ProductId == productId);
    if(line != null)
    {
      _orderLines.Remove(line);
    }
  }

  public void Confirm()
  {
    if(!_orderLines.Any())
      throw new InvalidOperationException("Cannot confirm empty order");

    Status = OrderStatus.Confirmed;
  }

  public decimal CalculateTotal()
  {
    return _orderLines.Sum(line => line.Subtotal);
  }
}

public class OrderLine
{
  public ProductId ProductId { get; private set; }
  public int Quantity { get; private set; }
  public decimal UnitPrice { get; private set; }

  public decimal Subtotal => Quantity * UnitPrice;

  internal OrderLine(ProductId productId, int quantity, decimal unitPrice)
  {
    ProductId = productId;
    UpdateQuantity(quantity);
    UnitPrice = unitPrice;
  }

  internal void UpdateQuantity(int quantity)
  {
    if(quantity <= 0)
      throw new ArgumentException("Quantity must be positive");
    
    Quantity = quantity;
  }
}

Refactoring Techniques

Extract Method

Break down large methods into smaller, focused methods.

// Before: Large method with multiple responsibilities
public class OrderProcessor
{
  public void ProcessOrder(Order order)
  {
    // Validate order
    if(order == null) throw new ArgumentNullException(nameof(order));
    if(!order.Items.Any()) throw new ArgumentException("Order has no items");
    
    foreach(var item in order.Items)
    {
      if(item.Quantity <= 0) throw new ArgumentException("Invalid quantity");
      if(item.Product == null) throw new ArgumentException("Invalid product");
    }

    // Calculate totals
    decimal subtotal = 0;
    foreach(var item in order.Items)
    {
      subtotal += item.Product.Price * item.Quantity;
    }
    
    decimal tax = subtotal * 0.08m;
    decimal shipping = subtotal > 100 ? 0 : 10;
    decimal total = subtotal + tax + shipping;

    // Process payment
    var paymentRequest = new PaymentRequest
    {
      Amount = total,
      CustomerInfo = order.Customer,
      PaymentMethod = order.PaymentMethod
    };

    var paymentResult = _paymentGateway.ProcessPayment(paymentRequest);
    if(!paymentResult.Success)
    {
      throw new PaymentException("Payment failed");
    }

    // Update inventory
    foreach(var item in order.Items)
    {
      _inventoryService.DecrementStock(item.Product.Id, item.Quantity);
    }

    // Send notifications
    _emailService.SendOrderConfirmation(order.Customer.Email, order);
    _emailService.SendShippingNotification(order.Customer.Email, order);
  }
}

// After: Extracted methods with single responsibilities
public class OrderProcessor
{
  public void ProcessOrder(Order order)
  {
    ValidateOrder(order);
    var total = CalculateOrderTotal(order);
    ProcessPayment(order, total);
    UpdateInventory(order);
    SendNotifications(order);
  }

  private void ValidateOrder(Order order)
  {
    if(order == null) throw new ArgumentNullException(nameof(order));
    if(!order.Items.Any()) throw new ArgumentException("Order has no items");
    
    foreach(var item in order.Items)
    {
      ValidateOrderItem(item);
    }
  }

  private void ValidateOrderItem(OrderItem item)
  {
    if(item.Quantity <= 0) throw new ArgumentException("Invalid quantity");
    if(item.Product == null) throw new ArgumentException("Invalid product");
  }

  private decimal CalculateOrderTotal(Order order)
  {
    decimal subtotal = CalculateSubtotal(order);
    decimal tax = CalculateTax(subtotal);
    decimal shipping = CalculateShipping(subtotal);
    
    return subtotal + tax + shipping;
  }

  private decimal CalculateSubtotal(Order order)
  {
    return order.Items.Sum(item => item.Product.Price * item.Quantity);
  }

  private decimal CalculateTax(decimal subtotal)
  {
    return subtotal * 0.08m;
  }

  private decimal CalculateShipping(decimal subtotal)
  {
    return subtotal > 100 ? 0 : 10;
  }

  private void ProcessPayment(Order order, decimal total)
  {
    var paymentRequest = new PaymentRequest
    {
      Amount = total,
      CustomerInfo = order.Customer,
      PaymentMethod = order.PaymentMethod
    };

    var paymentResult = _paymentGateway.ProcessPayment(paymentRequest);
    if(!paymentResult.Success)
    {
      throw new PaymentException("Payment failed");
    }
  }

  private void UpdateInventory(Order order)
  {
    foreach(var item in order.Items)
    {
      _inventoryService.DecrementStock(item.Product.Id, item.Quantity);
    }
  }

  private void SendNotifications(Order order)
  {
    _emailService.SendOrderConfirmation(order.Customer.Email, order);
    _emailService.SendShippingNotification(order.Customer.Email, order);
  }
}

Replace Conditional with Polymorphism

Replace complex conditional logic with polymorphic behavior.

// Before: Complex conditionals
public class EmployeePayrollCalculator
{
  public decimal CalculatePay(Employee employee)
  {
    switch(employee.Type)
    {
      case EmployeeType.FullTime:
        return employee.MonthlySalary;
        
      case EmployeeType.PartTime:
        return employee.HourlyRate * employee.HoursWorked;
        
      case EmployeeType.Contract:
        decimal contractPay = employee.HourlyRate * employee.HoursWorked;
        return contractPay * 1.1m; // 10% contractor markup
        
      case EmployeeType.Intern:
        return employee.HoursWorked * 15; // Fixed intern rate
        
      default:
        throw new ArgumentException("Unknown employee type");
    }
  }

  public decimal CalculateBonus(Employee employee)
  {
    switch(employee.Type)
    {
      case EmployeeType.FullTime:
        return employee.MonthlySalary * 0.1m;
        
      case EmployeeType.PartTime:
        return 0; // No bonus for part-time
        
      case EmployeeType.Contract:
        return 0; // No bonus for contractors
        
      case EmployeeType.Intern:
        return 500; // Fixed intern bonus
        
      default:
        throw new ArgumentException("Unknown employee type");
    }
  }
}

// After: Polymorphic approach
public abstract class Employee
{
  public string Name { get; set; }
  public decimal MonthlySalary { get; set; }
  public decimal HourlyRate { get; set; }
  public int HoursWorked { get; set; }

  public abstract decimal CalculatePay();
  public abstract decimal CalculateBonus();
}

public class FullTimeEmployee : Employee
{
  public override decimal CalculatePay()
  {
    return MonthlySalary;
  }

  public override decimal CalculateBonus()
  {
    return MonthlySalary * 0.1m;
  }
}

public class PartTimeEmployee : Employee
{
  public override decimal CalculatePay()
  {
    return HourlyRate * HoursWorked;
  }

  public override decimal CalculateBonus()
  {
    return 0; // No bonus for part-time
  }
}

public class ContractEmployee : Employee
{
  public override decimal CalculatePay()
  {
    return HourlyRate * HoursWorked * 1.1m; // 10% contractor markup
  }

  public override decimal CalculateBonus()
  {
    return 0; // No bonus for contractors
  }
}

public class InternEmployee : Employee
{
  public override decimal CalculatePay()
  {
    return HoursWorked * 15; // Fixed intern rate
  }

  public override decimal CalculateBonus()
  {
    return 500; // Fixed intern bonus
  }
}

// Usage is now much simpler
public class PayrollService
{
  public PayrollReport GeneratePayroll(List<Employee> employees)
  {
    var report = new PayrollReport();
    
    foreach(var employee in employees)
    {
      var payAmount = employee.CalculatePay();
      var bonusAmount = employee.CalculateBonus();
      
      report.AddEntry(employee.Name, payAmount, bonusAmount);
    }
    
    return report;
  }
}

Modern Architecture Patterns

Repository Pattern with Unit of Work

public interface IRepository<T> where T : class
{
  Task<T> GetByIdAsync(object id);
  Task<IEnumerable<T>> GetAllAsync();
  Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
  Task AddAsync(T entity);
  void Update(T entity);
  void Remove(T entity);
}

public interface IUnitOfWork : IDisposable
{
  IRepository<Customer> Customers { get; }
  IRepository<Order> Orders { get; }
  IRepository<Product> Products { get; }
  
  Task<int> SaveChangesAsync();
  Task BeginTransactionAsync();
  Task CommitTransactionAsync();
  Task RollbackTransactionAsync();
}

public class UnitOfWork : IUnitOfWork
{
  private readonly DbContext _context;
  private IDbContextTransaction _transaction;

  public UnitOfWork(DbContext context)
  {
    _context = context;
    Customers = new Repository<Customer>(_context);
    Orders = new Repository<Order>(_context);
    Products = new Repository<Product>(_context);
  }

  public IRepository<Customer> Customers { get; }
  public IRepository<Order> Orders { get; }
  public IRepository<Product> Products { get; }

  public async Task<int> SaveChangesAsync()
  {
    return await _context.SaveChangesAsync();
  }

  public async Task BeginTransactionAsync()
  {
    _transaction = await _context.Database.BeginTransactionAsync();
  }

  public async Task CommitTransactionAsync()
  {
    await _transaction?.CommitAsync();
  }

  public async Task RollbackTransactionAsync()
  {
    await _transaction?.RollbackAsync();
  }

  public void Dispose()
  {
    _transaction?.Dispose();
    _context?.Dispose();
  }
}

CQRS (Command Query Responsibility Segregation)

// Commands (Write operations)
public interface ICommand { }

public interface ICommandHandler<TCommand> where TCommand : ICommand
{
  Task HandleAsync(TCommand command);
}

public class CreateOrderCommand : ICommand
{
  public long CustomerId { get; set; }
  public List<OrderItem> Items { get; set; }
  public PaymentInfo PaymentInfo { get; set; }
}

public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand>
{
  private readonly IOrderRepository _orderRepository;
  private readonly IEventBus _eventBus;

  public CreateOrderCommandHandler(IOrderRepository orderRepository, IEventBus eventBus)
  {
    _orderRepository = orderRepository;
    _eventBus = eventBus;
  }

  public async Task HandleAsync(CreateOrderCommand command)
  {
    var order = new Order(command.CustomerId);
    
    foreach(var item in command.Items)
    {
      order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
    }

    await _orderRepository.SaveAsync(order);
    
    await _eventBus.PublishAsync(new OrderCreatedEvent(order.Id, order.CustomerId));
  }
}

// Queries (Read operations)
public interface IQuery<TResult> { }

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
  Task<TResult> HandleAsync(TQuery query);
}

public class GetOrdersByCustomerQuery : IQuery<List<OrderSummary>>
{
  public long CustomerId { get; set; }
  public DateTime? FromDate { get; set; }
  public DateTime? ToDate { get; set; }
}

public class GetOrdersByCustomerQueryHandler : IQueryHandler<GetOrdersByCustomerQuery, List<OrderSummary>>
{
  private readonly IReadOnlyOrderRepository _orderRepository;

  public GetOrdersByCustomerQueryHandler(IReadOnlyOrderRepository orderRepository)
  {
    _orderRepository = orderRepository;
  }

  public async Task<List<OrderSummary>> HandleAsync(GetOrdersByCustomerQuery query)
  {
    return await _orderRepository.GetOrderSummariesByCustomerAsync(
      query.CustomerId, 
      query.FromDate, 
      query.ToDate);
  }
}

// Command/Query Bus
public interface ICommandBus
{
  Task SendAsync<TCommand>(TCommand command) where TCommand : ICommand;
}

public interface IQueryBus
{
  Task<TResult> SendAsync<TResult>(IQuery<TResult> query);
}

Best Practices and Guidelines

Design Principles Summary

  1. Single Responsibility Principle: Each class should have one reason to change
  2. Open-Closed Principle: Open for extension, closed for modification
  3. Liskov Substitution Principle: Subtypes must be substitutable for their base types
  4. Interface Segregation Principle: Clients shouldn’t depend on interfaces they don’t use
  5. Dependency Inversion Principle: Depend on abstractions, not concretions

Pattern Selection Guidelines

When to Use Visitor Pattern:

  • Need to perform operations across different object types
  • Operations change more frequently than the object structure
  • Want to keep related operations together

When to Use Strategy Pattern:

  • Multiple ways to perform a task
  • Want to make algorithms interchangeable
  • Eliminate conditional statements

When to Use Observer Pattern:

  • One object’s state changes affect multiple other objects
  • Loose coupling between subject and observers is needed
  • Dynamic subscription/unsubscription is required

When to Use Factory Pattern:

  • Object creation logic is complex
  • Need to encapsulate object creation
  • Want to decouple client code from concrete classes

Architecture Guidelines

  1. Layered Architecture: Separate concerns into distinct layers (Presentation, Business, Data)
  2. Dependency Flow: Dependencies should point inward toward the business logic
  3. Cross-Cutting Concerns: Handle logging, security, caching at infrastructure level
  4. Domain Model: Keep business logic in domain objects, not in services
  5. Interface Design: Design interfaces from the client’s perspective

Code Quality Metrics

  • Cyclomatic Complexity: Keep methods simple (complexity < 10)
  • Class Coupling: Minimize dependencies between classes
  • Cohesion: Keep related functionality together
  • Abstraction Level: Maintain consistent abstraction levels within methods
  • Naming: Use intention-revealing names for classes, methods, and variables

This comprehensive guide represents decades of software engineering experience and best practices. The patterns and principles covered here provide a solid foundation for building maintainable, extensible, and robust software systems.


This guide consolidates knowledge from multiple blog posts and software engineering experience, covering foundational design patterns and architectural principles that remain relevant across technology stacks and programming languages.