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
MoneyvagyAddressrecord-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:
- 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.
- Ha nem valódi IS-A kapcsolat, inkább kompozíciót használj.
- Ha a leszármazott miatt módosítanod kell a szülőt, az öröklődés rossz irány.
- 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 (
@Autowiredkö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 Demeter —
order.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;
newaz ü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