MODUL 1 - LEKCIJA 5

Programerski Principi: SOLID, DRY, KISS i YAGNI

Temelji pisanja čistog, održivog i skalabilnog koda objašnjeni kroz specifične, detaljne primjere. Kako spriječiti pretvaranje vašeg projekta u "špageti kôd".

⏱️ Trajanje: ~60 min 📚 Nivo: Srednji / Napredni 🎯 Koncepti: SOLID, DRY, KISS, YAGNI, SoC

🏛️ Zašto su principi dizajna softvera ključni?

Bilo da pravite jednostavnu web stranicu ili ogroman preduzetnički sistem (kao što su registri korisnika, bankarski softveri ili portali e-Uprave), vaša baza koda će s vremenom rasti. Sistemi moraju raditi besprijekorno godinama, moraju se stalno prilagođavati novim zahtjevima klijenata (ili novim zakonima), a na njima često rade timovi inženjera koji se s vremenom mijenjaju.

Pisanje koda koji trenutačno "samo radi" u ovakvom okruženju je izuzetno opasno. Loše dizajniran kôd brzo postaje nemoguć za održavanje. Kada izmjena jedne funkcionalnosti uzrokuje pucanje tri naizgled nepovezane funkcionalnosti, nalazite se u problemu. Zato poštujemo programerske principe - set smjernica koje su decenijama razvijali najiskusniji softverski inženjeri kako bi osigurali da kôd ostane čitljiv, siguran za izmjene i visoko kvalitetan.

📊 Brzi Pregled Principa

Akronim / Princip Puni Naziv Suština
SOLID (5 principa Objektno Orijentisanog Programiranja) Osnova za objektno orijentisane sisteme: Omogućava lako dodavanje novih funkcionalnosti bez rušenja postojećih. Kreiranje modularnog koda.
DRY Don't Repeat Yourself Izbjegavanje dupliranja logike. Ako se pravilo promijeni, mijenjate ga samo na jednom, centralizovanom mjestu.
KISS Keep It Simple, Stupid Nemojte prekomplikovati sistem. Najjednostavnije rješenje koje ispunjava zahtjeve je uvijek najbolje rješenje.
YAGNI You Aren't Gonna Need It Ne pišite kôd i infrastrukturu za funkcionalnosti koje vam možda nekad u budućnosti zatrebaju. Implementirajte samo ono što vam sada treba.

💎 SOLID Principi (Detaljno objašnjeni)

SOLID je akronim od pet osnovnih principa objektno-orijentisanog dizajna koje je sistematično promovisao Robert C. Martin (poznat kao "Uncle Bob"). Ovi principi pomažu programerima da napišu kôd koji je otporan na promjene i lak za razumijevanje.

S - Single Responsibility Principle (SRP)

Pravilo: Klasa treba da ima samo jedan jedini razlog za promjenu (odnosno, jednu isključivu odgovornost).

Kada klasa radi više stvari odjednom, ona postaje robusna i lomljiva. Zamislimo "Svemoćnu Klasu" koja u isto vrijeme vrši validaciju podataka, ispisuje u bazu podataka i šalje email potvrde korisniku. Ako sutra odlučimo promijeniti provajdera za emailove, moramo otvoriti ovu ogromnu klasu i mijenjati je - čime rizikujemo da slučajno pokvarimo i logiku same validacije ili upisa u bazu!

❌ Loš Primjer - "SuperKlasa" koja radi sve i krši SRP
// Loše: ProcessorZahtjeva klasa je zadužena za VIZUELNU validaciju, BAZU PODATAKA i EMAIL obavještenja!
public class ProcessorGradjevinskihDozvola 
{
    public void ObradiZahtjev(Zahtjev zahtjev, Gradjanin gradjanin) 
    {
        // 1. Razlog za promjenu: Validacija (Šta ako se pravila po zakonu promijene?)
        if(string.IsNullOrEmpty(zahtjev.BrojParcele)) 
            throw new Exception("Broj parcele je obavezan!");
        
        // 2. Razlog za promjenu: Baza podataka (Šta ako promijenimo ORM ili strukturu tabela?)
        using(var db = new AppDbContext())
        {
            db.Zahtjevi.Add(zahtjev);
            db.SaveChanges();
        }
        
        // 3. Razlog za promjenu: Slanje obavijesti (Šta ako pređemo sa SMTP-a na SendGrid API?)
        var smtpClient = new SmtpClient("mail.server.com");
        smtpClient.Send("[email protected]", gradjanin.Email, "Vaš zahtjev je zaprimljen", "...");
    }
}
✅ Dobar Primjer - Razdvojeni servisi, klasa samo koordinira
// Specijalista za Pravnu Validaciju
public class GradjevinskaDozvolaValidator
{
    public bool JeLiZakonito(Zahtjev zahtjev) => !string.IsNullOrEmpty(zahtjev.BrojParcele);
}

// Specijalista za Rad s Bazom
public class ZahtjevRepository
{
    private readonly AppDbContext _db;
    public void Sacuvaj(Zahtjev zahtjev) { /* upis u bazu... */ }
}

// Specijalista za Slanje Emailova
public class EmailServis
{
    public void PosaljiPotvrdu(string email) { /* slanje emaila... */ }
}

// Klasa koja samo koordinira rad (ima samo jedan razlog za promjenu: promjenu samog "toka" procesa)
public class ProcessorGradjevinskihDozvola
{
    private readonly GradjevinskaDozvolaValidator _validator;
    private readonly ZahtjevRepository _repozitorij;
    private readonly EmailServis _emailServis;
    
    // Zavisnosti se ubrizgavaju kroz konstruktor (dependency injection)
    public ProcessorGradjevinskihDozvola(GradjevinskaDozvolaValidator v, ZahtjevRepository r, EmailServis e)
    {
        _validator = v; _repozitorij = r; _emailServis = e;
    }

    public void ObradiZahtjev(Zahtjev zahtjev, Gradjanin gradjanin)
    {
        if(!_validator.JeLiZakonito(zahtjev))
            throw new Exception("Zahtjev nije validan.");
            
        _repozitorij.Sacuvaj(zahtjev);
        _emailServis.PosaljiPotvrdu(gradjanin.Email);
    }
}

Rezultat: Ako sutra uvedemo "SendGrid" za emailove, mijenjamo samo klasu `EmailServis`. Kôd za izdavanje dozvola ostaje netaknut, siguran i 100% testiran.


O - Open/Closed Principle (OCP)

Pravilo: Klase trebaju biti otvorene za proširenje (Open for extension), ali zatvorene za izmjenu (Closed for modification).

Ovo je jedan od najvažnijih principa za rastuće sisteme. Ako morate modificirati postojeću klasu svaki put kada dodajete novu funkcionalnost (nova pravila, nove tipove korisnika), velika je šansa da ćete pokvariti staru funkcionalnost. Rješenje je korištenje polimorfizma (interfejsa i apstraktnih klasa) kako biste samo dodali novu klasu, a staru ostavili nedirnutom.

❌ Loš Primjer - Ogromni IF/ELSE blokovi krše OCP
public class ObracunTakse
{
    public decimal IzracunajTaksu(Zahtjev zahtjev, KategorijaLica kategorija)
    {
        decimal osnovnaTaksa = 50.0m;

        // PROBLEM: Za svaku novu kategoriju koju javna uprava uvede, MORAMO otvarati i prepravljati ovaj fajl!
        if(kategorija.Tip == "Student")
            return osnovnaTaksa * 0.5m; // 50% popusta
        else if(kategorija.Tip == "Penzioner")
            return 0m; // oslobođeni plaćanja
        else if(kategorija.Tip == "UbrzaniPostupak")
            return osnovnaTaksa * 2.0m; // dupla taksa
            
        return osnovnaTaksa;
    }
}
✅ Dobar Primjer - Novi zahtjevi znače samo kreiranje novih klasa
// 1. Zlatno pravilo - Kreiramo zajednički interfejs/apstrakciju
public abstract class PraviloZaTaksu
{
    protected decimal OsnovnaTaksa = 50.0m;
    public abstract decimal IzracunajIznos();
}

// 2. Postojeće kategorije
public class StandardnaTaksa : PraviloZaTaksu {
    public override decimal IzracunajIznos() => OsnovnaTaksa;
}

public class PenzionerTaksa : PraviloZaTaksu {
    public override decimal IzracunajIznos() => 0m; 
}

// 3. Glavna klasa obračuna. Nju VIŠE NIKADA NE DIRAMO! (Closed for modification)
public class ObracunTakse 
{
    public decimal IzracunajTaksu(PraviloZaTaksu pravilo) 
    {
        return pravilo.IzracunajIznos();
    }
}

// 🌟 MAGIJA: Ako se sutra uvede nova "E-Usluga" taksa, mi samo DODAJEMO NOVU KLASU. (Open for extension)
// Kod iznad ostaje 100% netaknut i nije ga potrebno ponovo testirati!
public class EUslugaTaksa : PraviloZaTaksu
{
    public override decimal IzracunajIznos() => OsnovnaTaksa * 0.8m; // 20% popusta
}

L - Liskov Substitution Principle (LSP)

Pravilo: Objekti bazne klase trebaju biti potpuno zamjenjivi objektima njenih naslijeđenih klasa, bez da to izazove pucanje aplikacije ili neočekivano ponašanje.

Ovaj princip nosi ime Barbare Liskov. Može zvučati komplikovano, ali suština je jednostavna: ako klasa B nasljeđuje klasu A, ona se mora ponašati u skladu s onim što korisnik očekuje od klase A. Naslijeđena klasa ne smije baciti neočekivane izuzetke (Exceptions) u metodama koje bi normalno trebale raditi u baznoj klasi, niti smije mijenjati osnovni smisao metoda iz bazne klase.

❌ Loš Primjer - Narušavanje ugovora (LSP) zbog kojeg puca cijeli sistem
// BAZNA KLASA
public class Radnik
{
    public virtual decimal IzracunajBonus()
    {
        return 100m; // Svaki radnik ima pravo na fiksni bonus
    }
}

// NASLIJEĐENA KLASA KOJA PRAVI PROBLEM (Volonter ne prima novac)
public class Volonter : Radnik
{
    public override decimal IzracunajBonus()
    {
        // Ova klasa BACA EXCEPTION za funkciju koja kod običnog radnika vraća broj!
        throw new InvalidOperationException("Volonteri ne primaju bonus i nemaju bankovni račun!");
    }
}

// KLIJENTSKI KOD GDJE SISTEM PUCA!
public class FinansijskiServis
{
    public void PodijeliBonuse(List sviRadnici)
    {
        foreach(var radnik in sviRadnici)
        {
            // Kada petlja dođe do objekta Volonter, funkcija IzracunajBonus bacit će Exception
            // i cijela isplata bonusa za kompaniju će BITI PREKINUTA zbog rušenja programa!
            decimal bonus = radnik.IzracunajBonus(); 
            IsplatiNaRacun(bonus);
        }
    }
}

Zašto je ovo užasno loše? Klasa FinansijskiServis očekuje listu "Radnika". Očekuje da svaki radnik može obaviti metodu IzracunajBonus() bez greške, jer tako stoji u baznoj klasi. Ubacivanjem klase Volonter mi smo slagali "klijenta" i grubo prekršili LSP pravilo. "Ako liči na patku i kvaka kao patka, ali joj trebaju baterije - prekršili ste LSP princip."

✅ Dobar Primjer - Očuvanje arhitektonskog ugovora
// Apstrahujemo na viši nivo: Radnik i OSOBA u kompaniji nisu isto.

// Svi koji rade (bilo plaćeni ili ne)
public abstract class ClanTima
{
    public string Ime { get; set; }
}

// Samo oni sa pravom na platu i bonus implementiraju ovaj Interfejs
public interface IPlacenoLice
{
    decimal IzracunajBonus();
}

// Standardni radnik
public class StalniRadnik : ClanTima, IPlacenoLice
{
    public decimal IzracunajBonus() => 100m;
}

// Volonter - nema "IPlacenoLice", sistem zna da ga ne smije ni pitati za bonus!
public class Volonter : ClanTima
{
    // Nema metode IzracunajBonus. Sistem je siguran.
}

// KLIJENTSKI KOD BEZ GREŠKE
public class FinansijskiServis
{
    // Sada primamo isključivo ona lica koja imaju ugovor o plaćanju
    public void PodijeliBonuse(List placenaLica)
    {
        foreach(var lice in placenaLica)
        {
            // Garantovano neće baciti Exception jer svi u ovoj listi POŠTUJU LSP
            decimal bonus = lice.IzracunajBonus(); 
            IsplatiNaRacun(bonus);
        }
    }
}

I - Interface Segregation Principle (ISP)

Pravilo: Klijente ne treba tjerati da zavise od interfejsa (metoda) koje ne koriste. Umjesto jednog ogromnog, opšteg interfejsa, trebamo imati više malih, specifičnih interfejsa.

Kada imate "Fat Interfejs" (debeli interfejs sa previše funkcija), klase koje ga implementiraju biće prisiljene ispunjavati metode koje nema smisla da ispunjavaju. To opet dovodi do pisanja koda koji baca `NotImplementedException().

❌ Loš Primjer - "Svemoćni" interfejs zbog kojeg klase pate
public interface IMultiFunkcionalniUredjaj
{
    void Printaj(Document d);
    void Skeniraj(Document d);
    void Faksiraj(Document d);
}

// Skupi moderni printer podržava sve - ok!
public class SkupiPrinter : IMultiFunkcionalniUredjaj {
    public void Printaj(Document d) { /* printa */ }
    public void Skeniraj(Document d) { /* skenira */ }
    public void Faksiraj(Document d) { /* faksira */ }
}

// Obični mali printer iz marketa
public class JeftiniPrinter : IMultiFunkcionalniUredjaj {
    public void Printaj(Document d) { /* printa ok! */ }
    
    // PRISILJENI SMO IMPLEMENTIRATI MADA UREĐAJ OVO NE MOŽE!
    public void Skeniraj(Document d) { 
        throw new NotImplementedException("Ne mogu skenirati!"); 
    }
    public void Faksiraj(Document d) { 
        throw new NotImplementedException("Ne mogu faksirati!"); 
    }
}
✅ Dobar Primjer - Razdvojeni mali interfejsi po funkcionalnosti
public interface IPrinter { void Printaj(Document d); }
public interface IScanner { void Skeniraj(Document d); }
public interface IFax { void Faksiraj(Document d); }

// Svaka klasa implementira SAMO ono što zaista zna raditi!
public class JeftiniPrinter : IPrinter 
{
    public void Printaj(Document d) { /* printa */ }
}

public class SkupiPrinter : IPrinter, IScanner, IFax 
{
    public void Printaj(Document d) { /* printa */ }
    public void Skeniraj(Document d) { /* skenira */ }
    public void Faksiraj(Document d) { /* faksira */ }
}

D - Dependency Inversion Principle (DIP)

Pravilo: Moduli visokog nivoa ne bi trebali zavisiti od modula niskog nivoa (konkretnih rješenja). Oba bi trebala zavisiti od apstrakcija (Interfejsa).

Konkretno, klasa koja upravlja vašom poslovnom logikom (`NoviKorisnikServis`) ne bi smjela unutar sebe ručno praviti instancu SQL baze (`new SqlDatabase()`), jer će tako zauvijek ostati čvrsto "zaključana" uz SQL. Ako sutra pređete na MongoDB ili želite napisati Unit Testove bez udaranja u pravu bazu, moraćete prepravljati srž svoje aplikacije.

❌ Loš Primjer - Čvrsto uvezan kod - Tightly Coupled
public class UpraviteljSistemom
{
    // VISOKI SLOJ (Logika) direktno zavisi od NISKOG SLOJA (Konkretna klasa za Logiranje)
    private LocalniFajlLoger _logger = new LocalniFajlLoger(); 
    
    public void ZabiljeziGresku(string poruka)
    {
        // Sutra šef traži da se logovi šalju u Cloud bazu. Mi smo u problemu.
        _logger.ZapisiUFajl("C:\\log.txt", poruka);
    }
}
✅ Dobar Primjer - Zavisnost o Apstrakciji (Dependency Injection)
// 1. Kreiramo Apstrakciju
public interface ILoggerService
{
    void LogInfo(string poruka);
}

// 2. Modul visokog nivoa u potpunosti ovisi isključivo o apstrakciji (NE O KLASAMA)
public class UpraviteljSistemom
{
    private readonly ILoggerService _logger;
    
    // Zatražimo bilo kojeg Logera od Dependency Injection sistema unutar konstruktora
    public UpraviteljSistemom(ILoggerService logger)
    {
        _logger = logger;
    }
    
    public void ZabiljeziGresku(string poruka)
    {
        _logger.LogInfo(poruka); // Sistem je sad potpuno otvoren za promjene!
    }
}

// U C# i ASP.NET Core-u zavisnost se uvezuje u Program.cs fajlu:
// builder.Services.AddScoped();
// ili ako razvijamo lokalno:
// builder.Services.AddScoped();

🧩 Separation of Concerns (SoC) - Odvajanje nadležnosti

Dok su SOLID principi fokusirani na interakciju klasa, SoC se često primjenjuje na arhitekturu cijelog sistema. Sistem podijelimo na različite "slojeve" s posebnim obavezama. U web aplikacijama najpoznatija implementacija SoC arhitekture je poznata i kao N-Tier Arhitektura ili korištenjem MVC paterna sa različitim servisima.

UI / Presentation Layer
(React, Angular, MVC Views, Controllers)
Business Logic / Service Layer
(Izračuni, validacije, implementacija zakona, pravila)
Data Access Layer
(Entity Framework, SQL baze, API Repozitoriji)

Zašto je to važno? Ako odlučite da umjesto postojeće React aplikacije kreirate mobilnu aplikaciju (novi Presentation sloj), vaša Business Logika i Baza podataka ostaju potpuno identični! Nema prepisivanja logike kojom se obračunava popust - vi samo isporučite novi frontend koji komunicira sa istim API-jem u kojem je sva logika smještena.


♻️ DRY (Don't Repeat Yourself) - Pametno pisanje koda

Princip kaže da se ista logika u aplikaciji smije napisati isključivo na jednom mjestu. Kopiranje i lijepljenje koda ("copy-paste" programiranje) je najveći uzrok grešaka (bugova) u industriji.

Zamislite web prodavnicu u kojoj korisnik obračunava PDV (Tax) na korpi prilikom plaćanja. Programer to napiše ovako: var porez = ukupnaCijena * 0.17m;. Sedmicu dana kasnije, druga osoba implementira generisanje PDF računa i na tom ekranu napiše: var iznosPoreza = sumaProizvoda * 0.17m;. Mjesec dana kasnije, Vlada promijeni iznos PDV-a na 19% (0.19m). Junior programer ispravi kôd u korpi na 0.19m ali onaj za PDF račune ostane na 0.17m. Firma izdaje neispravne fakture i upada u ozbiljne finansijske probleme sa zakonom!

💡 Rješenje: Centralizovani izvor istine

Uvijek izolirajte ovakve formule, validacije i bitna pravila u vlastitu klasu, metodu ili konfiguracijski fajl.

public class PdvKalkulator 
{
    // KONFIGURACIJA na JEDNOM mjestu u bazi koda!
    private const decimal PdvStopa = 0.17m; 
    
    public decimal DodajPdv(decimal osnovnaCijena) {
        return osnovnaCijena + (osnovnaCijena * PdvStopa);
    }
}

😘 KISS (Keep It Simple, Stupid) - Jednostavnost ispred mudrovanja

Programeri, posebno kada nauče nove koncepte, imaju tendenciju da prekomplikuju aplikacije. Princip KISS vas podsjeća da je u softverskom inženjeringu čitljiv i prost kôd vrjedniji od "pametnog" i kompliciranog koda. Što je kod kompleksniji, teže ga je održavati i teže je "provaliti" gdje se desila greška.

❌ Loše: Želimo dobiti prvi pozitivni broj iz niza, ali "pametujemo"
// Programer pokazao vještine bitovnog šiftanja, rekurzija i nepotrebnih LINQ funkcija
int GetFirstPositive(int[] numbers) 
{
    return numbers.Where(x => Math.Sign(x) == 1)
                  .DefaultIfEmpty(-1)
                  .Aggregate((a, b) => a > 0 ? a : b); 
}
✅ Dobro: Jednostavan, svima razumljiv i neuporedivo brži kôd (KISS)
int GetFirstPositive(int[] numbers) 
{
    foreach(var num in numbers)
    {
        if(num > 0) return num;
    }
    return -1;
}

🔮 YAGNI (You Aren't Gonna Need It) - Borba protiv predviđanja budućnosti

"Hajde da sada odmah implementiramo podršku za prepoznavanje šarenice oka korisnika, možda će to tražiti klijent sljedeće godine!"

Pravilo YAGNI govori: Ne dodajite kod i funkcionalnosti u aplikaciju sve dok to od vas ne bude eksplicitno zatraženo od strane biznisa ili klijenata. Pravljenje arhitekture za funkcije u budućnosti se zove Over-Engineering (prenaprezanje sistema). Rezultat:

Izvoljno dodajite rješenja tek onda kada klijent kaže: "Sada mi treba prepoznavanje šarenice".