Mastering Code Reviews and Refactoring: A Comprehensive Guide for .NET Developers
As a .NET developer, your role extends beyond writing functional code. You are responsible for ensuring that the codebase is clean, maintainable, and scalable. Code reviews and refactoring are two of the most critical practices to achieve this. In this blog, we’ll explore best practices, SOLID principles, design patterns, and practical examples to help you master these skills.
Code Review Guidelines
1. Code Quality and Readability
Consistent Formatting: Ensure the code follows consistent formatting (e.g., indentation, braces, line breaks) as per team or project standards.
Meaningful Naming: Variable, method, and class names should be descriptive and follow naming conventions (e.g.,
PascalCase
for methods,camelCase
for variables).Avoid Magic Numbers/Strings: Replace hardcoded values with constants or configuration settings.
Single Responsibility Principle (SRP): Ensure methods and classes have a single responsibility.
Avoid Deep Nesting: Refactor deeply nested
if
statements or loops for better readability.
2. Performance and Efficiency
Avoid Premature Optimization: Ensure optimizations are based on measurable performance issues, not assumptions.
Use Efficient Data Structures: Choose the right data structures (e.g.,
List<T>
vs.HashSet<T>
) based on use cases.Minimize Database Calls: Batch database operations and avoid N+1 query problems.
Asynchronous Programming: Use
async
/await
for I/O-bound operations to improve scalability.
3. Security
Input Validation: Validate all user inputs to prevent SQL injection, XSS, and other attacks.
Avoid Hardcoding Secrets: Use secure storage (e.g., Azure Key Vault, environment variables) for sensitive information.
Use Secure Libraries: Ensure third-party libraries are up-to-date and free from known vulnerabilities.
4. Maintainability
DRY Principle: Eliminate code duplication by refactoring into reusable methods or classes.
Modular Design: Encourage separation of concerns (e.g., MVC, Clean Architecture).
Unit Tests: Ensure new code is covered by unit tests and follows test-driven development (TDD) principles where applicable.
Documentation: Include XML comments for public APIs and complex logic.
5. Error Handling
Graceful Degradation: Handle exceptions gracefully and provide meaningful error messages.
Avoid Empty Catch Blocks: Ensure exceptions are logged or handled appropriately.
Use Custom Exceptions: Create custom exceptions for domain-specific errors.
6. Dependency Management
Dependency Injection (DI): Use DI to manage dependencies and promote testability.
Avoid Tight Coupling: Ensure classes are loosely coupled and depend on abstractions, not concrete implementations.
7. Testing
Unit Tests: Ensure unit tests cover edge cases and follow the Arrange-Act-Assert pattern.
Integration Tests: Verify interactions between components.
Mocking: Use mocking frameworks (e.g., Moq) to isolate dependencies in unit tests.
Refactoring Guidelines
1. Code Smells to Look For
Long Methods: Break down methods into smaller, reusable ones.
Large Classes: Split classes with too many responsibilities.
Duplicate Code: Extract common logic into shared methods or base classes.
Complex Conditionals: Simplify conditionals using polymorphism or strategy patterns.
Primitive Obsession: Replace primitive types with domain-specific types.
2. Refactoring Techniques
Extract Method: Break down large methods into smaller, reusable ones.
Rename Variables/Methods: Use meaningful names to improve readability.
Replace Conditional with Polymorphism: Use inheritance or interfaces to handle conditional logic.
Introduce Design Patterns: Apply patterns like Factory, Strategy, or Observer where appropriate.
Inline Temp/Method: Remove unnecessary temporary variables or methods.
3. Performance Refactoring
Optimize LINQ Queries: Avoid multiple enumerations and use
Any()
instead ofCount()
for existence checks.Reduce Memory Allocations: Use
StringBuilder
for string concatenation in loops.Lazy Loading: Delay resource-intensive operations until necessary.
4. Architectural Refactoring
Move to Microservices: Break monolithic applications into smaller, independent services.
Adopt Clean Architecture: Separate concerns into layers (e.g., Presentation, Application, Domain, Infrastructure).
Refactor Legacy Code: Gradually improve legacy code by adding tests and modularizing components.
5. Testing During Refactoring
Ensure Test Coverage: Run existing tests to ensure refactoring doesn’t introduce regressions.
Add New Tests: Cover new or modified functionality with unit and integration tests.
Refactor Tests: Improve test readability and maintainability.
Tools for Code Review and Refactoring
Static Analysis Tools: Roslyn analyzers, SonarQube, ReSharper.
Unit Testing Frameworks: xUnit, NUnit, MSTest.
Mocking Frameworks: Moq, NSubstitute.
CI/CD Integration: Azure DevOps, GitHub Actions, Jenkins.
Code Coverage: Coverlet, dotCover.
Below is a detailed explanation of the code review and refactoring guidelines for a .NET developer, with code examples for each point. This will help illustrate the concepts in practice.
1. Code Quality and Readability
Consistent Formatting
Ensure consistent indentation, braces, and line breaks.
Bad Example:
public void ProcessData(){
if (condition){
Console.WriteLine("Condition met");
}else{
Console.WriteLine("Condition not met");
}
}
Good Example:
public void ProcessData()
{
if (condition)
{
Console.WriteLine("Condition met");
}
else
{
Console.WriteLine("Condition not met");
}
}
Meaningful Naming
Use descriptive names for variables, methods, and classes.
Bad Example:
int x = 10; // What does 'x' represent?
public void DoStuff() { }
Good Example:
int retryAttempts = 10; // Clearly describes the purpose
public void ProcessOrder() { }
Avoid Magic Numbers/Strings
Replace hardcoded values with constants or configuration settings.
Bad Example:
if (status == 2) // What does 2 mean?
{
// Do something
}
Good Example:
const int OrderStatusShipped = 2;
if (status == OrderStatusShipped)
{
// Do something
}
2. Performance and Efficiency
Use Efficient Data Structures
Choose the right data structure for the task.
Bad Example:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
if (numbers.Contains(3)) // O(n) operation
{
// Do something
}
Good Example:
HashSet<int> numbers = new HashSet<int> { 1, 2, 3, 4, 5 };
if (numbers.Contains(3)) // O(1) operation
{
// Do something
}
Asynchronous Programming
Use async
/await
for I/O-bound operations.
Bad Example:
public void SaveData()
{
File.WriteAllText("data.txt", "Hello, World!"); // Blocks the thread
}
Good Example:
public async Task SaveDataAsync()
{
await File.WriteAllTextAsync("data.txt", "Hello, World!"); // Non-blocking
}
3. Security
Input Validation
Validate user inputs to prevent attacks.
Bad Example:
public void ProcessInput(string input)
{
// No validation
var command = $"SELECT * FROM Users WHERE Name = '{input}'";
// SQL injection risk
}
public void ProcessInput(string input)
{
if (string.IsNullOrWhiteSpace(input) || input.Contains("'"))
{
throw new ArgumentException("Invalid input");
}
var command = $"SELECT * FROM Users WHERE Name = '{input}'";
}
4. Maintainability
DRY Principle
Eliminate code duplication.
Bad Example:
public void CalculateArea(int length, int width)
{
int area = length * width;
Console.WriteLine(area);
}
public void CalculateVolume(int length, int width, int height)
{
int area = length * width;
int volume = area * height;
Console.WriteLine(volume);
}
Good Example:
public int CalculateArea(int length, int width)
{
return length * width;
}
public void CalculateVolume(int length, int width, int height)
{
int area = CalculateArea(length, width);
int volume = area * height;
Console.WriteLine(volume);
}
5. Error Handling
Graceful Degradation
Handle exceptions gracefully.
Bad Example:
public void ReadFile(string path)
{
string content = File.ReadAllText(path); // No error handling
}
Good Example:
public void ReadFile(string path)
{
try
{
string content = File.ReadAllText(path);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("File not found: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("An error occurred: " + ex.Message);
}
}
6. Dependency Management
Dependency Injection (DI)
Use DI to manage dependencies.
Bad Example:
public class OrderService
{
private readonly EmailService _emailService = new EmailService(); // Tight coupling
}
Good Example:
public class OrderService
{
private readonly IEmailService _emailService;
public OrderService(IEmailService emailService) // Dependency injected
{
_emailService = emailService;
}
}
7. Testing
Unit Tests
Write unit tests for critical logic.
Example:
[Fact]
public void CalculateArea_ShouldReturnCorrectValue()
{
// Arrange
var calculator = new AreaCalculator();
// Act
int result = calculator.CalculateArea(5, 10);
// Assert
Assert.Equal(50, result);
}
Refactoring Examples
Extract Method
Break down large methods.
Before:
public void ProcessOrder(Order order)
{
if (order.IsValid)
{
// Validate order
// Calculate total
// Apply discounts
// Save to database
}
}
After:
public void ProcessOrder(Order order)
{
if (order.IsValid)
{
ValidateOrder(order);
CalculateTotal(order);
ApplyDiscounts(order);
SaveOrder(order);
}
}
Replace Conditional with Polymorphism
Use polymorphism to handle conditionals.
Before:
public double CalculateDiscount(Customer customer)
{
if (customer.Type == "Regular")
{
return 0.1;
}
else if (customer.Type == "Premium")
{
return 0.2;
}
else
{
return 0;
}
}
After:
public abstract class Customer
{
public abstract double GetDiscount();
}
public class RegularCustomer : Customer
{
public override double GetDiscount() => 0.1;
}
public class PremiumCustomer : Customer
{
public override double GetDiscount() => 0.2;
}
The SOLID principles and design patterns are essential for writing maintainable, scalable, and robust code. Below, I’ll explain each SOLID principle and a few key design patterns with bad code and good code examples.
SOLID Principles
1. Single Responsibility Principle (SRP)
A class should have only one reason to change (i.e., one responsibility).
Bad Example:
public class OrderProcessor
{
public void ProcessOrder(Order order)
{
// Validate order
if (order.IsValid)
{
// Save order to database
SaveOrder(order);
// Send confirmation email
SendEmail(order);
}
}
private void SaveOrder(Order order)
{
// Database logic
}
private void SendEmail(Order order)
{
// Email logic
}
}
Good Example:
public class OrderProcessor
{
private readonly OrderValidator _validator;
private readonly OrderRepository _repository;
private readonly EmailService _emailService;
public OrderProcessor(OrderValidator validator, OrderRepository repository, EmailService emailService)
{
_validator = validator;
_repository = repository;
_emailService = emailService;
}
public void ProcessOrder(Order order)
{
if (_validator.Validate(order))
{
_repository.Save(order);
_emailService.SendConfirmation(order);
}
}
}
2. Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification.
Bad Example:
public class DiscountCalculator
{
public decimal CalculateDiscount(string customerType, decimal amount)
{
if (customerType == "Regular")
{
return amount * 0.1m;
}
else if (customerType == "Premium")
{
return amount * 0.2m;
}
else
{
return 0;
}
}
}
Good Example:
public abstract class DiscountCalculator
{
public abstract decimal CalculateDiscount(decimal amount);
}
public class RegularCustomerDiscount : DiscountCalculator
{
public override decimal CalculateDiscount(decimal amount)
{
return amount * 0.1m;
}
}
public class PremiumCustomerDiscount : DiscountCalculator
{
public override decimal CalculateDiscount(decimal amount)
{
return amount * 0.2m;
}
}
3. Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering the correctness of the program.
Bad Example:
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("Flying");
}
}
public class Ostrich : Bird
{
public override void Fly()
{
throw new NotImplementedException("Ostriches can't fly");
}
}
Good Example:
public abstract class Bird
{
public abstract void Move();
}
public class Sparrow : Bird
{
public override void Move()
{
Console.WriteLine("Flying");
}
}
public class Ostrich : Bird
{
public override void Move()
{
Console.WriteLine("Running");
}
}
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Bad Example:
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class Robot : IWorker
{
public void Work() { }
public void Eat() { throw new NotImplementedException(); } // Robots don't eat
public void Sleep() { throw new NotImplementedException(); } // Robots don't sleep
}
Good Example:
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public class Human : IWorkable, IEatable, ISleepable
{
public void Work() { }
public void Eat() { }
public void Sleep() { }
}
public class Robot : IWorkable
{
public void Work() { }
}
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Example:
public class OrderService
{
private readonly SqlDatabase _database = new SqlDatabase(); // Tight coupling
}
Good Example:
public interface IDatabase
{
void Save(Order order);
}
public class SqlDatabase : IDatabase
{
public void Save(Order order) { }
}
public class OrderService
{
private readonly IDatabase _database;
public OrderService(IDatabase database) // Dependency injected
{
_database = database;
}
}
Design Patterns— (Creational, Structural, Behavioral)
Factory Pattern (C)
Creates objects without specifying the exact class.
Bad Example:
public class Product
{
public string Name { get; set; }
}
public class ProductFactory
{
public Product CreateProduct(string type)
{
if (type == "A")
{
return new Product { Name = "Product A" };
}
else if (type == "B")
{
return new Product { Name = "Product B" };
}
throw new ArgumentException("Invalid type");
}
}
Good Example:
public interface IProduct
{
string Name { get; }
}
public class ProductA : IProduct
{
public string Name => "Product A";
}
public class ProductB : IProduct
{
public string Name => "Product B";
}
public class ProductFactory
{
public IProduct CreateProduct(string type)
{
return type switch
{
"A" => new ProductA(),
"B" => new ProductB(),
_ => throw new ArgumentException("Invalid type")
};
}
}
Why it's better:
Decouples object creation: The client code doesn’t need to know the exact class being instantiated.
Promotes extensibility: Adding new product types doesn’t require changes to the client code.
Abstract Factory Pattern
Provides an interface for creating families of related or dependent objects.
Bad Example:
public class Chair
{
public string Type { get; set; }
}
public class Sofa
{
public string Type { get; set; }
}
public class FurnitureFactory
{
public Chair CreateChair(string style)
{
return new Chair { Type = $"{style} Chair" };
}
public Sofa CreateSofa(string style)
{
return new Sofa { Type = $"{style} Sofa" };
}
}
Good Example:
public interface IChair
{
string Type { get; }
}
public interface ISofa
{
string Type { get; }
}
public class ModernChair : IChair
{
public string Type => "Modern Chair";
}
public class ModernSofa : ISofa
{
public string Type => "Modern Sofa";
}
public interface IFurnitureFactory
{
IChair CreateChair();
ISofa CreateSofa();
}
public class ModernFurnitureFactory : IFurnitureFactory
{
public IChair CreateChair() => new ModernChair();
public ISofa CreateSofa() => new ModernSofa();
}
Why it's better:
Encapsulates object creation, Ensures that the created objects are compatible (e.g., all modern furniture).
Supports multiple families, Easily switch between different families of related objects (e.g., modern vs. vintage).
Singleton Pattern
Ensures a class has only one instance and provides a global point of access to it.
Bad Example:
public class Logger
{
private static Logger _instance;
private Logger() { }
public static Logger GetInstance()
{
if (_instance == null)
{
_instance = new Logger();
}
return _instance;
}
public void Log(string message)
{
Console.WriteLine(message);
}
}
Good Example:
public sealed class Logger
{
private static readonly Lazy<Logger> _instance = new Lazy<Logger>(() => new Logger());
private Logger() { }
public static Logger Instance => _instance.Value;
public void Log(string message)
{
Console.WriteLine(message);
}
}
Why it's better:
Thread-safe using
Lazy<T>
.Ensures only one instance is created.
Bridge Pattern (S)
Decouples an abstraction from its implementation so that the two can vary independently.
Bad Example:
public class Circle
{
public string Color { get; set; }
public void Draw()
{
if (Color == "Red")
{
Console.WriteLine("Drawing a red circle");
}
else if (Color == "Blue")
{
Console.WriteLine("Drawing a blue circle");
}
}
}
Good Example:
public interface IColor
{
string ApplyColor();
}
public class Red : IColor
{
public string ApplyColor() => "Red";
}
public class Blue : IColor
{
public string ApplyColor() => "Blue";
}
public abstract class Shape
{
protected IColor Color;
protected Shape(IColor color)
{
Color = color;
}
public abstract void Draw();
}
public class Circle : Shape
{
public Circle(IColor color) : base(color) { }
public override void Draw()
{
Console.WriteLine($"Drawing a {Color.ApplyColor()} circle");
}
}
Why it's better:
Decouples abstraction and implementation, Changes in color implementation don’t affect the shape class.
Promotes flexibility, Allows combining different shapes and colors independently.
Decorator Pattern
Adds behavior to objects dynamically without affecting other objects.
Bad Example:
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
public class BasicCoffee : ICoffee
{
public string GetDescription() => "Basic Coffee";
public decimal GetCost() => 5.0m;
}
public class MilkCoffee : ICoffee
{
private readonly ICoffee _coffee;
public MilkCoffee(ICoffee coffee)
{
_coffee = coffee;
}
public string GetDescription() => _coffee.GetDescription() + ", Milk";
public decimal GetCost() => _coffee.GetCost() + 1.0m;
}
public class SugarCoffee : ICoffee
{
private readonly ICoffee _coffee;
public SugarCoffee(ICoffee coffee)
{
_coffee = coffee;
}
public string GetDescription() => _coffee.GetDescription() + ", Sugar";
public decimal GetCost() => _coffee.GetCost() + 0.5m;
}
Good Example:
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public abstract string GetDescription();
public abstract decimal GetCost();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override decimal GetCost() => _coffee.GetCost() + 1.0m;
}
public class SugarDecorator : CoffeeDecorator
{
public SugarDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Sugar";
public override decimal GetCost() => _coffee.GetCost() + 0.5m;
}
Why it's better:
Follows the Open/Closed Principle (OCP).
Allows dynamic addition of responsibilities.
Facade Pattern
Provides a simplified interface to a complex subsystem.
Bad Example:
public class SubsystemA
{
public void OperationA() => Console.WriteLine("SubsystemA: OperationA");
}
public class SubsystemB
{
public void OperationB() => Console.WriteLine("SubsystemB: OperationB");
}
public class Client
{
public void PerformOperations()
{
var subsystemA = new SubsystemA();
var subsystemB = new SubsystemB();
subsystemA.OperationA();
subsystemB.OperationB();
}
}
Good Example:
public class SubsystemA
{
public void OperationA() => Console.WriteLine("SubsystemA: OperationA");
}
public class SubsystemB
{
public void OperationB() => Console.WriteLine("SubsystemB: OperationB");
}
public class Facade
{
private readonly SubsystemA _subsystemA;
private readonly SubsystemB _subsystemB;
public Facade()
{
_subsystemA = new SubsystemA();
_subsystemB = new SubsystemB();
}
public void PerformOperations()
{
_subsystemA.OperationA();
_subsystemB.OperationB();
}
}
public class Client
{
public void Run()
{
var facade = new Facade();
facade.PerformOperations();
}
}
Why it's better:
Simplifies client interaction, Clients interact with a single facade instead of multiple subsystems.
Reduces coupling, Changes in subsystems don’t affect the client code.
Command Pattern
Encapsulates a request as an object, allowing parameterization of clients with queues, requests, and operations.
Bad Example:
public class Light
{
public void On() => Console.WriteLine("Light is on");
public void Off() => Console.WriteLine("Light is off");
}
public class RemoteControl
{
private readonly Light _light;
public RemoteControl(Light light)
{
_light = light;
}
public void PressButton(string command)
{
if (command == "On")
{
_light.On();
}
else if (command == "Off")
{
_light.Off();
}
}
}
Good Example:
public interface ICommand
{
void Execute();
}
public class LightOnCommand : ICommand
{
private readonly Light _light;
public LightOnCommand(Light light)
{
_light = light;
}
public void Execute() => _light.On();
}
public class LightOffCommand : ICommand
{
private readonly Light _light;
public LightOffCommand(Light light)
{
_light = light;
}
public void Execute() => _light.Off();
}
public class RemoteControl
{
private ICommand _command;
public void SetCommand(ICommand command)
{
_command = command;
}
public void PressButton()
{
_command.Execute();
}
}
Why it's better:
Decouples the sender (RemoteControl) from the receiver (Light).
Supports undo/redo functionality.
Observer Pattern
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.
Bad Example:
public class Subject
{
private int _state;
public int State
{
get => _state;
set
{
_state = value;
Notify();
}
}
private void Notify()
{
// Directly notify observers (tight coupling)
Console.WriteLine("State changed to: " + _state);
}
}
Good Example:
public interface IObserver
{
void Update(int state);
}
public class Subject
{
private readonly List<IObserver> _observers = new List<IObserver>();
private int _state;
public int State
{
get => _state;
set
{
_state = value;
Notify();
}
}
public void Attach(IObserver observer)
{
_observers.Add(observer);
}
private void Notify()
{
foreach (var observer in _observers)
{
observer.Update(_state);
}
}
}
public class ConcreteObserver : IObserver
{
public void Update(int state)
{
Console.WriteLine("State changed to: " + state);
}
}
Why it's better:
Decouples subjects and observers, Observers can be added or removed dynamically.
Promotes flexibility, Subjects don’t need to know the details of their observers.
Adapter Pattern
Allows incompatible interfaces to work together.
Bad Example:
public class LegacyPrinter
{
public void PrintDocument(string text)
{
Console.WriteLine($"Legacy Printer: {text}");
}
}
public class ModernPrinter
{
public void Print(string content)
{
Console.WriteLine($"Modern Printer: {content}");
}
}
public class Client
{
public void PrintDocument(ModernPrinter printer, string text)
{
printer.Print(text);
}
}
Good Example:
public interface IPrinter
{
void Print(string text);
}
public class LegacyPrinterAdapter : IPrinter
{
private readonly LegacyPrinter _legacyPrinter;
public LegacyPrinterAdapter(LegacyPrinter legacyPrinter)
{
_legacyPrinter = legacyPrinter;
}
public void Print(string text)
{
_legacyPrinter.PrintDocument(text);
}
}
public class Client
{
public void PrintDocument(IPrinter printer, string text)
{
printer.Print(text);
}
}
Why it's better:
Makes the
LegacyPrinter
compatible with theModernPrinter
interface.Promotes reusability and flexibility.
Strategy Pattern
Encapsulates interchangeable behaviors and uses delegation to decide which behavior to use.
Bad Example:
public class Context
{
public void Execute(string strategy)
{
if (strategy == "A")
{
Console.WriteLine("Executing Strategy A");
}
else if (strategy == "B")
{
Console.WriteLine("Executing Strategy B");
}
}
}
Good Example:
public interface IStrategy
{
void Execute();
}
public class StrategyA : IStrategy
{
public void Execute() => Console.WriteLine("Executing Strategy A");
}
public class StrategyB : IStrategy
{
public void Execute() => Console.WriteLine("Executing Strategy B");
}
public class Context
{
private IStrategy _strategy;
public void SetStrategy(IStrategy strategy)
{
_strategy = strategy;
}
public void Execute()
{
_strategy.Execute();
}
}
Why it's better:
Encapsulates algorithms, Each strategy is isolated and reusable.
Promotes flexibility, Algorithms can be swapped at runtime.
Template Method Pattern
Defines the skeleton of an algorithm in a method, deferring some steps to subclasses.
Bad Example:
public class Coffee
{
public void Prepare()
{
BoilWater();
BrewCoffee();
PourInCup();
AddSugarAndMilk();
}
private void BoilWater() => Console.WriteLine("Boiling water");
private void BrewCoffee() => Console.WriteLine("Brewing coffee");
private void PourInCup() => Console.WriteLine("Pouring into cup");
private void AddSugarAndMilk() => Console.WriteLine("Adding sugar and milk");
}
public class Tea
{
public void Prepare()
{
BoilWater();
SteepTea();
PourInCup();
AddLemon();
}
private void BoilWater() => Console.WriteLine("Boiling water");
private void SteepTea() => Console.WriteLine("Steeping tea");
private void PourInCup() => Console.WriteLine("Pouring into cup");
private void AddLemon() => Console.WriteLine("Adding lemon");
}
Good Example:
public abstract class Beverage
{
public void Prepare()
{
BoilWater();
Brew();
PourInCup();
AddCondiments();
}
private void BoilWater() => Console.WriteLine("Boiling water");
protected abstract void Brew();
private void PourInCup() => Console.WriteLine("Pouring into cup");
protected abstract void AddCondiments();
}
public class Coffee : Beverage
{
protected override void Brew() => Console.WriteLine("Brewing coffee");
protected override void AddCondiments() => Console.WriteLine("Adding sugar and milk");
}
public class Tea : Beverage
{
protected override void Brew() => Console.WriteLine("Steeping tea");
protected override void AddCondiments() => Console.WriteLine("Adding lemon");
}
Why it's better:
Avoids code duplication.
Encapsulates the algorithm structure.
Proxy Pattern
Provides a surrogate or placeholder for another object to control access to it.
Bad Example:
public class RealImage
{
private readonly string _filename;
public RealImage(string filename)
{
_filename = filename;
LoadFromDisk();
}
private void LoadFromDisk() => Console.WriteLine($"Loading {_filename}");
public void Display() => Console.WriteLine($"Displaying {_filename}");
}
public class Client
{
public void ShowImage(string filename)
{
var image = new RealImage(filename); // Image is loaded even if not displayed
image.Display();
}
}
Good Example:
public interface IImage
{
void Display();
}
public class RealImage : IImage
{
private readonly string _filename;
public RealImage(string filename)
{
_filename = filename;
LoadFromDisk();
}
private void LoadFromDisk() => Console.WriteLine($"Loading {_filename}");
public void Display() => Console.WriteLine($"Displaying {_filename}");
}
public class ProxyImage : IImage
{
private readonly string _filename;
private RealImage _realImage;
public ProxyImage(string filename)
{
_filename = filename;
}
public void Display()
{
if (_realImage == null)
{
_realImage = new RealImage(_filename); // Lazy loading
}
_realImage.Display();
}
}
Why it's better:
Implements lazy loading.
Controls access to the real object.
Builder Pattern (C)
Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
Bad Example:
public class Pizza
{
public string Dough { get; set; }
public string Sauce { get; set; }
public string Topping { get; set; }
public Pizza(string dough, string sauce, string topping)
{
Dough = dough;
Sauce = sauce;
Topping = topping;
}
}
public class PizzaMaker
{
public Pizza MakePizza()
{
return new Pizza("Thin Crust", "Tomato", "Cheese"); // Hardcoded values
}
}
Good Example:
public class Pizza
{
public string Dough { get; set; }
public string Sauce { get; set; }
public string Topping { get; set; }
public void Display()
{
Console.WriteLine($"Dough: {Dough}, Sauce: {Sauce}, Topping: {Topping}");
}
}
public interface IPizzaBuilder
{
void SetDough();
void SetSauce();
void SetTopping();
Pizza GetPizza();
}
public class HawaiianPizzaBuilder : IPizzaBuilder
{
private Pizza _pizza = new Pizza();
public void SetDough() => _pizza.Dough = "Thin Crust";
public void SetSauce() => _pizza.Sauce = "Tomato";
public void SetTopping() => _pizza.Topping = "Ham and Pineapple";
public Pizza GetPizza() => _pizza;
}
public class PizzaDirector
{
private readonly IPizzaBuilder _builder;
public PizzaDirector(IPizzaBuilder builder)
{
_builder = builder;
}
public void MakePizza()
{
_builder.SetDough();
_builder.SetSauce();
_builder.SetTopping();
}
}
Why it's better:
Allows flexible construction of objects.
Avoids hardcoding values.
Chain of Responsibility Pattern (B)
Passes a request along a chain of handlers, where each handler either processes the request or passes it to the next handler.
Bad Example:
public class RequestProcessor
{
public void ProcessRequest(int request)
{
if (request < 10)
{
Console.WriteLine("Handled by Handler A");
}
else if (request < 20)
{
Console.WriteLine("Handled by Handler B");
}
else
{
Console.WriteLine("Handled by Handler C");
}
}
}
Good Example:
public abstract class Handler
{
protected Handler _nextHandler;
public void SetNextHandler(Handler nextHandler)
{
_nextHandler = nextHandler;
}
public abstract void HandleRequest(int request);
}
public class HandlerA : Handler
{
public override void HandleRequest(int request)
{
if (request < 10)
{
Console.WriteLine("Handled by Handler A");
}
else
{
_nextHandler?.HandleRequest(request);
}
}
}
public class HandlerB : Handler
{
public override void HandleRequest(int request)
{
if (request < 20)
{
Console.WriteLine("Handled by Handler B");
}
else
{
_nextHandler?.HandleRequest(request);
}
}
}
public class HandlerC : Handler
{
public override void HandleRequest(int request)
{
Console.WriteLine("Handled by Handler C");
}
}
Why it's better:
Decouples the sender and receiver.
Allows dynamic handling of requests.
State Pattern
Allows an object to change its behavior when its internal state changes.
Bad Example:
public class Fan
{
private int _state = 0; // 0: Off, 1: Low, 2: High
public void PullChain()
{
if (_state == 0)
{
Console.WriteLine("Turning fan on to low.");
_state = 1;
}
else if (_state == 1)
{
Console.WriteLine("Turning fan on to high.");
_state = 2;
}
else if (_state == 2)
{
Console.WriteLine("Turning fan off.");
_state = 0;
}
}
}
Good Example:
public interface IFanState
{
void HandleRequest(Fan fan);
}
public class OffState : IFanState
{
public void HandleRequest(Fan fan)
{
Console.WriteLine("Turning fan on to low.");
fan.SetState(new LowState());
}
}
public class LowState : IFanState
{
public void HandleRequest(Fan fan)
{
Console.WriteLine("Turning fan on to high.");
fan.SetState(new HighState());
}
}
public class HighState : IFanState
{
public void HandleRequest(Fan fan)
{
Console.WriteLine("Turning fan off.");
fan.SetState(new OffState());
}
}
public class Fan
{
private IFanState _state;
public Fan()
{
_state = new OffState();
}
public void SetState(IFanState state)
{
_state = state;
}
public void PullChain()
{
_state.HandleRequest(this);
}
}
Why it's better:
Encapsulates state-specific behavior.
Makes it easy to add new states.
Flyweight Pattern
Reduces memory usage by sharing as much data as possible with similar objects.
Bad Example:
public class Tree
{
public string Type { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public string Color { get; set; }
public Tree(string type, int height, int width, string color)
{
Type = type;
Height = height;
Width = width;
Color = color;
}
public void Draw(int x, int y)
{
Console.WriteLine($"Drawing {Type} tree at ({x}, {y})");
}
}
Good Example:
public class TreeType
{
public string Type { get; }
public string Color { get; }
public TreeType(string type, string color)
{
Type = type;
Color = color;
}
public void Draw(int x, int y)
{
Console.WriteLine($"Drawing {Type} tree at ({x}, {y})");
}
}
public class Tree
{
public int X { get; }
public int Y { get; }
public TreeType TreeType { get; }
public Tree(int x, int y, TreeType treeType)
{
X = x;
Y = y;
TreeType = treeType;
}
public void Draw()
{
TreeType.Draw(X, Y);
}
}
public class TreeFactory
{
private static readonly Dictionary<string, TreeType> _treeTypes = new Dictionary<string, TreeType>();
public static TreeType GetTreeType(string type, string color)
{
if (!_treeTypes.ContainsKey(type))
{
_treeTypes[type] = new TreeType(type, color);
}
return _treeTypes[type];
}
}
Why it's better:
Reduces memory usage by sharing common data.
Improves performance for large numbers of similar objects.
Memento Pattern
Captures and externalizes an object’s internal state without violating encapsulation, so the object can be restored to this state later.
Bad Example:
public class Editor
{
public string Content { get; set; }
public void SaveState()
{
// Save to file or database
}
public void RestoreState()
{
// Restore from file or database
}
}
Good Example:
public class EditorMemento
{
public string Content { get; }
public EditorMemento(string content)
{
Content = content;
}
}
public class Editor
{
public string Content { get; set; }
public EditorMemento Save()
{
return new EditorMemento(Content);
}
public void Restore(EditorMemento memento)
{
Content = memento.Content;
}
}
public class History
{
private readonly Stack<EditorMemento> _mementos = new Stack<EditorMemento>();
public void Save(EditorMemento memento)
{
_mementos.Push(memento);
}
public EditorMemento Undo()
{
return _mementos.Pop();
}
}
Why it's better:
Preserves encapsulation.
Provides undo functionality.
Visitor Pattern
Adds new operations to a collection of objects without modifying their structure.
Bad Example:
public interface IAnimal
{
void Speak();
}
public class Dog : IAnimal
{
public void Speak() => Console.WriteLine("Woof!");
}
public class Cat : IAnimal
{
public void Speak() => Console.WriteLine("Meow!");
}
public class Zoo
{
private readonly List<IAnimal> _animals = new List<IAnimal>();
public void AddAnimal(IAnimal animal)
{
_animals.Add(animal);
}
public void PerformSpeak()
{
foreach (var animal in _animals)
{
animal.Speak();
}
}
}
Good Example:
public interface IAnimal
{
void Accept(IAnimalVisitor visitor);
}
public class Dog : IAnimal
{
public void Accept(IAnimalVisitor visitor) => visitor.Visit(this);
}
public class Cat : IAnimal
{
public void Accept(IAnimalVisitor visitor) => visitor.Visit(this);
}
public interface IAnimalVisitor
{
void Visit(Dog dog);
void Visit(Cat cat);
}
public class SpeakVisitor : IAnimalVisitor
{
public void Visit(Dog dog) => Console.WriteLine("Woof!");
public void Visit(Cat cat) => Console.WriteLine("Meow!");
}
public class Zoo
{
private readonly List<IAnimal> _animals = new List<IAnimal>();
public void AddAnimal(IAnimal animal)
{
_animals.Add(animal);
}
public void PerformSpeak(IAnimalVisitor visitor)
{
foreach (var animal in _animals)
{
animal.Accept(visitor);
}
}
}
Why it's better:
Adds new operations without modifying existing classes.
Follows the Open/Closed Principle (OCP).
Iterator Pattern
Provides a way to access elements of a collection sequentially without exposing its underlying representation.
Bad Example:
public class MyCollection
{
private readonly List<string> _items = new List<string>();
public void AddItem(string item)
{
_items.Add(item);
}
public List<string> GetItems()
{
return _items; // Exposes internal representation
}
}
Good Example:
public interface IIterator
{
bool HasNext();
string Next();
}
public class MyCollection
{
private readonly List<string> _items = new List<string>();
public void AddItem(string item)
{
_items.Add(item);
}
public IIterator CreateIterator()
{
return new MyIterator(_items);
}
}
public class MyIterator : IIterator
{
private readonly List<string> _items;
private int _position = 0;
public MyIterator(List<string> items)
{
_items = items;
}
public bool HasNext()
{
return _position < _items.Count;
}
public string Next()
{
return _items[_position++];
}
}
Why it's better:
Encapsulates the collection’s internal structure.
Provides a consistent way to iterate over different collections.
Mediator Pattern
Defines an object that encapsulates how a set of objects interact, promoting loose coupling.
Bad Example:
public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
public void SendMessage(string message, User recipient)
{
Console.WriteLine($"{Name} sends: {message} to {recipient.Name}");
}
}
Good Example:
public interface IChatMediator
{
void SendMessage(string message, User sender, User recipient);
}
public class ChatMediator : IChatMediator
{
public void SendMessage(string message, User sender, User recipient)
{
Console.WriteLine($"{sender.Name} sends: {message} to {recipient.Name}");
}
}
public class User
{
public string Name { get; }
private readonly IChatMediator _mediator;
public User(string name, IChatMediator mediator)
{
Name = name;
_mediator = mediator;
}
public void SendMessage(string message, User recipient)
{
_mediator.SendMessage(message, this, recipient);
}
}
Why it's better:
Reduces direct dependencies between objects.
Centralizes communication logic.
Prototype Pattern
Creates new objects by copying an existing object (prototype).
Bad Example:
public class Car
{
public string Model { get; set; }
public string Color { get; set; }
public Car(string model, string color)
{
Model = model;
Color = color;
}
public Car Clone()
{
return new Car(Model, Color); // Manual cloning
}
}
Good Example:
public interface IPrototype
{
IPrototype Clone();
}
public class Car : IPrototype
{
public string Model { get; set; }
public string Color { get; set; }
public Car(string model, string color)
{
Model = model;
Color = color;
}
public IPrototype Clone()
{
return MemberwiseClone() as IPrototype; // Shallow copy
}
}
Why it's better:
Simplifies object creation.
Avoids repetitive initialization code.
Composite Pattern
Composes objects into tree structures to represent part-whole hierarchies.
Bad Example:
public class File
{
public string Name { get; }
public File(string name)
{
Name = name;
}
public void Display()
{
Console.WriteLine($"File: {Name}");
}
}
public class Folder
{
private readonly List<File> _files = new List<File>();
public void AddFile(File file)
{
_files.Add(file);
}
public void Display()
{
foreach (var file in _files)
{
file.Display();
}
}
}
Good Example:
public interface IFileSystemItem
{
void Display();
}
public class File : IFileSystemItem
{
public string Name { get; }
public File(string name)
{
Name = name;
}
public void Display()
{
Console.WriteLine($"File: {Name}");
}
}
public class Folder : IFileSystemItem
{
private readonly List<IFileSystemItem> _items = new List<IFileSystemItem>();
public void AddItem(IFileSystemItem item)
{
_items.Add(item);
}
public void Display()
{
foreach (var item in _items)
{
item.Display();
}
}
}
Why it's better:
Treats individual objects and compositions uniformly.
Simplifies client code.
Interpreter Pattern
Defines a grammatical representation for a language and provides an interpreter to evaluate sentences in the language.
Bad Example:
public class Calculator
{
public int Evaluate(string expression)
{
// Simple parsing logic (not scalable)
if (expression.Contains("+"))
{
var parts = expression.Split('+');
return int.Parse(parts[0]) + int.Parse(parts[1]);
}
else if (expression.Contains("-"))
{
var parts = expression.Split('-');
return int.Parse(parts[0]) - int.Parse(parts[1]);
}
throw new ArgumentException("Invalid expression");
}
}
Good Example:
public interface IExpression
{
int Interpret();
}
public class Number : IExpression
{
private readonly int _value;
public Number(int value)
{
_value = value;
}
public int Interpret() => _value;
}
public class Add : IExpression
{
private readonly IExpression _left;
private readonly IExpression _right;
public Add(IExpression left, IExpression right)
{
_left = left;
_right = right;
}
public int Interpret() => _left.Interpret() + _right.Interpret();
}
public class Subtract : IExpression
{
private readonly IExpression _left;
private readonly IExpression _right;
public Subtract(IExpression left, IExpression right)
{
_left = left;
_right = right;
}
public int Interpret() => _left.Interpret() - _right.Interpret();
}
Why it's better:
Provides a flexible way to evaluate expressions.
Extensible for new grammar rules.
Some other patterns—
Null Object Pattern
Provides a default object to avoid null references.
Bad Example:
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public class Application
{
private readonly ILogger _logger;
public Application(ILogger logger)
{
_logger = logger;
}
public void Run()
{
_logger?.Log("Application started"); // Null check required
}
}
Good Example:
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public class NullLogger : ILogger
{
public void Log(string message)
{
// Do nothing
}
}
public class Application
{
private readonly ILogger _logger;
public Application(ILogger logger)
{
_logger = logger ?? new NullLogger(); // Default behavior
}
public void Run()
{
_logger.Log("Application started"); // No null check needed
}
}
Why it's better:
Eliminates null checks.
Provides a default behavior.
19. Specification Pattern
Encapsulates business rules for reusable and composable filtering.
Bad Example:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductFilter
{
public List<Product> FilterByPrice(List<Product> products, decimal minPrice)
{
return products.Where(p => p.Price >= minPrice).ToList();
}
}
Good Example:
public interface ISpecification<T>
{
bool IsSatisfiedBy(T item);
}
public class PriceSpecification : ISpecification<Product>
{
private readonly decimal _minPrice;
public PriceSpecification(decimal minPrice)
{
_minPrice = minPrice;
}
public bool IsSatisfiedBy(Product product)
{
return product.Price >= _minPrice;
}
}
public class ProductFilter
{
public List<Product> Filter(List<Product> products, ISpecification<Product> spec)
{
return products.Where(spec.IsSatisfiedBy).ToList();
}
}
Why it's better:
Encapsulates business rules.
Supports composable and reusable specifications.
Some additional patterns
20. Service Locator Pattern
Provides a global point of access to a service without coupling clients to the concrete implementation.
Bad Example:
public class Logger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public class Application
{
private readonly Logger _logger = new Logger(); // Tight coupling
public void Run()
{
_logger.Log("Application started");
}
}
Good Example:
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();
public static void Register<T>(T service)
{
_services[typeof(T)] = service;
}
public static T Resolve<T>()
{
return (T)_services[typeof(T)];
}
}
public class Application
{
public void Run()
{
var logger = ServiceLocator.Resolve<ILogger>();
logger.Log("Application started");
}
}
Why it's better:
Decouples the client from the service implementation.
Centralizes service management.
Specification Pattern
Encapsulates business rules for reusable and composable filtering.
Bad Example:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductFilter
{
public List<Product> FilterByPrice(List<Product> products, decimal minPrice)
{
return products.Where(p => p.Price >= minPrice).ToList();
}
}
Good Example:
public interface ISpecification<T>
{
bool IsSatisfiedBy(T item);
}
public class PriceSpecification : ISpecification<Product>
{
private readonly decimal _minPrice;
public PriceSpecification(decimal minPrice)
{
_minPrice = minPrice;
}
public bool IsSatisfiedBy(Product product)
{
return product.Price >= _minPrice;
}
}
public class ProductFilter
{
public List<Product> Filter(List<Product> products, ISpecification<Product> spec)
{
return products.Where(spec.IsSatisfiedBy).ToList();
}
}
Why it's better:
Encapsulates business rules.
Supports composable and reusable specifications.
CQRS Pattern (Command Query Responsibility Segregation)
Separates read and write operations into different models.
Bad Example:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductService
{
private readonly List<Product> _products = new List<Product>();
public void AddProduct(Product product)
{
_products.Add(product);
}
public Product GetProduct(int id)
{
return _products.FirstOrDefault(p => p.Id == id);
}
}
Good Example:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductCommandService
{
private readonly List<Product> _products = new List<Product>();
public void AddProduct(Product product)
{
_products.Add(product);
}
}
public class ProductQueryService
{
private readonly List<Product> _products;
public ProductQueryService(List<Product> products)
{
_products = products;
}
public Product GetProduct(int id)
{
return _products.FirstOrDefault(p => p.Id == id);
}
}
Why it's better:
Separates concerns for read and write operations.
Improves scalability and maintainability.
Event Sourcing Pattern
Stores the state of an application as a sequence of events.
Bad Example:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductService
{
private readonly List<Product> _products = new List<Product>();
public void AddProduct(Product product)
{
_products.Add(product);
}
public void UpdateProduct(int id, string name, decimal price)
{
var product = _products.FirstOrDefault(p => p.Id == id);
if (product != null)
{
product.Name = name;
product.Price = price;
}
}
}
Good Example:
public class ProductEvent
{
public int ProductId { get; set; }
public string EventType { get; set; }
public string Data { get; set; }
}
public class ProductEventStore
{
private readonly List<ProductEvent> _events = new List<ProductEvent>();
public void AddEvent(ProductEvent @event)
{
_events.Add(@event);
}
public List<ProductEvent> GetEvents()
{
return _events;
}
}
public class ProductService
{
private readonly ProductEventStore _eventStore;
public ProductService(ProductEventStore eventStore)
{
_eventStore = eventStore;
}
public void AddProduct(int id, string name, decimal price)
{
_eventStore.AddEvent(new ProductEvent
{
ProductId = id,
EventType = "ProductAdded",
Data = $"Name: {name}, Price: {price}"
});
}
public void UpdateProduct(int id, string name, decimal price)
{
_eventStore.AddEvent(new ProductEvent
{
ProductId = id,
EventType = "ProductUpdated",
Data = $"Name: {name}, Price: {price}"
});
}
}
Why it's better:
Provides a complete history of changes.
Enables auditing and replaying events.
Pipeline Pattern
Processes data through a series of stages (handlers) in a sequence.
Bad Example:
public class DataProcessor
{
public void Process(string data)
{
// Step 1: Validate data
if (string.IsNullOrEmpty(data))
{
throw new ArgumentException("Data is invalid");
}
// Step 2: Transform data
data = data.ToUpper();
// Step 3: Save data
Console.WriteLine($"Data saved: {data}");
}
}
Good Example:
public interface IPipelineStep<T>
{
T Process(T input);
}
public class ValidationStep : IPipelineStep<string>
{
public string Process(string input)
{
if (string.IsNullOrEmpty(input))
{
throw new ArgumentException("Data is invalid");
}
return input;
}
}
public class TransformationStep : IPipelineStep<string>
{
public string Process(string input)
{
return input.ToUpper();
}
}
public class SaveStep : IPipelineStep<string>
{
public string Process(string input)
{
Console.WriteLine($"Data saved: {input}");
return input;
}
}
public class Pipeline<T>
{
private readonly List<IPipelineStep<T>> _steps = new List<IPipelineStep<T>>();
public void AddStep(IPipelineStep<T> step)
{
_steps.Add(step);
}
public T Execute(T input)
{
foreach (var step in _steps)
{
input = step.Process(input);
}
return input;
}
}
Why it's better:
Separates concerns into individual steps.
Makes the pipeline extensible and reusable.
Blackboard Pattern
A shared knowledge repository where multiple components collaborate to solve a problem.
Bad Example:
public class ProblemSolver
{
private readonly Dictionary<string, object> _knowledge = new Dictionary<string, object>();
public void Solve()
{
// Step 1: Gather data
_knowledge["data"] = FetchData();
// Step 2: Analyze data
_knowledge["result"] = AnalyzeData(_knowledge["data"]);
// Step 3: Present result
Console.WriteLine($"Result: {_knowledge["result"]}");
}
private object FetchData() => "Some data";
private object AnalyzeData(object data) => $"Analyzed {data}";
}
Good Example:
public class Blackboard
{
private readonly Dictionary<string, object> _knowledge = new Dictionary<string, object>();
public void Set(string key, object value)
{
_knowledge[key] = value;
}
public object Get(string key)
{
return _knowledge.ContainsKey(key) ? _knowledge[key] : null;
}
}
public class DataFetcher
{
public void Fetch(Blackboard blackboard)
{
blackboard.Set("data", "Some data");
}
}
public class DataAnalyzer
{
public void Analyze(Blackboard blackboard)
{
var data = blackboard.Get("data");
blackboard.Set("result", $"Analyzed {data}");
}
}
public class ResultPresenter
{
public void Present(Blackboard blackboard)
{
var result = blackboard.Get("result");
Console.WriteLine($"Result: {result}");
}
}
public class ProblemSolver
{
private readonly Blackboard _blackboard = new Blackboard();
private readonly DataFetcher _dataFetcher = new DataFetcher();
private readonly DataAnalyzer _dataAnalyzer = new DataAnalyzer();
private readonly ResultPresenter _resultPresenter = new ResultPresenter();
public void Solve()
{
_dataFetcher.Fetch(_blackboard);
_dataAnalyzer.Analyze(_blackboard);
_resultPresenter.Present(_blackboard);
}
}
Why it's better:
Decouples components.
Promotes collaboration through a shared knowledge base.
Double Dispatch Pattern
Uses multiple method calls to determine the behavior of an operation based on the types of two objects.
Bad Example:
public class Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
}
public class AreaCalculator
{
public int CalculateArea(object shape)
{
if (shape is Rectangle rect)
{
return rect.Width * rect.Height;
}
throw new ArgumentException("Unsupported shape");
}
}
Good Example:
public interface IShape
{
int Accept(IAreaCalculator calculator);
}
public class Rectangle : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int Accept(IAreaCalculator calculator)
{
return calculator.Calculate(this);
}
}
public interface IAreaCalculator
{
int Calculate(Rectangle rectangle);
}
public class AreaCalculator : IAreaCalculator
{
public int Calculate(Rectangle rectangle)
{
return rectangle.Width * rectangle.Height;
}
}
Why it's better:
Avoids type checks and casts.
Extensible for new shapes and calculators.
Role Interface Pattern
Defines interfaces based on the roles an object can play.
Bad Example:
public class User
{
public string Name { get; set; }
public bool IsAdmin { get; set; }
}
public class AdminDashboard
{
public void Display(User user)
{
if (user.IsAdmin)
{
Console.WriteLine($"Welcome, {user.Name}");
}
else
{
throw new UnauthorizedAccessException("Access denied");
}
}
}
Good Example:
public interface IAdmin
{
string Name { get; }
}
public class User : IAdmin
{
public string Name { get; set; }
public bool IsAdmin { get; set; }
}
public class AdminDashboard
{
public void Display(IAdmin admin)
{
Console.WriteLine($"Welcome, {admin.Name}");
}
}
Why it's better:
Decouples roles from implementations.
Promotes interface segregation.
Type Object Pattern
Encapsulates type information in an object to allow dynamic behavior.
Bad Example:
public class Monster
{
public string Type { get; set; }
public int Health { get; set; }
public void Attack()
{
if (Type == "Dragon")
{
Console.WriteLine("Dragon breathes fire!");
}
else if (Type == "Goblin")
{
Console.WriteLine("Goblin stabs with a knife!");
}
}
}
Good Example:
public class MonsterType
{
public string Name { get; }
public string AttackBehavior { get; }
public MonsterType(string name, string attackBehavior)
{
Name = name;
AttackBehavior = attackBehavior;
}
public void Attack()
{
Console.WriteLine(AttackBehavior);
}
}
public class Monster
{
public MonsterType Type { get; }
public int Health { get; set; }
public Monster(MonsterType type)
{
Type = type;
}
public void Attack()
{
Type.Attack();
}
}
Why it's better:
Encapsulates type-specific behavior.
Allows dynamic addition of new types.
Registry Pattern
Provides a global point of access to objects.
Bad Example:
public class Logger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public class Application
{
private readonly Logger _logger = new Logger(); // Tight coupling
public void Run()
{
_logger.Log("Application started");
}
}
Good Example:
public static class Registry
{
private static readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();
public static void Register<T>(T service)
{
_services[typeof(T)] = service;
}
public static T Resolve<T>()
{
return (T)_services[typeof(T)];
}
}
public class Application
{
public void Run()
{
var logger = Registry.Resolve<Logger>();
logger.Log("Application started");
}
}
Why it's better:
Decouples the client from the service implementation.
Centralizes service management.
Lazy Loading Pattern
Delays the initialization of an object until it is needed.
Bad Example:
public class ExpensiveResource
{
public ExpensiveResource()
{
Console.WriteLine("ExpensiveResource created");
}
public void Use()
{
Console.WriteLine("Using ExpensiveResource");
}
}
public class Application
{
private readonly ExpensiveResource _resource = new ExpensiveResource(); // Eager loading
public void Run()
{
_resource.Use();
}
}
Good Example:
public class ExpensiveResource
{
public ExpensiveResource()
{
Console.WriteLine("ExpensiveResource created");
}
public void Use()
{
Console.WriteLine("Using ExpensiveResource");
}
}
public class Application
{
private Lazy<ExpensiveResource> _resource = new Lazy<ExpensiveResource>(() => new ExpensiveResource());
public void Run()
{
_resource.Value.Use();
}
}
Why it's better:
Improves performance by delaying resource creation.
Reduces memory usage.
Circuit Breaker Pattern
Prevents a system from making repeated requests to a failing service.
Bad Example:
public class ExternalService
{
public string Call()
{
// Simulate a failure
throw new Exception("Service unavailable");
}
}
public class Application
{
private readonly ExternalService _service = new ExternalService();
public void Run()
{
try
{
var result = _service.Call();
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
Good Example:
public class CircuitBreaker
{
private readonly ExternalService _service = new ExternalService();
private bool _isOpen = false;
private DateTime _lastFailureTime = DateTime.MinValue;
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(5);
public string Call()
{
if (_isOpen && DateTime.UtcNow - _lastFailureTime < _timeout)
{
throw new Exception("Circuit breaker is open");
}
try
{
var result = _service.Call();
_isOpen = false;
return result;
}
catch (Exception ex)
{
_isOpen = true;
_lastFailureTime = DateTime.UtcNow;
throw new Exception("Service unavailable", ex);
}
}
}
public class Application
{
private readonly CircuitBreaker _circuitBreaker = new CircuitBreaker();
public void Run()
{
try
{
var result = _circuitBreaker.Call();
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
Why it's better:
Prevents cascading failures.
Improves system resilience.
Event Aggregator Pattern
Centralizes event handling to decouple publishers and subscribers.
Bad Example:
public class Publisher
{
public event EventHandler<string> OnPublish;
public void Publish(string message)
{
OnPublish?.Invoke(this, message);
}
}
public class Subscriber
{
public void HandleEvent(object sender, string message)
{
Console.WriteLine($"Received: {message}");
}
}
public class Application
{
public void Run()
{
var publisher = new Publisher();
var subscriber = new Subscriber();
publisher.OnPublish += subscriber.HandleEvent;
publisher.Publish("Hello");
}
}
Good Example:
public class EventAggregator
{
private readonly Dictionary<Type, List<Delegate>> _handlers = new Dictionary<Type, List<Delegate>>();
public void Subscribe<T>(Action<T> handler)
{
if (!_handlers.ContainsKey(typeof(T)))
{
_handlers[typeof(T)] = new List<Delegate>();
}
_handlers[typeof(T)].Add(handler);
}
public void Publish<T>(T message)
{
if (_handlers.ContainsKey(typeof(T)))
{
foreach (var handler in _handlers[typeof(T)])
{
((Action<T>)handler)(message);
}
}
}
}
public class Publisher
{
private readonly EventAggregator _eventAggregator;
public Publisher(EventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}
public void Publish(string message)
{
_eventAggregator.Publish(message);
}
}
public class Subscriber
{
public Subscriber(EventAggregator eventAggregator)
{
eventAggregator.Subscribe<string>(HandleEvent);
}
public void HandleEvent(string message)
{
Console.WriteLine($"Received: {message}");
}
}
public class Application
{
public void Run()
{
var eventAggregator = new EventAggregator();
var publisher = new Publisher(eventAggregator);
var subscriber = new Subscriber(eventAggregator);
publisher.Publish("Hello");
}
}
Why it's better:
Decouples publishers and subscribers.
Centralizes event handling.
By applying these patterns, you can solve specific design problems effectively and write cleaner, more maintainable, and scalable code. Each pattern addresses a unique challenge and promotes best practices in software design