Paradygmaty w C# — proceduralny, OOP, funkcyjny

C# to jeden z niewielu języków, w którym możesz napisać ten sam program w trzech zupełnie różnych stylach — i wszystkie się skompilują. To nie historia z podręcznika — to praktyczny wybór, którego dokonujesz codziennie, często nieświadomie. Rozłóżmy jedno zadanie na trzy paradygmaty, żeby zobaczyć różnicę na własne oczy.
Zadanie: policz wartość koszyka z rabatem
Mamy listę produktów w koszyku. Klienci z statusem VIP dostają 10% rabatu na całość. Chcemy policzyć końcową kwotę do zapłaty.
Programowanie proceduralne
Kod to sekwencja kroków wykonywanych jeden po drugim, operujących na współdzielonych danych.
decimal ObliczKoszykProceduralnie(List<decimal> ceny, bool czyVip)
{
decimal suma = 0;
foreach (var cena in ceny)
{
suma += cena;
}
if (czyVip)
{
suma = suma * 0.9m;
}
return suma;
}Prosto, czytelnie, działa. Problem pojawia się, gdy reguł rabatowych przybywa (rabat za ilość, kod promocyjny, rabat sezonowy) — funkcja rośnie, zbierając kolejne if-y, aż staje się trudna do ogarnięcia jednym spojrzeniem.
Programowanie obiektowe
Dane i zachowanie, które na nich operuje, żyją razem w jednym obiekcie.
public class Koszyk
{
private readonly List<decimal> _ceny = new();
public bool CzyVip { get; set; }
public void Dodaj(decimal cena) => _ceny.Add(cena);
public decimal ObliczSume()
{
decimal suma = _ceny.Sum();
return CzyVip ? suma * 0.9m : suma;
}
}
var koszyk = new Koszyk { CzyVip = true };
koszyk.Dodaj(100m);
koszyk.Dodaj(50m);
Console.WriteLine(koszyk.ObliczSume()); // 135Teraz Koszyk hermetyzuje swój stan (_ceny) i logikę (ObliczSume) w jednym miejscu. Nowe reguły rabatowe mogą stać się osobnymi klasami implementującymi wspólny interfejs (dokładnie to pokazuje Strategy Pattern) — zamiast rosnącego if-a, masz zestaw małych, wymiennych klas.
Programowanie funkcjonalne
Dane się nie zmieniają (immutability), a rozwiązanie to złożenie czystych funkcji, które dla tych samych argumentów zawsze zwracają ten sam wynik.
decimal ObliczKoszykLinq(IEnumerable<decimal> ceny, bool czyVip)
{
decimal suma = ceny.Sum();
return czyVip ? suma * 0.9m : suma;
}Żadnej zmiennej, która mutuje się krok po kroku (suma += cena zniknęło na rzecz ceny.Sum()) — dane przepływają przez funkcje, nie są modyfikowane w miejscu. To styl, w którym pisze się większość kodu LINQ — szerzej w LINQ w C#.
C# to język wieloparadygmatowy — i to jest jego siła
Powyższe trzy przykłady to nie trzy różne języki — to jeden i ten sam C#. W praktyce miksujesz style w obrębie tej samej klasy: obiektowa struktura (Koszyk jako klasa) z funkcyjną implementacją metody (_ceny.Sum() zamiast pętli), z proceduralnym fragmentem tam, gdzie kolejność kroków ma znaczenie (np. logowanie). Nie musisz wybierać jednego na całe życie projektu — wybierasz najlepsze narzędzie do konkretnego fragmentu kodu.
public class KoszykHybrydowy // obiektowa struktura
{
private readonly List<decimal> _ceny = new();
public bool CzyVip { get; set; }
public void Dodaj(decimal cena) => _ceny.Add(cena); // proceduralna operacja — kolejność ma znaczenie
public decimal ObliczSume() =>
CzyVip ? _ceny.Sum() * 0.9m : _ceny.Sum(); // funkcyjna implementacja — LINQ, bez mutacji
}Pułapka: dogmatyczne trzymanie się jednego paradygmatu
// ❌ "Wszystko musi być obiektowe" — klasa i interfejs dla trywialnego dodawania VAT
public interface IVatCalculator { decimal Calculate(decimal cena); }
public class VatCalculator : IVatCalculator
{
public decimal Calculate(decimal cena) => cena * 1.23m;
}
var kalkulator = new VatCalculator();
var wynik = kalkulator.Calculate(100m);// ✅ To wystarczy — nie każdy problem potrzebuje pełnej struktury obiektowej
decimal DodajVat(decimal cena) => cena * 1.23m;To samo dotyczy przesady w drugą stronę — jeden długi łańcuch LINQ, który próbuje zrobić wszystko w jednej linijce, bywa trudniejszy do przeczytania niż prosta pętla foreach. Żaden paradygmat nie jest “lepszy” uniwersalnie — dogmatyczne trzymanie się jednego z nich, niezależnie od problemu, to dokładnie ta sama pułapka, przed którą ostrzega KISS: nie komplikuj kodu narzędziem, którego problem nie potrzebuje.
Kiedy który styl wybrać
- Proceduralny — krótkie skrypty, sekwencje kroków, gdzie kolejność wykonania jest istotą problemu (np. konfiguracja aplikacji przy starcie).
- Obiektowy — gdy modelujesz byty z tożsamością i stanem, które zmieniają się w czasie i mają jasno określone zachowanie (encje domenowe, serwisy).
- Funkcjonalny — transformacje danych, przetwarzanie kolekcji, logika bez efektów ubocznych, którą łatwo testować w izolacji (dokładnie to, co LINQ robi najlepiej).
Nowoczesny C# (rekordy, pattern matching, LINQ, wyrażenia lambda) systematycznie dokłada narzędzia funkcyjne do języka, który zaczynał jako czysto obiektowy — nie dlatego, że jeden paradygmat “wygrał”, tylko dlatego, że różne problemy w tym samym systemie zasługują na różne narzędzia.
Podsumowanie
Paradygmat programowania to nie akademicka etykieta — to konkretny sposób organizacji kodu, który wybierasz świadomie, patrząc na naturę problemu. C# pozwala łączyć proceduralne, obiektowe i funkcyjne podejście w jednym pliku, jednej klasie, a nawet jednej metodzie — umiejętność rozpoznania, które podejście pasuje do którego fragmentu kodu, jest bardziej wartościowa niż deklaracja przynależności do jednego “obozu”.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →