Architektura oprogramowania to sposób organizacji kodu, który pozwala aplikacji dostosowywać się do zmian biznesowych bez bólu i bez zwiększania zespołu programistów.
„Czy ważniejsze jest to, żeby system działał, czy może ważniejsza jest łatwość wprowadzania do niego zmian?” Robert C. Martin, „Czysta Architektura”
W dobie AI, które potrafi szybko generować kod, pytanie to staje się kluczowe. Martin ostrzega: „pierwszy rok pracy nad projektem jest znacznie tańszy niż drugi, a drugi okazuje się o wiele tańszy od trzeciego”. Dobra architektura zapobiega temu problemowi.
Paradygmaty programowania
Architektura powinna być ponadczasowa i niezależna od języka programowania. Jej jakość w dużej mierze wynika ze sposobu myślenia o kodzie, a ten od dekad kształtują podstawowe paradygmaty programowania, które do dziś pozostają aktualne.
Programowanie strukturalne
Programowanie strukturalne to paradygmat programowania, który zakłada, że kod powinien być pisany używając trzech podstawowych struktur kontrolnych:
Sekwencja – instrukcje wykonywane jedna po drugiej
Selekcja – instrukcje warunkowe (if/else, switch)
Iteracja – pętle (for, while)
Czyli to jest kod który można czytać jak tekst: od góry do dołu, z jasnymi rozgałęzieniami i pętlami, każdy język programowania posiada instrukcje if, pętle for itp.
W kontekście architektury programowanie strukturalne uczy nas, że kod powinien być przewidywalny i podzielny. Chodzi o to, by każdą funkcję dało się rozbić na tak małe logiczne kroki, byś mógł z całą pewnością stwierdzić, co dzieje się na każdym etapie.
Choć w teorii dążymy do tego, by kod był 'nieomylny’, w praktyce polegamy na testach. Ważne jest jednak zrozumienie, że testy nie dają nam gwarancji braku błędów one jedynie potwierdzają, że błędy nie wystąpiły w sprawdzonych przez nas scenariuszach. Dlatego właśnie strukturalny podział kodu jest tak ważny: im prostszy i bardziej poukładany jest moduł, tym mniejsza szansa, że przeoczymy w nim błąd.
Czyli kod musimy tworzyć w małych blokach które w łatwy sposób będą poddawały się testom.
Dekompozycja funkcyjna dobrze wpisuje się w programowanie strukturalne. Polega na rozbijaniu kodu na małe, zrozumiałe funkcje, które można analizować i rozwijać niezależnie.
Przykład kodu bez dekompozycji funkcji:
function calculateFinalPrice(price: number, quantity: number, discountCode: string) {
// Wszystko w jednej funkcji
const subtotal = price * quantity;
let discount = 0;
if (discountCode === 'SAVE10') {
discount = subtotal * 0.1;
} else if (discountCode === 'SAVE20') {
discount = subtotal * 0.2;
}
const afterDiscount = subtotal - discount;
const tax = afterDiscount * 0.23;
const total = afterDiscount + tax;
return total;
}Z dekompozycja:
function calculateFinalPrice(price: number, quantity: number, discountCode: string): number {
const subtotal = calculateSubtotal(price, quantity);
const discount = calculateDiscount(subtotal, discountCode);
const afterDiscount = subtotal - discount;
const total = addTax(afterDiscount);
return total;
}
function calculateSubtotal(price: number, quantity: number): number {
return price * quantity;
}
function calculateDiscount(amount: number, code: string): number {
if (code === 'SAVE10') return amount * 0.1;
if (code === 'SAVE20') return amount * 0.2;
return 0;
}
function addTax(amount: number): number {
const TAX_RATE = 0.23;
return amount + (amount * TAX_RATE);
}Programowanie obiektowe
Kolejny paradygmat który jest często wykorzystywany w językach programowania. Programowanie obiektowe to sposób pisania kodu, w którym grupujesz rzeczy (np. „Samochód”) razem z tym, co potrafią robić (np. „jedź”, „zatrzymaj się”). Ten paradygmat wprowadza ważne koncepcje:
Hermetyzacja
Ukrywanie wewnętrznych szczegółów implementacji klasy i udostępnianie tylko tego, co jest potrzebne na zewnątrz. Dane (pola) są prywatne, a dostęp do nich kontrolowany przez publiczne metody (gettery/settery).
Dziedziczenie
Tworzenie nowych klas bazując na istniejących klasach, przejmując ich właściwości i zachowania. Pozwala na reużywalność kodu i budowanie hierarchii klas (np. Dog extends Animal).
Polimorfizm
Ta sama metoda/interfejs może mieć różne implementacje w różnych klasach. Obiekt może być traktowany jako instancja swojej klasy bazowej, ale wykonywać swoją specyficzną wersję metody.
W kontekście architektury ważną praktyką jest odwrócenie zależności (Dependency Inversion), które wykorzystuje polimorfizm. Zamiast pisać kod bezpośrednio zależny od konkretnych technologii (np. MySQL), tworzy się kod oparty na ogólnych interfejsach (np. „baza danych”). Dzięki temu możliwa jest łatwa podmiana implementacji (MySQL → PostgreSQL) bez zmian w głównej logice aplikacji.
BEZ odwrócenia zależności (zła praktyka)
// Konkretna implementacja bazy danych
class MySQLDatabase {
save(user: User) {
console.log('Saving to MySQL:', user);
// kod MySQL...
}
}
// Logika biznesowa BEZPOŚREDNIO zależy od MySQL
class UserService {
private database = new MySQLDatabase(); // ← PROBLEM: twardo wpisany MySQL
registerUser(user: User) {
// jakaś logika biznesowa...
this.database.save(user);
}
}Z odwróceniem zależności (dobra praktyka)
// 1. INTERFEJS (abstrakcja) - mówi "co", nie "jak"
interface Database {
save(user: User): void;
}
// 2. Konkretne implementacje
class MySQLDatabase implements Database {
save(user: User) {
console.log('Saving to MySQL:', user);
}
}
class PostgreSQLDatabase implements Database {
save(user: User) {
console.log('Saving to PostgreSQL:', user);
}
}
class FakeDatabase implements Database {
save(user: User) {
console.log('Fake save for testing:', user);
}
}
// 3. Logika biznesowa zależy od INTERFEJSU, nie konkretnej bazy
class UserService {
constructor(private database: Database) {} // ← dostaje INTERFEJS
registerUser(user: User) {
// jakaś logika biznesowa...
this.database.save(user); // nie wie czy to MySQL, PostgreSQL czy fake
}
}
// 4. Użycie - decydujesz z zewnątrz, co wstrzyknąć
const mysqlDb = new MySQLDatabase();
const userService = new UserService(mysqlDb); // ← dependency injection
// Łatwo zmienić na PostgreSQL:
const postgresDb = new PostgreSQLDatabase();
const userService2 = new UserService(postgresDb);
// Łatwo testować:
const fakeDb = new FakeDatabase();
const userServiceTest = new UserService(fakeDb);Zamiast: „Serwis SAM tworzy konkretną bazę danych”
Robisz: „Serwis mówi 'daj mi COKOLWIEK co umie zapisywać’ i używa tego”
Programowanie funkcyjne
Programowanie funkcyjne to pisanie kodu przy użyciu czystych funkcji, dla tych samych danych zawsze zwracają ten sam wynik i nie zmieniają niczego poza sobą. To daje bezpieczeństwo w wielowątkowości i łatwiejsze debugowanie.
W architekturze kluczowa jest zasada podziału: logika biznesowa powinna być „czysta” (bez efektów ubocznych), a operacje jak zapis do bazy czy wysyłka emaili, świadomie „brudne” (z efektami ubocznymi).
Podsumowanie
Dobra architektura nie polega na wyborze jednego stylu, lecz na świadomym łączeniu różnych podejść, takich jak dekompozycja funkcyjna, odwrócenie zależności czy ograniczanie mutowalnego stanu tam, gdzie to możliwe.
Warto też pamiętać, że opisane paradygmaty nie wyczerpują tematu, a większość współczesnych języków programowania umożliwiają korzystanie z wielu z nich jednocześnie.
W dobie narzędzi opartych o AI, które potrafią generować duże ilości kodu, znajomość paradygmatów i zasad architektonicznych staje się jeszcze ważniejsza. AI nie rozumie kontekstu biznesowego ani długoterminowych konsekwencji projektowych to programista odpowiada za to, aby wygenerowany kod był sensownie zorganizowany i możliwy do rozwoju przez lata.
