Clean&Reactoring Clean Code Created: 09 Feb 2026 Updated: 09 Feb 2026

Understanding the Difference Between OOP and FP in C#

In this chapter, we will be discussing Functional Programming (FP). Because most C# developers are accustomed to Object-Oriented Programming (OOP), the shift to FP can feel rather alien to begin with.

To make this clear, we will move away from simple shapes and look at a real-world scenario: A Shopping Cart. We will implement a simple logic to calculate the total price of items in a cart, first using the traditional OOP approach, and then using the FP approach.

The OOP Approach: Encapsulating State

In OOP, we model the world using "objects" that hold their own data (state) and have methods to modify that data.

Here is a ShoppingCart class. It holds a list of prices internally and has methods to add items and calculate the total.

using System.Collections.Generic;
using System.Linq;

public class ShoppingCart
{
// 1. Internal State: The data is hidden inside the class
private List<decimal> _items;

public ShoppingCart()
{
_items = new List<decimal>();
}

// 2. Behavior: This method Mutates (changes) the internal state
public void AddItem(decimal price)
{
_items.Add(price);
}

// 3. Behavior: This method uses the internal state to produce a result
public decimal CalculateTotal()
{
return _items.Sum();
}
}

class Program
{
static void Main()
{
// We instantiate an object that tracks its own history
ShoppingCart myCart = new ShoppingCart();
// We modify the object's state
myCart.AddItem(10.50m);
myCart.AddItem(5.25m);

// We ask the object to calculate based on its current state
System.Console.WriteLine("Total Price: " + myCart.CalculateTotal());
}
}

The FP Approach: Pure Functions and Data

Now, let’s look at the Functional Programming version. In FP, data and behavior are separate. We don't create a "Cart" object that holds items; we just have a list of items (Data) and a function that calculates the total (Behavior).

using System.Collections.Generic;
using System.Linq;

class Program
{
// 1. Pure Function: Takes input, produces output.
// It does not know about or change any external "state".
static decimal CalculateTotal(List<decimal> items)
{
return items.Sum();
}

static void Main()
{
// 2. Data: The data is simple and transparent (no hidden private fields)
List<decimal> cartItems = new List<decimal> { 10.50m, 5.25m };

// 3. Execution: We pass the data into the function
System.Console.WriteLine("Total Price: " + CalculateTotal(cartItems));
}
}

Explanation of the Differences

To truly understand the shift, let's break down exactly what changed between the two examples.

1. State Handling

  1. OOP (Encapsulation): The ShoppingCart class "owns" the data. The list _items is private. You cannot calculate the total without creating an instance of the class first. The state is coupled with the logic.
  2. FP (Separation): The data (List<decimal>) is separate from the function (CalculateTotal). The function doesn't care where the list came from; it just calculates the result.

2. Mutability vs. Immutability

  1. OOP: OOP relies on change. In the example, we called myCart.AddItem(). This method changed the internal state of myCart. If you checked myCart before and after that line, it would be different.
  2. FP: FP prefers immutability. Although our simple example used a list, a strict FP approach would not "add" an item to an existing list. Instead, it would create a new list containing the old items plus the new one. This ensures that data never changes unexpectedly.

3. The "Pure Function" Concept

  1. In the FP example, CalculateTotal is a Pure Function.
  2. Definition: A function is pure if it always returns the same result for the same input and has no side effects (it doesn't change anything outside itself).
  3. Benefit: Pure functions are incredibly easy to test. You don't need to set up a complex ShoppingCart object with a specific history to test the total; you just pass a list of numbers.

Summary: Which is better?

Neither is strictly "better," but they serve different goals:

  1. OOP is excellent for modeling complex systems where entities (like a User, Window, or GameCharacter) need to manage their own changing state and interactions.
  2. FP is excellent for data processing, concurrent programming, and logic-heavy applications where you want to minimize bugs caused by unexpected state changes.

Most modern C# codebases use a hybrid approach, utilizing OOP for architecture (Dependency Injection, Services) and FP for logic (LINQ, immutable records).

Share this lesson: