Skip to main content

8.3 - Declarative vs. Imperative Programming

Programming paradigms can be broadly categorized as either imperative or declarative. Imperative programming focuses on how to achieve a result through explicit steps, while declarative programming focuses on what result is desired without specifying the exact steps to achieve it. C# supports both paradigms, allowing developers to choose the most appropriate approach for each situation.

8.3.1 - Imperative Programming in C#

Imperative programming is the traditional approach where you explicitly define the sequence of steps the computer should take to solve a problem.

8.3.1.1 - Characteristics of Imperative Programming

Imperative programming is characterized by:

  • Explicit control flow: Using statements like if, for, while to control program execution
  • Mutable state: Variables that can be modified throughout the program
  • Step-by-step instructions: Detailed instructions for how to perform operations
  • Focus on how: Emphasis on the process rather than the result
// Imperative approach to finding even numbers
public static List<int> FindEvenNumbers(List<int> numbers)
{
List<int> evenNumbers = new List<int>();

for (int i = 0; i < numbers.Count; i++)
{
if (numbers[i] % 2 == 0)
{
evenNumbers.Add(numbers[i]);
}
}

return evenNumbers;
}

// Imperative approach to calculating sum
public static int CalculateSum(List<int> numbers)
{
int sum = 0;

foreach (int number in numbers)
{
sum += number;
}

return sum;
}

// Imperative approach to sorting
public static void BubbleSort(int[] array)
{
int n = array.Length;

for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (array[j] > array[j + 1])
{
// Swap elements
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}

8.3.1.2 - Advantages of Imperative Programming

Imperative programming offers several benefits:

  • Direct control: Precise control over how operations are performed
  • Efficiency: Can optimize algorithms for specific scenarios
  • Familiarity: Matches how many developers naturally think about problem-solving
  • Debugging: Often easier to trace through step-by-step execution

8.3.1.3 - Common Imperative Patterns in C#

// Iteration patterns
public static void ProcessItems<T>(List<T> items)
{
// For loop with index
for (int i = 0; i < items.Count; i++)
{
Console.WriteLine($"Processing item {i}: {items[i]}");
}

// Foreach loop
foreach (var item in items)
{
Console.WriteLine($"Processing: {item}");
}

// While loop
int index = 0;
while (index < items.Count)
{
Console.WriteLine($"While loop: {items[index]}");
index++;
}

// Do-while loop
index = 0;
do
{
Console.WriteLine($"Do-while loop: {items[index]}");
index++;
} while (index < items.Count);
}

// Conditional logic patterns
public static void ProcessValue(int value)
{
// If-else statements
if (value > 0)
{
Console.WriteLine("Positive");
}
else if (value < 0)
{
Console.WriteLine("Negative");
}
else
{
Console.WriteLine("Zero");
}

// Switch statement
switch (value % 3)
{
case 0:
Console.WriteLine("Divisible by 3");
break;
case 1:
Console.WriteLine("Remainder 1 when divided by 3");
break;
case 2:
Console.WriteLine("Remainder 2 when divided by 3");
break;
}

// Nested conditionals
if (value > 0)
{
if (value % 2 == 0)
{
Console.WriteLine("Positive and even");
}
else
{
Console.WriteLine("Positive and odd");
}
}
}

// State manipulation patterns
public class Counter
{
private int _count;

public void Increment()
{
_count++;
}

public void Decrement()
{
_count--;
}

public void Reset()
{
_count = 0;
}

public int GetCount()
{
return _count;
}
}

8.3.2 - Declarative Approaches

Declarative programming focuses on describing what the program should accomplish without specifying how to achieve it.

8.3.2.1 - Characteristics of Declarative Programming

Declarative programming is characterized by:

  • Expression-based: Using expressions rather than statements
  • Abstraction of control flow: Hiding the details of iteration and control
  • Focus on what: Emphasis on the desired result rather than the process
  • Minimized state changes: Reduced reliance on mutable state
// Declarative approach to finding even numbers using LINQ
public static IEnumerable<int> FindEvenNumbers(IEnumerable<int> numbers)
{
return numbers.Where(n => n % 2 == 0);
}

// Declarative approach to calculating sum
public static int CalculateSum(IEnumerable<int> numbers)
{
return numbers.Sum();
}

// Declarative approach to sorting
public static IEnumerable<int> Sort(IEnumerable<int> numbers)
{
return numbers.OrderBy(n => n);
}

8.3.2.2 - LINQ as a Declarative Tool

LINQ (Language Integrated Query) is the primary declarative programming tool in C#:

// Basic LINQ queries
var numbers = Enumerable.Range(1, 100);

// Filtering
var evenNumbers = numbers.Where(n => n % 2 == 0);

// Projection
var squares = numbers.Select(n => n * n);

// Ordering
var descendingNumbers = numbers.OrderByDescending(n => n);

// Aggregation
var sum = numbers.Sum();
var average = numbers.Average();
var max = numbers.Max();

// Grouping
var groupedByRemainder = numbers.GroupBy(n => n % 3);

// Complex query
var result = numbers
.Where(n => n > 50)
.Select(n => new { Number = n, Square = n * n })
.OrderBy(x => x.Square)
.Take(10);

// Query syntax
var queryResult = from n in numbers
where n > 50
let square = n * n
orderby square
select new { Number = n, Square = square };

8.3.2.3 - Other Declarative Approaches in C#

Beyond LINQ, C# offers several other declarative programming approaches:

// Declarative UI with XAML (WPF, UWP, Xamarin.Forms)
/*
<Button Content="Click Me"
Background="Blue"
Foreground="White"
Click="Button_Click" />
*/

// Declarative configuration with attributes
[Serializable]
[Table("Customers")]
public class Customer
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }

[Required]
[MaxLength(100)]
public string Name { get; set; }

[EmailAddress]
public string Email { get; set; }
}

// Declarative validation with DataAnnotations
public class RegistrationModel
{
[Required(ErrorMessage = "Username is required")]
[StringLength(50, MinimumLength = 3)]
public string Username { get; set; }

[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
public string Email { get; set; }

[Required(ErrorMessage = "Password is required")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
[DataType(DataType.Password)]
public string Password { get; set; }

[Compare("Password", ErrorMessage = "Passwords do not match")]
[DataType(DataType.Password)]
public string ConfirmPassword { get; set; }
}

// Declarative routing in ASP.NET Core
/*
[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
[HttpGet]
public IActionResult GetAll()
{
// Implementation
}

[HttpGet("{id}")]
public IActionResult GetById(int id)
{
// Implementation
}

[HttpPost]
[Authorize(Roles = "Admin")]
public IActionResult Create([FromBody] Customer customer)
{
// Implementation
}
}
*/

// Declarative transaction management
[TransactionScope]
public void TransferFunds(Account from, Account to, decimal amount)
{
from.Withdraw(amount);
to.Deposit(amount);
}

8.3.2.4 - Advantages of Declarative Programming

Declarative programming offers several benefits:

  • Readability: Code often more closely resembles the problem statement
  • Conciseness: Typically requires fewer lines of code
  • Abstraction: Hides implementation details, focusing on intent
  • Maintainability: Changes to implementation details don't affect the declarative code
  • Parallelization: Easier to parallelize since operations are described, not sequenced

8.3.3 - Combining Paradigms Effectively

Most real-world C# applications combine imperative and declarative approaches, leveraging the strengths of each paradigm.

8.3.3.1 - When to Use Each Paradigm

public class OrderProcessor
{
private readonly List<Order> _orders;

public OrderProcessor(List<Order> orders)
{
_orders = orders ?? throw new ArgumentNullException(nameof(orders));
}

// Declarative approach for data querying
public IEnumerable<Order> GetHighValueOrders(decimal threshold)
{
return _orders
.Where(o => o.TotalAmount > threshold)
.OrderByDescending(o => o.Date);
}

// Imperative approach for complex business logic
public void ProcessOrder(Order order)
{
// Validate order
if (order == null)
throw new ArgumentNullException(nameof(order));

if (order.Items.Count == 0)
throw new InvalidOperationException("Order must have at least one item");

// Calculate totals
decimal subtotal = 0;
foreach (var item in order.Items)
{
subtotal += item.Price * item.Quantity;
}

// Apply discounts
decimal discount = 0;
if (subtotal > 1000)
{
discount = subtotal * 0.1m;
}
else if (subtotal > 500)
{
discount = subtotal * 0.05m;
}

// Calculate tax
decimal taxRate = GetTaxRate(order.ShippingAddress.State);
decimal tax = (subtotal - discount) * taxRate;

// Set order totals
order.Subtotal = subtotal;
order.Discount = discount;
order.Tax = tax;
order.TotalAmount = subtotal - discount + tax;

// Update order status
order.Status = OrderStatus.Processed;
order.ProcessedDate = DateTime.Now;
}

// Hybrid approach: declarative query with imperative processing
public void ApplyBulkDiscount(decimal percentage)
{
// Declarative: Find eligible orders
var eligibleOrders = _orders
.Where(o => o.Status == OrderStatus.Pending && o.TotalAmount > 100);

// Imperative: Process each order
foreach (var order in eligibleOrders)
{
decimal discountAmount = order.TotalAmount * (percentage / 100);
order.Discount += discountAmount;
order.TotalAmount -= discountAmount;
order.Notes += $"Bulk discount of {percentage}% applied on {DateTime.Now}.";
}
}

private decimal GetTaxRate(string state)
{
// Implementation details
return state switch
{
"CA" => 0.0725m,
"NY" => 0.0845m,
"TX" => 0.0625m,
_ => 0.05m
};
}
}

public class Order
{
public int Id { get; set; }
public List<OrderItem> Items { get; set; } = new List<OrderItem>();
public DateTime Date { get; set; }
public Address ShippingAddress { get; set; }
public decimal Subtotal { get; set; }
public decimal Discount { get; set; }
public decimal Tax { get; set; }
public decimal TotalAmount { get; set; }
public OrderStatus Status { get; set; }
public DateTime? ProcessedDate { get; set; }
public string Notes { get; set; } = string.Empty;
}

public class OrderItem
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}

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

public enum OrderStatus
{
Pending,
Processed,
Shipped,
Delivered,
Cancelled
}

8.3.3.2 - Guidelines for Choosing the Right Approach

Here are some guidelines for when to use each paradigm:

Use declarative approaches for:

  • Data querying and transformation
  • Configuration and specification
  • Simple, well-defined operations
  • When readability and conciseness are priorities
  • When the "what" is more important than the "how"

Use imperative approaches for:

  • Complex algorithms with specific optimization needs
  • Low-level operations requiring precise control
  • Performance-critical code sections
  • When the step-by-step process is important
  • When debugging and tracing execution flow is crucial

8.3.3.3 - Refactoring from Imperative to Declarative

Often, imperative code can be refactored to a more declarative style:

// Imperative approach
public static List<Customer> FindHighValueCustomers(List<Customer> customers)
{
List<Customer> result = new List<Customer>();

foreach (var customer in customers)
{
decimal totalPurchases = 0;

foreach (var order in customer.Orders)
{
if (order.Status == OrderStatus.Delivered)
{
totalPurchases += order.TotalAmount;
}
}

if (totalPurchases > 10000)
{
result.Add(customer);
}
}

result.Sort((a, b) => b.Name.CompareTo(a.Name));

return result;
}

// Refactored to declarative approach
public static IEnumerable<Customer> FindHighValueCustomers(IEnumerable<Customer> customers)
{
return customers
.Where(c => c.Orders
.Where(o => o.Status == OrderStatus.Delivered)
.Sum(o => o.TotalAmount) > 10000)
.OrderByDescending(c => c.Name);
}

8.3.3.4 - Balancing Readability and Performance

Sometimes there's a trade-off between declarative elegance and performance:

public class DataProcessor
{
// Purely declarative approach - elegant but may have performance issues
public IEnumerable<ProductSummary> GetProductSummariesDeclarative(IEnumerable<Order> orders)
{
return orders
.Where(o => o.Status == OrderStatus.Delivered)
.SelectMany(o => o.Items)
.GroupBy(i => i.ProductId)
.Select(g => new ProductSummary
{
ProductId = g.Key,
ProductName = g.First().ProductName,
TotalQuantity = g.Sum(i => i.Quantity),
TotalRevenue = g.Sum(i => i.Price * i.Quantity)
})
.OrderByDescending(p => p.TotalRevenue);
}

// Hybrid approach - less elegant but more efficient
public List<ProductSummary> GetProductSummariesHybrid(List<Order> orders)
{
// Use a dictionary for efficient grouping
var productSummaries = new Dictionary<int, ProductSummary>();

// Imperative processing for efficiency
foreach (var order in orders)
{
if (order.Status != OrderStatus.Delivered)
continue;

foreach (var item in order.Items)
{
if (!productSummaries.TryGetValue(item.ProductId, out var summary))
{
summary = new ProductSummary
{
ProductId = item.ProductId,
ProductName = item.ProductName,
TotalQuantity = 0,
TotalRevenue = 0
};
productSummaries[item.ProductId] = summary;
}

summary.TotalQuantity += item.Quantity;
summary.TotalRevenue += item.Price * item.Quantity;
}
}

// Declarative sorting
return productSummaries.Values
.OrderByDescending(p => p.TotalRevenue)
.ToList();
}
}

public class ProductSummary
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int TotalQuantity { get; set; }
public decimal TotalRevenue { get; set; }
}

8.3.3.5 - Domain-Specific Examples

Different domains may favor different paradigms:

// Data access layer - often declarative with Entity Framework
public class CustomerRepository
{
private readonly ApplicationDbContext _context;

public CustomerRepository(ApplicationDbContext context)
{
_context = context;
}

// Declarative query
public async Task<List<Customer>> GetCustomersWithRecentOrdersAsync()
{
return await _context.Customers
.Where(c => c.Orders.Any(o => o.Date >= DateTime.Now.AddMonths(-3)))
.Include(c => c.Orders)
.OrderBy(c => c.Name)
.ToListAsync();
}
}

// Business logic layer - often a mix of paradigms
public class DiscountService
{
// Declarative approach for simple rule
public decimal CalculateDiscountDeclarative(Customer customer)
{
return customer.LoyaltyPoints switch
{
>= 1000 => 0.15m,
>= 500 => 0.10m,
>= 100 => 0.05m,
_ => 0.00m
};
}

// Imperative approach for complex rules
public decimal CalculateDiscountImperative(Customer customer, Order order)
{
decimal discount = 0;

// Loyalty discount
if (customer.LoyaltyPoints >= 1000)
{
discount += 0.10m;
}
else if (customer.LoyaltyPoints >= 500)
{
discount += 0.05m;
}

// Order size discount
if (order.TotalAmount >= 1000)
{
discount += 0.05m;
}

// Seasonal discount
if (DateTime.Now.Month == 12)
{
discount += 0.02m;
}

// First-time customer discount
if (customer.Orders.Count == 1)
{
discount += 0.05m;
}

// Cap the maximum discount
return Math.Min(discount, 0.25m);
}
}

// UI layer - often declarative for UI definition, imperative for event handling
/*
// Declarative UI definition (XAML)
<StackPanel>
<TextBlock Text="{Binding CustomerName}" />
<ListView ItemsSource="{Binding Orders}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Id}" Width="50" />
<TextBlock Text="{Binding Date, StringFormat=d}" Width="100" />
<TextBlock Text="{Binding TotalAmount, StringFormat=C}" Width="100" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Content="Process Orders" Command="{Binding ProcessOrdersCommand}" />
</StackPanel>

// Imperative event handling
private void Button_Click(object sender, RoutedEventArgs e)
{
try
{
// Show loading indicator
loadingIndicator.Visibility = Visibility.Visible;

// Process the order
_orderService.ProcessOrder(currentOrder);

// Update UI
orderStatusText.Text = "Order processed successfully";
processButton.IsEnabled = false;
}
catch (Exception ex)
{
// Handle error
MessageBox.Show($"Error processing order: {ex.Message}");
}
finally
{
// Hide loading indicator
loadingIndicator.Visibility = Visibility.Collapsed;
}
}
*/

Summary

Both imperative and declarative programming have their place in modern C# development. Imperative programming provides direct control over program execution and is well-suited for complex algorithms and performance-critical code. Declarative programming offers conciseness, readability, and a focus on what should be accomplished rather than how.

The most effective C# code often combines both paradigms, using declarative approaches for data querying, configuration, and high-level operations, while employing imperative techniques for complex business logic, optimization, and fine-grained control. By understanding the strengths and weaknesses of each paradigm, you can choose the most appropriate approach for each situation and write more maintainable, efficient code.

Additional Resources