Haladó Olvasási idő: ~15 perc

Design elvek

SOLID elvek, kompozíció vs öröklődés és clean code

Design elvek

SOLID elvek, kompozíció vs öröklődés és clean code alapelvek — hogyan gondolkodik egy senior fejlesztő a kód tervezéséről.

1. Definíció

Mi az?

A SOLID elvek öt, Robert C. Martin ("Uncle Bob") által összefoglalt tervezési irányelv, amelyek célja, hogy az objektumorientált kód karbantartható, bővíthető és tesztelhető legyen. A betűszó:

Betű Elv Magyar
S Single Responsibility Principle Egyetlen felelősség elve
O Open/Closed Principle Nyílt/zárt elv
L Liskov Substitution Principle Liskov-féle helyettesítési elv
I Interface Segregation Principle Interfész-szétválasztás elve
D Dependency Inversion Principle Függőség-megfordítás elve

Miért léteznek?

Nagy kódbázisban a rossz tervezés következményei gyorsan megmutatkoznak:

  • Egy kis változtatás kaszkád-hatást okoz — tíz helyen kell módosítani
  • Nehéz unit tesztet írni, mert az osztályok szorosan összefonódnak
  • Új fejlesztők nem értik, hol mit kell megváltoztatni

A SOLID elvek megelőzik ezeket a problémákat.

Hol helyezkednek el?

A tervezési elvek egy tágabb rétegben helyezkednek el:

  • SOLID elvek — osztály- és modulszintű iránymutatás ← ez a téma
  • Design Patterns — újrahasznosítható megoldási sablonok, például Strategy vagy Factory
  • Architecture Patterns — rendszerszintű struktúrák, például Hexagonal vagy Clean Architecture
  • OOP alapelvek — az alapfogalmi háttér; → Lásd: OOP Alapelvek

2. Alapfogalmak

2.1 Single Responsibility Principle (SRP)

„Egy osztálynak csak egy oka legyen a változásra."

Egy osztály csak egy dologért legyen felelős. Ha megváltozik az üzleti logika, az adatbázis réteg, vagy a megjelenítési formátum — ezek ne érintsék egymást.

Rossz példa — egy osztály, három felelősség:

// ❌ Rossz: UserService felelős az üzleti logikáért, adatbázisért ÉS e-mailküldésért
public class UserService {
    public void registerUser(String email, String password) {
        // 1. Validáció (üzleti logika)
        if (!email.contains("@")) throw new IllegalArgumentException("Érvénytelen e-mail");
        // 2. Mentés (adatbázis felelősség)
        database.save(new User(email, hashPassword(password)));
        // 3. E-mail küldés (értesítési felelősség)
        emailClient.send(email, "Üdvözlünk!", "Sikeres regisztráció!");
    }
}

Jó példa — felelősségek szétválasztva:

// ✅ Jó: minden osztály egy dologért felel
public class UserRegistrationService {
    private final UserRepository userRepository;
    private final EmailNotificationService emailService;
    private final PasswordEncoder passwordEncoder;

    public void register(String email, String password) {
        validateEmail(email);
        User user = new User(email, passwordEncoder.encode(password));
        userRepository.save(user);
        emailService.sendWelcomeEmail(email);
    }

    private void validateEmail(String email) {
        if (!email.contains("@")) throw new IllegalArgumentException("Érvénytelen e-mail");
    }
}

2.2 Open/Closed Principle (OCP)

„Egy osztály legyen nyílt a bővítésre, de zárt a módosításra."

Új funkcionalitást hozzáadással valósíts meg, ne a meglévő kód módosításával. Legtöbbször interfészekkel és polimorfizmussal érhető el.

Rossz példa — minden új típushoz módosítani kell az osztályt:

// ❌ Rossz: új fizetési módhoz if-ágat kell hozzáadni
public class PaymentProcessor {
    public void process(Payment payment) {
        if (payment.getType() == PaymentType.CREDIT_CARD) {
            // hitelkártya logika...
        } else if (payment.getType() == PaymentType.PAYPAL) {
            // PayPal logika...
        } else if (payment.getType() == PaymentType.CRYPTO) {
            // kriptó logika... (módosítani kellett az osztályt!)
        }
    }
}

Jó példa — bővítés módosítás nélkül:

// ✅ Jó: új fizetési mód → új osztály, régi kód érintetlen
public interface PaymentStrategy {
    void process(Payment payment);
}

public class CreditCardPayment implements PaymentStrategy {
    @Override public void process(Payment payment) { /* hitelkártya */ }
}

public class PayPalPayment implements PaymentStrategy {
    @Override public void process(Payment payment) { /* PayPal */ }
}

public class CryptoPayment implements PaymentStrategy {
    @Override public void process(Payment payment) { /* kriptó — új osztály, régi kód nem változott */ }
}

public class PaymentProcessor {
    public void process(PaymentStrategy strategy, Payment payment) {
        strategy.process(payment); // soha nem kell módosítani
    }
}

2.3 Liskov Substitution Principle (LSP)

„Ha S az T altípusa, akkor T objektumait fel lehet cserélni S objektumaival a program helyes működésének megváltoztatása nélkül."

Egyszerűbben: a leszármazott osztálynak minden helyen behelyettesíthetőnek kell lennie a szülője helyett, és a viselkedés meglepetés nélkül kell hogy megfeleljen az elvárásoknak.

Klasszikus LSP sértés — négyzet/téglalap probléma:

// ❌ LSP sértés: Square extends Rectangle
public class Rectangle {
    protected int width, height;

    public void setWidth(int width)   { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int area() { return width * height; }
}

public class Square extends Rectangle {
    @Override public void setWidth(int width)  { this.width = this.height = width; }
    @Override public void setHeight(int height){ this.width = this.height = height; }
}

// Ez a kód elromlik, ha Square-t adunk Rectangle helyett:
void testArea(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.area() == 20; // ❌ Square esetén area() = 16, nem 20!
}

Helyes megközelítés:

// ✅ Közös interfész, nem öröklődés
public interface Shape {
    int area();
}

public class Rectangle implements Shape {
    private final int width, height;
    public Rectangle(int width, int height) { this.width = width; this.height = height; }
    @Override public int area() { return width * height; }
}

public class Square implements Shape {
    private final int side;
    public Square(int side) { this.side = side; }
    @Override public int area() { return side * side; }
}

2.4 Interface Segregation Principle (ISP)

„Egy kliens ne legyen kényszerítve olyan metódusokra, amelyeket nem használ."

A vastag interfészek helyett inkább kisebb, célzott interfészeket definiálj.

Rossz példa — „kövér" interfész:

// ❌ Rossz: nem minden Worker tud minden feladatot ellátni
public interface Worker {
    void work();
    void eat();
    void sleep();
}

public class RobotWorker implements Worker {
    @Override public void work()  { /* OK */ }
    @Override public void eat()   { throw new UnsupportedOperationException("Robot nem eszik!"); }
    @Override public void sleep() { throw new UnsupportedOperationException("Robot nem alszik!"); }
}

Jó példa — szétválasztott interfészek:

// ✅ Jó: kis, fókuszált interfészek
public interface Workable  { void work(); }
public interface Eatable   { void eat(); }
public interface Sleepable { void sleep(); }

public class HumanWorker implements Workable, Eatable, Sleepable {
    @Override public void work()  { /* dolgozik */ }
    @Override public void eat()   { /* eszik */ }
    @Override public void sleep() { /* alszik */ }
}

public class RobotWorker implements Workable {
    @Override public void work() { /* dolgozik */ }
    // Robot nem implementál Eatable-t vagy Sleepable-t
}

2.5 Dependency Inversion Principle (DIP)

„A magas szintű modulok ne függjenek az alacsony szintű moduloktól. Mindkettő absztrakcióktól függjön."

Ez az elv az alapja a Dependency Injectionnek (DI) és az IoC (Inversion of Control) konténereknek (pl. Spring).

Rossz példa — közvetlen függőség:

// ❌ Rossz: OrderService pontosan tudja, melyik osztályt használja
public class OrderService {
    private final MySQLOrderRepository repository = new MySQLOrderRepository(); // szoros csatolás!

    public void placeOrder(Order order) {
        repository.save(order);
    }
}

Jó példa — függőség megfordítása interfészen keresztül:

// ✅ Jó: OrderService csak az absztrakciótól függ
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(String id);
}

public class MySQLOrderRepository implements OrderRepository { /* MySQL implementáció */ }
public class InMemoryOrderRepository implements OrderRepository { /* teszteléshez */ }

public class OrderService {
    private final OrderRepository repository; // interfészt lát, nem implementációt

    public OrderService(OrderRepository repository) { // konstruktor DI
        this.repository = repository;
    }

    public void placeOrder(Order order) {
        repository.save(order);
    }
}

2.6 Kompozíció vs Öröklődés

„Részesítsd előnyben a kompozíciót az öröklődéssel szemben" — Gang of Four

Szempont Öröklődés Kompozíció
Csatolás Szoros (tight coupling) Laza (loose coupling)
Rugalmasság Merev, fordítási időn rögzített Futásidőben cserélhető
Kód újrafelhasználás IS-A kapcsolaton át HAS-A kapcsolaton át
Tesztelhetőség Nehezebb (szülő is fut) Könnyebb (mock-olható)
Mikor jó? Valódi IS-A reláció esetén Majdnem mindig

3. Gyakorlati használat

Mikor alkalmazzuk?

  • SRP: ha egy osztályod tesztelésekor azt veszed észre, hogy a test setup 50 soros és sok mindent kell mock-olni — ez SRP sértés jele
  • OCP: ha egy meglévő osztályhoz új if-ágat kell hozzáadni egy új típus miatt — OCP sértés
  • LSP: mielőtt öröklődést használsz, kérdezd meg: „Mindig igaz, hogy a leszármazott IS-A szülő?"
  • ISP: ha egy interfész implementálásakor UnsupportedOperationException-t dobsz — ISP sértés
  • DIP: ha unit tesztet írsz és nem tudod mock-olni a függőségeket — DIP sértés

Mikor NEM kell erőltetni?

  • Kis, egyszerű kódbázisokon túlzott komplexitást okozhat
  • Prototípusoknál, PoC-knál a szigorú betartás időpazarlás lehet
  • Az elvek iránymutatások, nem dogmák — okosan kell alkalmazni őket

4. Kód példák

Teljes példa: Értesítési rendszer refaktorálása

Kiinduló állapot (minden elvet sérti):

// ❌ Isten-osztály: mindent tud, mindent csinál
public class NotificationManager {
    public void notify(User user, String type) {
        if (type.equals("EMAIL")) {
            // Közvetlen SMTP kapcsolódás
            Properties props = new Properties();
            props.put("mail.smtp.host", "smtp.gmail.com");
            Session session = Session.getDefaultInstance(props);
            // ... 30 sor e-mail küldés ...
            // ... logolás ...
            // ... adatbázisba mentés ...
        } else if (type.equals("SMS")) {
            // SMS API közvetlen hívás
            // ... 20 sor SMS küldés ...
        }
        // ❌ SRP: 3+ felelősség egy helyen
        // ❌ OCP: új típushoz módosítani kell
        // ❌ DIP: konkrét implementációktól függ
    }
}

Refaktorált, SOLID verzió:

// ✅ SOLID: minden elvnek megfelel

// DIP: absztrakció a függőségekre
public interface NotificationChannel {
    void send(User user, String message);
    boolean supports(NotificationType type);
}

// OCP: új csatorna → új osztály, meglévő kód nem változik
public class EmailNotificationChannel implements NotificationChannel {
    private final SmtpClient smtpClient; // DIP: konstruktoron át injektálva

    public EmailNotificationChannel(SmtpClient smtpClient) {
        this.smtpClient = smtpClient;
    }

    @Override
    public void send(User user, String message) {
        smtpClient.sendEmail(user.getEmail(), message);
    }

    @Override
    public boolean supports(NotificationType type) {
        return type == NotificationType.EMAIL;
    }
}

public class SmsNotificationChannel implements NotificationChannel {
    private final SmsGateway smsGateway;

    public SmsNotificationChannel(SmsGateway smsGateway) {
        this.smsGateway = smsGateway;
    }

    @Override
    public void send(User user, String message) {
        smsGateway.sendSms(user.getPhoneNumber(), message);
    }

    @Override
    public boolean supports(NotificationType type) {
        return type == NotificationType.SMS;
    }
}

// SRP: NotificationService csak koordinál
public class NotificationService {
    private final List<NotificationChannel> channels;
    private final NotificationAuditRepository auditRepository; // SRP: audit külön

    public NotificationService(List<NotificationChannel> channels,
                                NotificationAuditRepository auditRepository) {
        this.channels = channels;
        this.auditRepository = auditRepository;
    }

    public void notify(User user, NotificationType type, String message) {
        channels.stream()
            .filter(channel -> channel.supports(type))
            .findFirst()
            .orElseThrow(() -> new UnsupportedOperationException("Nem támogatott: " + type))
            .send(user, message);

        auditRepository.record(user.getId(), type, message);
    }
}

Kompozíció példa — Stack megvalósítása

// ❌ Rossz: Stack extends ArrayList — LSP sértés!
// A Stack IS-A ArrayList? Nem igazán — add(index, element) szemantikailag hibás Stack esetén.
public class BadStack<T> extends ArrayList<T> {
    public void push(T element) { add(element); }
    public T pop() { return remove(size() - 1); }
    // De örököltük: add(int index, T element), set(), remove(int index) — ezek nem kellenek!
}

// ✅ Jó: Stack HAS-A List — kompozíció
public class GoodStack<T> {
    private final Deque<T> storage = new ArrayDeque<>(); // implementáció rejtett

    public void push(T element) { storage.push(element); }
    public T pop() { return storage.pop(); }
    public T peek() { return storage.peek(); }
    public boolean isEmpty() { return storage.isEmpty(); }
    public int size() { return storage.size(); }
    // Csak a Stack-hez szükséges műveletek vannak kitéve — ISP!
}

5. Trade-offok

Szempont Naiv kód (SOLID nélkül) SOLID-kompatibilis kód
Teljesítmény Minimális overhead Kis overhead (interfész dispatch, DI) — ritkán mérhető
💾 Memória Kevesebb osztály, kevesebb objektum Több kis osztály — de elhanyagolható
🔧 Karbantarthatóság Gyorsan romlik, nehéz bővíteni Jól strukturált, könnyű bővíteni
🔄 Rugalmasság Merev, szoros csatolás Laza csatolás, könnyen cserélhető részek
🧪 Tesztelhetőség Nehéz mock-olni Interfészek → egyszerű mock/stub
📚 Tanulási görbe Nincs — bárki ír Elvek megértése időt igényel
🏗️ Boilerplate Kevés fájl/osztály Több fájl, interfész — de szándékosan

Fő meglátás: A SOLID elvek rövid távon több munkát igényelnek, de hosszú távon drámaian csökkentik a karbantartási és bővítési költséget.


6. Gyakori hibák

1. SRP: „Isten-osztály" (God Class)

Mi a hiba? Egy osztály minden felelősséget magába szív — adatbázis, üzleti logika, validáció, formázás.

// ❌ UserService 500 soros, 15+ függőséggel
public class UserService {
    private UserRepository userRepo;
    private OrderRepository orderRepo;
    private EmailService emailService;
    private SmsService smsService;
    private PaymentService paymentService;
    private AuditLogger auditLogger;
    private CacheManager cacheManager;
    // ...
    // Jel: ha a konstruktornak 7+ paramétere van, SRP sérül!
}

Megoldás: Bontsd szét kisebb, célzott szolgáltatásokra. Ha a mock-setup teszteléskor hosszabb, mint a tényleges teszt — ez SRP sértés jele.


2. OCP: Típus-alapú if/switch lánc

Mi a hiba? Minden új típusnál meg kell nyitni és módosítani a meglévő kódot.

// ❌ Minden új shape-hez módosítani kell
public double calculateArea(Shape shape) {
    if (shape instanceof Circle c)    return Math.PI * c.getRadius() * c.getRadius();
    if (shape instanceof Rectangle r) return r.getWidth() * r.getHeight();
    if (shape instanceof Triangle t)  return 0.5 * t.getBase() * t.getHeight();
    // ← új shape → új sor ITT, de ezt a metódust módosítani kellett!
    throw new UnsupportedOperationException("Ismeretlen shape");
}

// ✅ Megoldás: polimorfizmus
public interface Shape { double area(); }
// Minden implementáció tudja a saját területszámítását

3. LSP: Kivétel dobása örökölt metódusban

Mi a hiba? A leszármazott nem tudja teljesíteni a szülő kontraktusát.

// ❌ ReadOnlyList extends ArrayList — de mit csinálunk az add() metódussal?
public class ReadOnlyList<T> extends ArrayList<T> {
    @Override
    public boolean add(T element) {
        throw new UnsupportedOperationException("Read-only!"); // LSP sértés!
    }
}

// ✅ Megoldás: ne örökölj, implementálj List interfészt és delegálj,
// vagy használj Collections.unmodifiableList()

4. ISP figyelmen kívül hagyása „kényelemből"

Mi a hiba? Egy nagy interfészt írunk, mert „biztos kell majd minden metódus valakinek".

// ❌ Vastag interfész
public interface UserRepository {
    User findById(Long id);
    List<User> findAll();
    void save(User user);
    void delete(Long id);
    List<User> findByEmailDomain(String domain);     // ritkán kell
    Map<String, Long> getUserCountByCountry();       // ritkán kell
    List<User> findInactiveUsersOlderThan(int days); // ritkán kell
}

// ✅ Szétválasztott interfészek
public interface UserReadRepository  { User findById(Long id); List<User> findAll(); }
public interface UserWriteRepository { void save(User user); void delete(Long id); }
public interface UserReportRepository {
    Map<String, Long> getUserCountByCountry();
    List<User> findInactiveUsersOlderThan(int days);
}

5. DIP: `new` operátor üzleti logikában

Mi a hiba? Ha egy osztály new-val hoz létre függőséget, az tesztelhetetlen és merev.

// ❌ new a konstruktorban — nem mock-olható tesztben
public class ReportService {
    private final DatabaseReporter reporter = new DatabaseReporter(); // szoros csatolás!
}

// ✅ DI konstruktoron át
public class ReportService {
    private final Reporter reporter;
    public ReportService(Reporter reporter) { this.reporter = reporter; }
}

7. Senior-szintű meglátások

„Mikor sérted meg szándékosan a SOLID-ot?"

Senior interjúkérdés. Az elvek iránymutatások, nem törvények. Helyes válasz:

  • Kis utility osztályoknál: StringUtils, DateUtils — az SRP erőltetése túlzott lenne
  • Value objecteknél: egy Money vagy Address record-ban nincs szükség interfészre
  • Performanszkritkus kódban: a DI overhead mérhetően magas lehet (pl. real-time streaming)
  • Tesztkódban: a teszt-kód lehet pragmatikusabb, a produkciós kód viszont tartsa be az elveket

Kompozíció vs öröklődés döntési fa

Kompozíció vs öröklődés ellenőrzőlista:

  1. Ha minden körülmények között valódi IS-A reláció áll fenn, az öröklődés szóba jöhet — de az LSP-t ellenőrizd.
  2. Ha nem valódi IS-A kapcsolat, inkább kompozíciót használj.
  3. Ha a leszármazott miatt módosítanod kell a szülőt, az öröklődés rossz irány.
  4. Ha csak azért írsz felül metódust, hogy UnsupportedOperationException-t dobj, azonnal válts kompozícióra.

A Dependency Injection mélysége

  • Constructor injection — ajánlott (immutable, tesztelhető, kötelező függőségek)
  • Setter injection — opcionális függőségekre
  • Field injection (@Autowired közvetlenül a mezőn) — kerülendő (nem tesztelhető DI konténer nélkül!)
// ❌ Field injection — kerülendő
@Service
public class OrderService {
    @Autowired private OrderRepository repository; // nem tesztelhető könnyen
}

// ✅ Constructor injection — ajánlott
@Service
public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

Clean Code kapcsolódó elvek

  • DRY (Don't Repeat Yourself) — de ne over-abstract-olj: a rossz absztrakció rosszabb, mint a duplikáció
  • YAGNI (You Aren't Gonna Need It) — ne bővíts előre, amire ma nincs szükség
  • KISS (Keep It Simple, Stupid) — az egyszerű kód értékesebb a „okos" kódnál
  • Law of Demeterorder.getCustomer().getAddress().getCity() → rossz, order.getCustomerCity() → jobb

Interjún hogyan kommunikálj?

Ne csak definiáld az elveket — mutasd be, hogy mikor és miért alkalmazod őket! Pl.:

„Az SRP-t mindig ellenőrzöm a tesztelés során: ha a mock-setup nehéz, az osztálynak valószínűleg több felelőssége van. Ilyenkor kisebb egységekre bontom."


8. Szószedet

Fogalom Definíció
SOLID 5 tervezési elv: SRP, OCP, LSP, ISP, DIP — Robert C. Martin nyomán
Tight coupling Szoros csatolás: egy osztály pontosan tudja, melyik másik osztályt használja
Loose coupling Laza csatolás: az osztályok interfészeken keresztül kommunikálnak
Dependency Injection (DI) Függőségek kívülről való megadása (konstruktoron, setteres, stb.)
IoC Inversion of Control — a vezérlési folyamat megfordítása (pl. Spring konténer kezeli a DI-t)
Composition HAS-A kapcsolat: egy osztály tartalmaz más objektumokat delegált logikával
God Class Anti-pattern: egy osztály túl sok felelősséget lát el
Strategy Pattern Design pattern, amely az OCP megvalósításának egyik módja
DRY Don't Repeat Yourself — az ismétlést kerüld el
YAGNI You Aren't Gonna Need It — ne implementálj előre nem kért funkcionalitást

9. Gyorsreferencia

  • 🅢 SRP — egy osztály = egy felelősség; ha a test setup komplex → SRP sértés
  • 🅞 OCP — új funkció = új osztály, nem módosítás; polimorfizmus/stratégia minta
  • 🅛 LSP — a leszármazott mindig helyettesítheti a szülőt; UnsupportedOperationException = LSP sértés
  • 🅘 ISP — kis interfészek > nagy interfész; kliensnek csak azt add, amit használ
  • 🅓 DIP — magas szint függ absztrakciótól, nem konkréttól; new az üzleti logikában = DIP sértés
  • 🔗 Kompozíció > Öröklődés — HAS-A általában flexibilisebb, mint IS-A
  • 💉 Constructor injection a DI arany-szabványa — immutable, tesztelhető
  • God Class — ha >7 függőség → SRP sérül, bontsd szét
  • 🧪 Tesztelhetőség a legjobb SOLID-metrika — ha nehéz tesztelni, az elvek sérülnek
  • 🎯 SOLID = iránymutatás, nem dogma — értsd meg, mikor alkalmazd és mikor nem

🎮 Játékok

8 kérdés