Md Mominul Islam | Software and Data Enginnering | SQL Server, .NET, Power BI, Azure Blog

while(!(succeed=try()));

LinkedIn Portfolio Banner

Latest

Home Top Ad

Responsive Ads Here

Friday, August 22, 2025

Module 9: Best Practices & Anti-Patterns in Software Design Patterns: A Comprehensive Guide for .NET Developers

 



Table of Contents

  1. Introduction to Best Practices and Anti-Patterns

  2. Common Anti-Patterns to Avoid2.1 Spaghetti Code
    2.2 Golden Hammer
    2.3 Big Ball of Mud
    2.4 God Object
    2.5 Overengineering

  3. Choosing the Right Pattern for the Right Scenario3.1 Analyzing Requirements
    3.2 Evaluating Trade-offs
    3.3 Real-World Example: E-Commerce System

  4. Refactoring Legacy Code Using Patterns4.1 Identifying Refactoring Opportunities
    4.2 Step-by-Step Refactoring with Patterns
    4.3 C# Example: Refactoring a Monolithic Service

  5. Combining Patterns for Real-World Solutions5.1 Why Combine Patterns?
    5.2 Example: Factory, Singleton, and Repository Patterns
    5.3 C# Example: Building an Order Processing System

  6. Versioning and Maintaining Design Patterns in Modern Apps6.1 Versioning Strategies
    6.2 Maintaining Patterns in Evolving Systems
    6.3 C# Example: API Versioning with Strategy Pattern

  7. Best Practices for Design Pattern Implementation7.1 Keep It Simple (KISS)
    7.2 Follow SOLID Principles
    7.3 Exception Handling in Patterns
    7.4 Testing Patterns

  8. Pros, Cons, and Alternatives8.1 Pros of Using Design Patterns
    8.2 Cons of Using Design Patterns
    8.3 Alternatives to Design Patterns

  9. Conclusion


1. Introduction to Best Practices and Anti-Patterns

Design patterns are proven solutions to recurring software design problems, but their misuse can lead to complexity and inefficiency. Module 9 of our Software Design Patterns Course focuses on best practices for implementing patterns effectively in .NET applications, while highlighting anti-patterns that can sabotage your codebase. This guide provides practical, real-world examples using C#, ASP.NET, and SQL Server, ensuring you can build scalable, maintainable, and robust systems. Whether you're refactoring legacy code or designing modern apps, this module equips you with the tools to make informed decisions.


2. Common Anti-Patterns to Avoid

Anti-patterns are common mistakes that seem like solutions but lead to technical debt, poor performance, or unmaintainable code. Below, we explore five prevalent anti-patterns and how to avoid them in .NET projects.

2.1 Spaghetti Code

Definition: Code with tangled logic, excessive dependencies, and no clear structure, making it hard to maintain or extend.

Example: A monolithic ASP.NET controller handling business logic, data access, and UI rendering.

Impact:

  • Difficult to debug or test.

  • Changes in one area break unrelated functionality.

  • High onboarding time for new developers.

Solution: Apply the Single Responsibility Principle (SRP) and use patterns like MVC or Repository to separate concerns.

C# Example: Refactoring a spaghetti controller into a clean architecture.

// Bad: Spaghetti Code
public class OrderController : Controller
{
    public IActionResult CreateOrder(int customerId, string productName, int quantity)
    {
        // Mixed concerns: validation, data access, and business logic
        if (string.IsNullOrEmpty(productName)) return BadRequest("Invalid product");
        var connection = new SqlConnection("connectionString");
        var command = new SqlCommand("SELECT * FROM Products WHERE Name = @name", connection);
        // ... More inline SQL and logic
    }
}

// Better: Separated Concerns
public class OrderController : Controller
{
    private readonly IOrderService _orderService;

    public OrderController(IOrderService orderService)
    {
        _orderService = orderService;
    }

    public IActionResult CreateOrder(CreateOrderDto dto)
    {
        try
        {
            var order = _orderService.CreateOrder(dto);
            return Ok(order);
        }
        catch (ValidationException ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

Best Practice: Use dependency injection and separate business logic into services.

2.2 Golden Hammer

Definition: Overusing a single design pattern (e.g., Singleton) for every problem, ignoring better alternatives.

Example: Using Singleton for all service classes in an ASP.NET Core app, leading to tight coupling.

Impact:

  • Reduced flexibility and testability.

  • Hidden dependencies.

  • Scalability issues in multi-threaded environments.

Solution: Evaluate patterns like Dependency Injection or Factory for more flexibility.

C# Example: Replacing Singleton with Dependency Injection.

// Bad: Golden Hammer Singleton
public class Logger
{
    private static Logger _instance;
    public static Logger Instance => _instance ??= new Logger();
    private Logger() { }
    public void Log(string message) { /* Log to file */ }
}

// Better: Dependency Injection
public interface ILogger
{
    void Log(string message);
}

public class FileLogger : ILogger
{
    public void Log(string message) { /* Log to file */ }
}

// Startup.cs
services.AddSingleton<ILogger, FileLogger>();

2.3 Big Ball of Mud

Definition: A system with no clear architecture, where components are tightly coupled and lack modularity.

Example: An ASP.NET app with all logic in a single project, mixing UI, business logic, and data access.

Solution: Adopt Layered Architecture or Domain-Driven Design (DDD) to modularize the codebase.

2.4 God Object

Definition: A single class that handles too many responsibilities, violating SRP.

Example: A Customer class managing orders, payments, and notifications.

Solution: Break the God Object into smaller classes using patterns like Facade or Mediator.

2.5 Overengineering

Definition: Adding unnecessary complexity by using patterns when simpler solutions suffice.

Example: Using the Strategy Pattern for a simple conditional check.

Solution: Follow the KISS (Keep It Simple, Stupid) principle and use patterns only when they add value.


3. Choosing the Right Pattern for the Right Scenario

Selecting the appropriate design pattern requires understanding the problem, system requirements, and trade-offs. This section guides you through the decision-making process with a real-world example.

3.1 Analyzing Requirements

  • Identify the Problem: Is it about object creation (Creational), behavior (Behavioral), or structure (Structural)?

  • Consider Constraints: Performance, scalability, maintainability, and team expertise.

  • Evaluate Context: Is the app a monolithic ASP.NET app or a microservices-based system?

3.2 Evaluating Trade-offs

  • Singleton: Simple but can lead to tight coupling.

  • Factory: Flexible for object creation but adds complexity.

  • Observer: Great for event-driven systems but may cause memory leaks if not managed.

3.3 Real-World Example: E-Commerce System

Scenario: An e-commerce platform needs to handle order processing, payment integration, and inventory updates.

Pattern Choices:

  • Factory Pattern: For creating payment processors (PayPal, Stripe).

  • Observer Pattern: For notifying inventory and email services when an order is placed.

  • Repository Pattern: For data access to SQL Server.

C# Example:

public interface IPaymentProcessor
{
    bool ProcessPayment(decimal amount);
}

public class PayPalProcessor : IPaymentProcessor
{
    public bool ProcessPayment(decimal amount) { /* PayPal logic */ return true; }
}

public class PaymentProcessorFactory
{
    public IPaymentProcessor CreateProcessor(string type)
    {
        return type switch
        {
            "PayPal" => new PayPalProcessor(),
            "Stripe" => new StripeProcessor(),
            _ => throw new ArgumentException("Invalid processor type")
        };
    }
}

4. Refactoring Legacy Code Using Patterns

Legacy code often lacks structure, making it hard to maintain. Design patterns can help refactor it into a cleaner, modular system.

4.1 Identifying Refactoring Opportunities

  • Code Smells: Tight coupling, duplicated code, or large classes.

  • Metrics: High cyclomatic complexity or low test coverage.

  • Business Needs: New features requiring extensibility.

4.2 Step-by-Step Refactoring with Patterns

  1. Extract Responsibilities: Use Repository for data access.

  2. Introduce Abstractions: Apply Interface Segregation for flexibility.

  3. Add Patterns: Use Strategy or Decorator for extensibility.

4.3 C# Example: Refactoring a Monolithic Service

Before (Monolithic Order Service):

public class OrderService
{
    public void CreateOrder(int customerId, string productName, int quantity)
    {
        var connection = new SqlConnection("connectionString");
        // Inline SQL and business logic
        // ...
    }
}

After (Refactored with Repository and Strategy):

public interface IOrderRepository
{
    void SaveOrder(Order order);
}

public class SqlOrderRepository : IOrderRepository
{
    private readonly string _connectionString;

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

    public void SaveOrder(Order order)
    {
        using var connection = new SqlConnection(_connectionString);
        // Save to SQL Server
    }
}

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IPaymentStrategy _paymentStrategy;

    public OrderService(IOrderRepository repository, IPaymentStrategy paymentStrategy)
    {
        _repository = repository;
        _paymentStrategy = paymentStrategy;
    }

    public async Task CreateOrderAsync(CreateOrderDto dto)
    {
        try
        {
            var order = new Order { /* Map DTO to Order */ };
            if (!_paymentStrategy.ProcessPayment(order.Total))
                throw new PaymentFailedException("Payment failed");
            _repository.SaveOrder(order);
        }
        catch (Exception ex)
        {
            throw new OrderCreationException("Failed to create order", ex);
        }
    }
}

Best Practice: Use async/await for I/O-bound operations like database calls.


5. Combining Patterns for Real-World Solutions

Combining patterns can create robust, scalable solutions. This section explores how to integrate patterns effectively.

5.1 Why Combine Patterns?

  • Flexibility: Patterns like Factory and Strategy allow extensibility.

  • Separation of Concerns: Repository and Facade reduce coupling.

  • Reusability: Singleton or Decorator enhances code reuse.

5.2 Example: Factory, Singleton, and Repository Patterns

Scenario: An order processing system with logging, data access, and payment processing.

Patterns Used:

  • Factory: Creates payment processors.

  • Singleton: Ensures a single logger instance.

  • Repository: Abstracts SQL Server data access.

5.3 C# Example: Building an Order Processing System

public interface IOrderRepository
{
    Task SaveOrderAsync(Order order);
}

public class SqlOrderRepository : IOrderRepository
{
    private readonly string _connectionString;

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

    public async Task SaveOrderAsync(Order order)
    {
        using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync();
        // Save order to SQL Server
    }
}

public interface ILogger
{
    void Log(string message);
}

public class Logger : ILogger
{
    private static readonly Logger _instance = new Logger();
    public static Logger Instance => _instance;

    private Logger() { }

    public void Log(string message)
    {
        // Log to file or console
        Console.WriteLine(message);
    }
}

public interface IPaymentProcessor
{
    Task<bool> ProcessPaymentAsync(decimal amount);
}

public class PaymentProcessorFactory
{
    public IPaymentProcessor CreateProcessor(string type)
    {
        return type switch
        {
            "PayPal" => new PayPalProcessor(),
            "Stripe" => new StripeProcessor(),
            _ => throw new ArgumentException("Invalid processor type")
        };
    }
}

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly ILogger _logger;

    public OrderService(IOrderRepository repository, IPaymentProcessor paymentProcessor, ILogger logger)
    {
        _repository = repository;
        _paymentProcessor = paymentProcessor;
        _logger = logger;
    }

    public async Task CreateOrderAsync(CreateOrderDto dto)
    {
        try
        {
            var order = new Order { /* Map DTO to Order */ };
            _logger.Log($"Processing order for {dto.CustomerId}");
            if (!await _paymentProcessor.ProcessPaymentAsync(order.Total))
                throw new PaymentFailedException("Payment failed");
            await _repository.SaveOrderAsync(order);
            _logger.Log("Order created successfully");
        }
        catch (Exception ex)
        {
            _logger.Log($"Error: {ex.Message}");
            throw new OrderCreationException("Failed to create order", ex);
        }
    }
}

// Usage in ASP.NET Controller
public class OrderController : ControllerBase
{
    private readonly OrderService _orderService;

    public OrderController()
    {
        var repository = new SqlOrderRepository("connectionString");
        var factory = new PaymentProcessorFactory();
        var paymentProcessor = factory.CreateProcessor("PayPal");
        _orderService = new OrderService(repository, paymentProcessor, Logger.Instance);
    }

    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto dto)
    {
        try
        {
            await _orderService.CreateOrderAsync(dto);
            return Ok("Order created");
        }
        catch (OrderCreationException ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

6. Versioning and Maintaining Design Patterns in Modern Apps

Modern apps evolve rapidly, requiring strategies to version and maintain design patterns.

6.1 Versioning Strategies

  • API Versioning: Use the Strategy Pattern to support multiple API versions.

  • Pattern Evolution: Gradually refactor patterns as requirements change.

  • Documentation: Maintain clear documentation for pattern usage.

6.2 Maintaining Patterns in Evolving Systems

  • Code Reviews: Ensure patterns are applied consistently.

  • Automated Tests: Use unit tests to validate pattern behavior.

  • Refactoring: Continuously improve pattern implementations.

6.3 C# Example: API Versioning with Strategy Pattern

public interface IOrderApiStrategy
{
    Task<Order> GetOrderAsync(int orderId);
}

public class OrderApiV1 : IOrderApiStrategy
{
    public async Task<Order> GetOrderAsync(int orderId)
    {
        // Legacy API logic
        return await Task.FromResult(new Order { Id = orderId });
    }
}

public class OrderApiV2 : IOrderApiStrategy
{
    public async Task<Order> GetOrderAsync(int orderId)
    {
        // Modern API logic with additional fields
        return await Task.FromResult(new Order { Id = orderId, Status = "Processed" });
    }
}

public class OrderApiContext
{
    private readonly IOrderApiStrategy _strategy;

    public OrderApiContext(string version)
    {
        _strategy = version switch
        {
            "v1" => new OrderApiV1(),
            "v2" => new OrderApiV2(),
            _ => throw new ArgumentException("Invalid API version")
        };
    }

    public async Task<Order> GetOrderAsync(int orderId)
    {
        return await _strategy.GetOrderAsync(orderId);
    }
}

// Usage in ASP.NET Controller
public class OrderController : ControllerBase
{
    [HttpGet("{version}/{orderId}")]
    public async Task<IActionResult> GetOrder(string version, int orderId)
    {
        try
        {
            var context = new OrderApiContext(version);
            var order = await context.GetOrderAsync(orderId);
            return Ok(order);
        }
        catch (Exception ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

7. Best Practices for Design Pattern Implementation

7.1 Keep It Simple (KISS)

  • Avoid overusing patterns.

  • Use patterns only when they solve specific problems.

7.2 Follow SOLID Principles

  • S: Single Responsibility Principle.

  • O: Open/Closed Principle.

  • L: Liskov Substitution Principle.

  • I: Interface Segregation Principle.

  • D: Dependency Inversion Principle.

7.3 Exception Handling in Patterns

  • Centralize exception handling in service layers.

  • Use custom exceptions for specific failure cases.

C# Example:

public class OrderCreationException : Exception
{
    public OrderCreationException(string message, Exception inner) : base(message, inner) { }
}

7.4 Testing Patterns

  • Write unit tests for each pattern implementation.

  • Use mocking frameworks like Moq for dependency injection.

C# Example (Unit Test with Moq):

public class OrderServiceTests
{
    [Fact]
    public async Task CreateOrderAsync_ValidDto_SavesOrder()
    {
        // Arrange
        var mockRepo = new Mock<IOrderRepository>();
        var mockPayment = new Mock<IPaymentProcessor>();
        var mockLogger = new Mock<ILogger>();
        mockPayment.Setup(p => p.ProcessPaymentAsync(It.IsAny<decimal>())).ReturnsAsync(true);
        var service = new OrderService(mockRepo.Object, mockPayment.Object, mockLogger.Object);
        var dto = new CreateOrderDto { CustomerId = 1, ProductName = "Laptop", Quantity = 1 };

        // Act
        await service.CreateOrderAsync(dto);

        // Assert
        mockRepo.Verify(r => r.SaveOrderAsync(It.IsAny<Order>()), Times.Once());
    }
}

8. Pros, Cons, and Alternatives

8.1 Pros of Using Design Patterns

  • Reusability: Standardized solutions save development time.

  • Maintainability: Clear structure improves code readability.

  • Scalability: Patterns like Factory and Strategy support growth.

8.2 Cons of Using Design Patterns

  • Complexity: Overuse can lead to unnecessary abstraction.

  • Learning Curve: Requires team training.

  • Performance Overhead: Some patterns (e.g., Decorator) may add overhead.

8.3 Alternatives to Design Patterns

  • Functional Programming: Use pure functions and immutability.

  • Microservices: Break systems into smaller, independent services.

  • Plain Old C# Objects (POCO): Simple classes without patterns for small apps.


9. Conclusion

Module 9 equips .NET developers with the knowledge to apply design patterns effectively while avoiding common pitfalls. By understanding anti-patterns, choosing the right patterns, refactoring legacy code, combining patterns, and maintaining them in modern apps, you can build robust, scalable solutions. The provided C#, ASP.NET, and SQL Server examples demonstrate practical applications, ensuring you can implement these concepts in real-world projects. Continue practicing these principles to master software design patterns and elevate your development skills.

No comments:

Post a Comment

Thanks for your valuable comment...........
Md. Mominul Islam