KezdőOlvasási idő: ~35 perc

Alapelvek

Egységbezárás, öröklődés, polimorfizmus és absztrakció

Az objektumorientált programozás négy alappillére: encapsulation, inheritance, polymorphism és abstraction — valós üzleti domain példákon keresztül.


1. Definíció

Mi az OOP?

Az Object-Oriented Programming (OOP) egy programozási paradigma, amely az adatokat és a rajtuk végzett műveleteket objektumokba szervezi. A Java nyelv tervezésének alapja — a java.lang.Object minden osztály őse.

Miért létezik?

A procedurális megközelítés nagy kódbázisoknál gyorsan kezelhetetlenné válik: az adatstruktúrák és a hozzájuk tartozó logika szétszóródik. Az OOP ezt a problémát oldja meg:

  • Modularitás — egymástól független egységek, amelyek önállóan fejleszthetők
  • Újrafelhasználhatóság — közös viselkedés kiemelése, nem copy-paste
  • Karbantarthatóság — a változtatás hatóköre jól behatárolható
  • Modellezés — az üzleti domain természetes leképezése kódra

Hol helyezkedik el?

Az OOP a fő programozási paradigmák egyike:

  • Procedurális — például C
  • Objektumorientált — Java, C#, Kotlin ← ez a téma
  • Funkcionális — Haskell, Scala
  • Hibrid — Java 8+ és Kotlin, ahol OOP és FP együtt jelenik meg

A modern Java (8+) ötvözi az OOP-t és a funkcionális elemeket: lambda expressions, Stream API, Optional — de az alapja továbbra is objektumorientált.


2. Alapfogalmak

2.1 Encapsulation (Egységbezárás)

Az encapsulation az adatok és a hozzájuk tartozó műveletek egy egységbe zárása, valamint az adatok közvetlen hozzáférésének korlátozása.

Access modifiers

Modifier Osztályon belül Package Leszármazott Mindenhol
private
(default)
protected
public

Miért fontos?

  • Invariánsok védelme — az objektum belső állapota mindig konzisztens marad
  • Változáskezelés — a belső implementáció cserélhető anélkül, hogy a klienskód változna
  • API felület minimalizálás — csak azt tesszük publikussá, amit tényleg használni kell
// Egy fizetési tranzakció encapsulált modellje
public class PaymentTransaction {

    // Belső állapot — kívülről nem módosítható közvetlenül
    private final String transactionId;
    private BigDecimal amount;
    private Currency currency;
    private TransactionStatus status;

    public PaymentTransaction(String transactionId, BigDecimal amount, Currency currency) {
        // Invariáns: összeg nem lehet negatív
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Az összeg pozitív kell legyen: " + amount);
        }
        this.transactionId = transactionId;
        this.amount = amount;
        this.currency = currency;
        this.status = TransactionStatus.PENDING;
    }

    // Állapotváltás üzleti szabályokkal védve
    public void approve() {
        if (this.status != TransactionStatus.PENDING) {
            throw new IllegalStateException(
                "Csak PENDING tranzakció hagyható jóvá, jelenlegi: " + this.status
            );
        }
        this.status = TransactionStatus.APPROVED;
    }

    // Getter — az összeg olvasható, de nem írható felül közvetlenül
    public BigDecimal getAmount() {
        return amount;
    }

    // Nincs setAmount()! Az összeg módosítás üzleti műveleten keresztül történik.
    public void applyDiscount(BigDecimal percentage) {
        if (this.status != TransactionStatus.PENDING) {
            throw new IllegalStateException("Kedvezmény csak PENDING státuszban adható");
        }
        this.amount = this.amount.multiply(BigDecimal.ONE.subtract(percentage));
    }
}

Lényeg: Ne csak getter/setter-eket írj — gondolkodj azon, hogy milyen üzleti műveleteken keresztül érhető el az állapotváltozás.


2.2 Inheritance (Öröklődés)

Az inheritance lehetővé teszi, hogy egy osztály átvegye egy másik osztály mezőit és metódusait. Java-ban extends kulcsszóval jelöljük — egyszeres öröklődés van (egy osztálynak max egy szülőosztálya lehet).

IS-A reláció

Az öröklődés IS-A (egy fajta) kapcsolatot fejez ki:

Példák IS-A kapcsolatra:

  • EmailNotificationSender egy NotificationSender
  • SmsNotificationSender egy NotificationSender
  • PushNotificationSender egy NotificationSender

Method overriding

A leszármazott felülírhatja a szülő metódusait, ha:

  • Ugyanaz a metódus-szignatúra (név + paraméterek)
  • A visszatérési típus azonos vagy szűkebb (covariant return)
  • Az access modifier azonos vagy tágabb (nem szűkíthető)
  • A @Override annotáció nem kötelező, de erősen ajánlott — fordítási hibát kapsz, ha elírod a nevet
// Közös logika a szülőosztályban
public abstract class NotificationSender {

    private final AuditLogger auditLogger;

    protected NotificationSender(AuditLogger auditLogger) {
        this.auditLogger = auditLogger;
    }

    // Template Method pattern — a közös flow itt van, a specifikus részt a leszármazott adja
    public final void send(Notification notification) {
        validate(notification);
        doSend(notification);                     // absztrakt — leszármazott implementálja
        auditLogger.log(notification, channel()); // közös audit log
    }

    protected void validate(Notification notification) {
        if (notification.getRecipient() == null || notification.getRecipient().isBlank()) {
            throw new IllegalArgumentException("A címzett nem lehet üres");
        }
    }

    // A leszármazottak ezt implementálják
    protected abstract void doSend(Notification notification);
    protected abstract String channel();
}

public class EmailNotificationSender extends NotificationSender {

    private final EmailGateway emailGateway;

    public EmailNotificationSender(AuditLogger auditLogger, EmailGateway emailGateway) {
        super(auditLogger);
        this.emailGateway = emailGateway;
    }

    @Override
    protected void doSend(Notification notification) {
        emailGateway.sendEmail(
            notification.getRecipient(),
            notification.getSubject(),
            notification.getBody()
        );
    }

    @Override
    protected String channel() {
        return "EMAIL";
    }
}

Figyelem: Az öröklődés szoros csatolást (tight coupling) hoz létre a szülő és a leszármazott között. Használd megfontoltan — → lásd: 7. Mélymerülés.


2.3 Polymorphism (Polimorfizmus)

A polimorfizmus azt jelenti, hogy ugyanaz az interfész különböző implementációkat takarhat. Java-ban két formája van:

Compile-time (statikus) polimorfizmus — Method Overloading

A fordító a paraméterek típusa és száma alapján dönti el, melyik metódus hívódik meg:

public class RiskScoreCalculator {

    // Alapértelmezett kockázat számítás
    public RiskScore calculate(Transaction transaction) {
        return evaluate(transaction, RiskProfile.DEFAULT);
    }

    // Profil-alapú kockázat számítás — overloaded verzió
    public RiskScore calculate(Transaction transaction, RiskProfile profile) {
        return evaluate(transaction, profile);
    }

    // Batch kockázat számítás — újabb overload
    public List<RiskScore> calculate(List<Transaction> transactions) {
        return transactions.stream()
            .map(this::calculate)
            .toList();
    }

    private RiskScore evaluate(Transaction tx, RiskProfile profile) {
        // ... kockázat-értékelési logika
        return new RiskScore(tx.getId(), computeScore(tx, profile));
    }
}

Runtime (dinamikus) polimorfizmus — Method Overriding

A JVM futásidőben dönti el, hogy melyik implementáció hívódik meg (virtual method dispatch):

// Interfész — a szerződés
public interface PaymentProcessor {
    PaymentResult process(PaymentRequest request);
    boolean supports(PaymentMethod method);
}

// Implementáció: kártyás fizetés
public class CardPaymentProcessor implements PaymentProcessor {

    @Override
    public PaymentResult process(PaymentRequest request) {
        // Kártyaadatok validálása, tokenizálás, acquirer hívás
        CardDetails card = request.getCardDetails();
        AuthResponse auth = acquirerGateway.authorize(card, request.getAmount());
        return new PaymentResult(auth.isSuccessful(), auth.getAuthCode());
    }

    @Override
    public boolean supports(PaymentMethod method) {
        return method == PaymentMethod.CREDIT_CARD || method == PaymentMethod.DEBIT_CARD;
    }
}

// Implementáció: banki átutalás
public class BankTransferProcessor implements PaymentProcessor {

    @Override
    public PaymentResult process(PaymentRequest request) {
        // IBAN validálás, SEPA/ACH hívás
        BankAccount account = request.getBankAccount();
        TransferResult result = bankingGateway.initiateTransfer(account, request.getAmount());
        return new PaymentResult(result.isAccepted(), result.getReferenceId());
    }

    @Override
    public boolean supports(PaymentMethod method) {
        return method == PaymentMethod.BANK_TRANSFER;
    }
}

// Használat — a hívó kód nem tudja (és nem is érdekli), melyik implementáció fut
public class PaymentService {

    private final List<PaymentProcessor> processors;

    public PaymentService(List<PaymentProcessor> processors) {
        this.processors = processors;
    }

    public PaymentResult processPayment(PaymentRequest request) {
        // Futásidőben dől el, melyik processor fut
        return processors.stream()
            .filter(p -> p.supports(request.getMethod()))
            .findFirst()
            .orElseThrow(() -> new UnsupportedPaymentMethodException(request.getMethod()))
            .process(request);
    }
}

2.4 Abstraction (Absztrakció)

Az abstraction a lényeges részletek kiemelése és a nem lényeges részletek elrejtése. Java-ban két eszközzel valósítjuk meg:

Abstract class vs Interface

Szempont abstract class interface
Példányosítás ❌ Nem ❌ Nem
Konstruktor ✅ Van ❌ Nincs
Állapot (mezők) ✅ Instance fields ⚠️ Csak static final
Többszörös öröklés ❌ Egy szülő ✅ Több interface
default metódus ✅ Java 8+
Mikor használd Közös állapot + részleges implementáció Szerződés, capability

Mikor melyiket?

  • Abstract class → ha van közös állapot vagy közös logika, amit a leszármazottak megosztanak (pl. NotificationSender fent — van közös auditLogger)
  • Interface → ha viselkedési szerződést adsz meg, amit bármilyen osztály megvalósíthat (pl. PaymentProcessor — kártya és banki átutalás teljesen különböző osztályok)
// Interface — tisztán viselkedési szerződés
public interface RiskEvaluator {
    RiskLevel evaluate(Transaction transaction);
    String evaluatorName();
}

// Abstract class — közös logika és állapot
public abstract class AbstractRiskEvaluator implements RiskEvaluator {

    private final RiskThresholdConfig config;
    private final MetricsCollector metrics;

    protected AbstractRiskEvaluator(RiskThresholdConfig config, MetricsCollector metrics) {
        this.config = config;
        this.metrics = metrics;
    }

    @Override
    public final RiskLevel evaluate(Transaction transaction) {
        long start = System.nanoTime();
        try {
            double score = computeScore(transaction);   // leszármazott logikája
            return mapToLevel(score);                    // közös mapping
        } finally {
            metrics.record(evaluatorName(), System.nanoTime() - start);
        }
    }

    // Leszármazottak saját scoring logikát adnak
    protected abstract double computeScore(Transaction transaction);

    // Közös mapping — a score-ból RiskLevel-t csinálunk a config alapján
    private RiskLevel mapToLevel(double score) {
        if (score >= config.getHighThreshold()) return RiskLevel.HIGH;
        if (score >= config.getMediumThreshold()) return RiskLevel.MEDIUM;
        return RiskLevel.LOW;
    }

    protected RiskThresholdConfig getConfig() {
        return config;
    }
}

// Konkrét implementáció — sebességalapú kockázat
public class VelocityRiskEvaluator extends AbstractRiskEvaluator {

    private final TransactionHistoryRepository historyRepo;

    public VelocityRiskEvaluator(RiskThresholdConfig config, MetricsCollector metrics,
                                  TransactionHistoryRepository historyRepo) {
        super(config, metrics);
        this.historyRepo = historyRepo;
    }

    @Override
    protected double computeScore(Transaction transaction) {
        // Az elmúlt 24 óra tranzakcióinak száma alapján
        int recentCount = historyRepo.countRecent(transaction.getUserId(), Duration.ofHours(24));
        int limit = getConfig().getVelocityLimit();
        return Math.min((double) recentCount / limit, 1.0);
    }

    @Override
    public String evaluatorName() {
        return "VELOCITY";
    }
}

3. Gyakorlati használat

Mikor használd

Pillér Használati eset
Encapsulation Mindig, amikor üzleti invariánsokat kell védeni (pl. tranzakció összege ne lehessen negatív)
Inheritance Template Method pattern, közös állapot + részleges implementáció, framework hook-ok
Polymorphism Strategy pattern, plugin-architektúra, tesztelhetőség (mock-ok)
Abstraction API tervezés, modul-határok definiálása, dependency inversion

Mikor NE használd

Anti-pattern Probléma
Getter/Setter mindenhol Az encapsulation illúzióját kelti — az objektum valójában egy „nyílt adat-struktúra"
Mély öröklődési hierarchia (3+ szint) Fragile Base Class problem — a szülő változtatása megtöri a leszármazottakat
Marker interface állapottal Ha állapotot akarsz megosztani, az interface nem a megfelelő eszköz
Polimorfizmus egyetlen implementációval Over-engineering — ha tudom, hogy csak egyféle payment processor lesz, ne csináljak belőle interface-t
Abstract class egy metódussal, állapot nélkül Használj interface-t helyette — egyszerűbb és rugalmasabb

4. Kód példák

4.1 Encapsulation — Immutable value object

/**
 * Immutable pénzösszeg — teljesen encapsulált.
 * Egyszer létrehozva nem módosítható, szálbiztos.
 */
public final class Money {

    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        Objects.requireNonNull(amount, "Az összeg nem lehet null");
        Objects.requireNonNull(currency, "A pénznem nem lehet null");
        // Két tizedesjegy, bankári kerekítéssel
        this.amount = amount.setScale(2, RoundingMode.HALF_EVEN);
        this.currency = currency;
    }

    // Üzleti művelet — új objektumot ad vissza (immutable)
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException(this.currency, other.currency);
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money multiply(int quantity) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
    }

    public boolean isGreaterThan(Money other) {
        ensureSameCurrency(other);
        return this.amount.compareTo(other.amount) > 0;
    }

    private void ensureSameCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException(this.currency, other.currency);
        }
    }

    // Csak olvasás — nincs setter
    public BigDecimal getAmount() { return amount; }
    public Currency getCurrency() { return currency; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money money)) return false;
        return amount.compareTo(money.amount) == 0 && currency.equals(money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount.stripTrailingZeros(), currency);
    }
}

4.2 Inheritance — Template Method a notification küldéshez

→ Lásd a 2.2 Inheritance szekció NotificationSender példáját.

4.3 Polymorphism — Strategy pattern kockázat-értékeléshez

// Több evaluator — mindegyik más szempontot vizsgál
public class CompositeRiskEvaluator implements RiskEvaluator {

    private final List<RiskEvaluator> evaluators;

    public CompositeRiskEvaluator(List<RiskEvaluator> evaluators) {
        this.evaluators = List.copyOf(evaluators); // defensive copy
    }

    @Override
    public RiskLevel evaluate(Transaction transaction) {
        // A legmagasabb kockázati szintet adjuk vissza
        return evaluators.stream()
            .map(e -> e.evaluate(transaction))
            .max(Comparator.comparingInt(RiskLevel::severity))
            .orElse(RiskLevel.LOW);
    }

    @Override
    public String evaluatorName() {
        return "COMPOSITE";
    }
}

4.4 Gyakori hiba — ❌ / ✅

// ❌ ROSSZ: Az encapsulation megsértése — publikus mutable mező
public class PaymentOrder {
    public List<LineItem> items = new ArrayList<>();  // bárki módosíthatja!
    public BigDecimal totalAmount;                     // felülírható közvetlenül!
}

// Használat — az invariáns könnyen megsérül:
PaymentOrder order = new PaymentOrder();
order.items.add(new LineItem("Widget", new BigDecimal("10.00")));
order.totalAmount = new BigDecimal("-999.99");  // ⚠️ Negatív összeg!
// ✅ HELYES: Üzleti műveleteken keresztül módosítható, invariánsok védettek
public class PaymentOrder {

    private final List<LineItem> items = new ArrayList<>();
    private BigDecimal totalAmount = BigDecimal.ZERO;

    public void addItem(LineItem item) {
        Objects.requireNonNull(item, "A tétel nem lehet null");
        if (item.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("A tétel ára pozitív kell legyen");
        }
        this.items.add(item);
        this.totalAmount = this.totalAmount.add(item.getPrice());
    }

    // Defensive copy — a hívó nem módosíthatja a belső listát
    public List<LineItem> getItems() {
        return Collections.unmodifiableList(items);
    }

    public BigDecimal getTotalAmount() {
        return totalAmount;
    }
}

5. Trade-offok

Szempont Részletek
Performance A virtual method dispatch (polimorfizmus) minimális overhead-del jár — a JIT compiler inline-olja a hot path-okat. Az abstraction layerek viszont cache miss-t okozhatnak, ha túl sok indirection van.
💾 Memory Minden objektumnak van 12-16 byte header-je (mark word + class pointer). A mély öröklődési hierarchia növeli az objektum méretét, mert minden szülő mezői is benne vannak.
🔧 Maintainability Az OOP jól strukturált kódot eredményez, ha megfelelően alkalmazzák. Túlzott öröklődés és túl sok abstract layer viszont rontja az olvashatóságot — „trace hell", ahol 8 osztályon keresztül kell követni egy hívást.
🔄 Flexibility Az interface-alapú tervezés (programming to interfaces) kiváló rugalmasságot ad — új implementáció hozzáadása nem igényel meglévő kód módosítást (Open/Closed Principle). Az öröklődés ezzel szemben szoros csatolást hoz létre.

6. Gyakori hibák

6.1 — Anemic Domain Model

// ❌ Az objektum csak adat-tartó, a logika egy "service"-ben van
public class Transaction {
    private String id;
    private BigDecimal amount;
    private TransactionStatus status;

    // Getter + Setter mindenhol...
    public void setStatus(TransactionStatus status) { this.status = status; }
    public void setAmount(BigDecimal amount) { this.amount = amount; }
}

public class TransactionService {
    public void approve(Transaction tx) {
        if (tx.getStatus() == TransactionStatus.PENDING) {
            tx.setStatus(TransactionStatus.APPROVED);  // kívülről piszkáljuk az állapotot
        }
    }
}
// ✅ Rich Domain Model — az objektum felelős a saját állapotáért
public class Transaction {
    private final String id;
    private BigDecimal amount;
    private TransactionStatus status;

    public void approve() {
        if (this.status != TransactionStatus.PENDING) {
            throw new IllegalStateException("Csak PENDING tranzakció hagyható jóvá");
        }
        this.status = TransactionStatus.APPROVED;
    }

    // Nincs setStatus()! Az állapotváltás üzleti műveleten keresztül történik.
}

6.2 — Inheritance a kód-újrafelhasználáshoz (HAS-A helyett IS-A)

// ❌ ROSSZ: A PaymentLogger nem "egy fajta" ArrayList!
public class PaymentLogger extends ArrayList<String> {
    public void logPayment(String message) {
        this.add("[PAYMENT] " + message);
    }
}
// Probléma: a PaymentLogger örökli az ArrayList ÖSSZES publikus metódusát
// — clear(), remove(), set() — amik nem relevánsak egy logger-nek!
// ✅ HELYES: Composition — a logger TARTALMAZ egy listát, nem KITERJESZTI
public class PaymentLogger {
    private final List<String> logs = new ArrayList<>();

    public void logPayment(String message) {
        logs.add("[PAYMENT] " + message);
    }

    // Csak azt tesszük elérhetővé, amire tényleg szükség van
    public List<String> getRecentLogs(int count) {
        int start = Math.max(0, logs.size() - count);
        return Collections.unmodifiableList(logs.subList(start, logs.size()));
    }
}

6.3 — Leaky Abstraction (belső implementáció kiszivárgása)

// ❌ A belső lista referencia kiszivárog
public class RiskAssessmentResult {
    private final List<RiskFactor> factors;

    public RiskAssessmentResult(List<RiskFactor> factors) {
        this.factors = factors;  // ⚠️ Nem defensive copy!
    }

    public List<RiskFactor> getFactors() {
        return factors;  // ⚠️ Mutable referencia — a hívó módosíthatja!
    }
}

// A hívó véletlenül (vagy szándékosan) módosítja a belső állapotot:
List<RiskFactor> factors = result.getFactors();
factors.clear();  // 💥 Az eredeti objektum belső állapota elromlott!
// ✅ Defensive copy + unmodifiable view
public class RiskAssessmentResult {
    private final List<RiskFactor> factors;

    public RiskAssessmentResult(List<RiskFactor> factors) {
        this.factors = List.copyOf(factors);  // Defensive copy a konstruktorban
    }

    public List<RiskFactor> getFactors() {
        return factors;  // List.copyOf() már unmodifiable-t ad vissza
    }
}

6.4 — @Override annotáció hiánya

// ❌ Elírt metódusnév — override helyett új metódus jön létre
public class CustomRiskEvaluator extends AbstractRiskEvaluator {

    // Nem komputScore, hanem computeScore kellene!
    // @Override nélkül a fordító nem szól
    protected double komputScore(Transaction transaction) {
        return 0.5;
    }
    // Eredmény: az AbstractRiskEvaluator absztrakt computeScore()-ja fut
    // — vagy ha az is abstract, fordítási hiba, de ha default implementáció van, csendben rossz
}
// ✅ Az @Override annotáció fordítási hibát ad, ha a metódus nem létezik a szülőben
public class CustomRiskEvaluator extends AbstractRiskEvaluator {

    @Override  // ← A fordító ellenőrzi, hogy tényleg felülírás-e
    protected double computeScore(Transaction transaction) {
        return 0.5;
    }
}

7. Mélymerülés

Composition over Inheritance

A Gang of Four (GoF) könyv óta ismert elv: részesítsd előnyben a kompozíciót az öröklődés helyett. Az öröklődés a legerősebb csatolási forma Java-ban — ha a szülő változik, minden leszármazott potenciálisan érintett.

// ❌ Öröklődés alapú megközelítés — merev, nehezen bővíthető
public class FraudCheckingPaymentService extends PaymentService {
    @Override
    public PaymentResult process(PaymentRequest request) {
        fraudChecker.check(request);
        return super.process(request);
    }
}
// Mi van, ha rate limiting-et is akarok? Újabb extends?
// FraudCheckingRateLimitedPaymentService extends FraudCheckingPaymentService? 🤮
// ✅ Composition + Decorator pattern — rugalmasan bővíthető
public class FraudCheckingPaymentProcessor implements PaymentProcessor {

    private final PaymentProcessor delegate;
    private final FraudChecker fraudChecker;

    public FraudCheckingPaymentProcessor(PaymentProcessor delegate, FraudChecker fraudChecker) {
        this.delegate = delegate;
        this.fraudChecker = fraudChecker;
    }

    @Override
    public PaymentResult process(PaymentRequest request) {
        fraudChecker.check(request);       // előfeltétel
        return delegate.process(request);  // delegálás
    }

    @Override
    public boolean supports(PaymentMethod method) {
        return delegate.supports(method);
    }
}
// Rate limiting? Csinálj egy RateLimitingPaymentProcessor-t, ami szintén delegate-el.
// Tetszőlegesen kombinálhatók, egymásba ágyazhatók.

SOLID kapcsolat

Az OOP pillérei önmagukban nem elegek — a SOLID elvek adják meg, hogyan alkalmazd őket helyesen:

SOLID Kapcsolat az OOP-hez
S — Single Responsibility Egy osztály egy okból változzon → encapsulation fókuszálása
O — Open/Closed Bővíthető, de nem módosítandó → polimorfizmus + abstraction
L — Liskov Substitution A leszármazott helyettesíthesse a szülőt → helyes inheritance
I — Interface Segregation Kis, fókuszált interface-ek → jobb abstraction
D — Dependency Inversion Absztrakciókra építs, ne konkrét implementációkra → interface-ek használata

Over-abstraction veszélyei

Kódbázis mérete:     ~5000 sor
Absztrakt rétegek:   8 (ControllerServiceFacadeStrategyHandlerProcessorAdapterGateway)
Egy request trace:    15 osztály, 3 interface
Új fejlesztő onboarding ideje: 3 hét 😱

Szabály: Kérdezd meg magadtól — „Ha ezt holnap kellene módosítanom, hány fájlt kell megnyitnom, hogy megértsem a flow-t?" Ha a válasz 6+, valószínűleg túl sok az absztrakció.

Sealed classes (Java 17+)

A modern Java sealed kulcsszóval korlátozhatja az öröklődést — csak az engedélyezett leszármazottak létezhetnek:

// Pontosan három féle notification-outcome lehet, más nem
public sealed interface NotificationOutcome
    permits Delivered, Failed, Retrying {
}

public record Delivered(Instant timestamp) implements NotificationOutcome {}
public record Failed(String reason, Instant timestamp) implements NotificationOutcome {}
public record Retrying(int attempt, Instant nextRetry) implements NotificationOutcome {}

// Pattern matching — a fordító ellenőrzi, hogy minden esetet kezelünk
public String describe(NotificationOutcome outcome) {
    return switch (outcome) {
        case Delivered d   -> "Kézbesítve: " + d.timestamp();
        case Failed f      -> "Sikertelen: " + f.reason();
        case Retrying r    -> "Újrapróbálkozás #" + r.attempt();
        // Nincs szükség default ágra — a sealed biztosítja a teljességet
    };
}

Interview tipp

Kérdés: „Mikor használnál abstract class-t és mikor interface-t?"

Jó válasz: „Ha van közös állapot vagy implementáció, amit a leszármazottak megosztanak — abstract class. Ha tisztán viselkedési szerződésről van szó, amit különböző, nem rokon osztályok is megvalósíthatnak — interface. Java 8 óta az interface-eknek is lehet default metódusuk, ami összezavarja a határt — de a döntő különbség, hogy interface-nek nincs állapota (instance field)."


8. Interjúkérdések

Hogyan különbözteted meg röviden a kapszulázást és az absztrakciót?

A kapszulázás a belső állapot védelméről szól, az absztrakció pedig arról, hogy a hívó csak a szükséges fogalmakkal és műveletekkel találkozzon.

Miért biztonságosabb sokszor a kompozíció, mint az öröklődés?

Mert lazább csatolást ad. Az öröklődésnél a leszármazott erősen függ a szülő viselkedésétől és fejlődésétől, míg a kompozícióban a viselkedés könnyebben cserélhető vagy kombinálható.

Mikor jobb választás az abstract class, mint az interface?

Ha több szorosan kapcsolódó osztály közös állapotot vagy közös implementációt használ. Ha csak szerződés kell különböző típusok között, általában az interface a jobb választás.

Mi a jó interjúválasz az OOP négy pillérére?

Rövid definíció, Java-nyelvi példa és legalább egy trade-off. Az ítélőképesség fontosabb, mint a bemagolt definíció.


9. Szószedet

Fogalom Definíció
Virtual method dispatch A JVM mechanizmusa, amellyel futásidőben dönt el, melyik override-olt metódus hívódik meg — a vtable (virtual method table) alapján
Covariant return type A felülírt metódus visszatérési típusa lehet szűkebb (specifikusabb) az eredetinél (Java 5+)
Fragile Base Class Probléma, amikor a szülőosztály módosítása váratlanul megtöri a leszármazottakat
Tight coupling Szoros csatolás — két komponens annyira függ egymástól, hogy az egyik változása maga után vonja a másik módosítását
Defensive copy Másolat készítése bejövő/kimenő mutable objektumokról, hogy megvédjük a belső állapotot
Anemic Domain Model Anti-pattern, ahol az objektumok csak adatot tárolnak, az üzleti logika „service" osztályokban van
Diamond problem Többszörös öröklődésnél felmerülő ambiguitás — Java az interface default metódusokkal kezeli
Marker interface Üres interface (pl. Serializable), ami jelölésre szolgál, nem viselkedés definiálására

10. Gyorsreferencia

  1. Encapsulation = adatok + műveletek egy egységben, access modifiers-szel korlátozva
  2. Inheritance = IS-A reláció, extends kulcsszó, egyszeres öröklődés Java-ban
  3. Polymorphism = compile-time (overloading) + runtime (overriding, virtual dispatch)
  4. Abstraction = abstract class (állapot + részleges impl.) vs interface (szerződés)
  5. Ne írj getter/setter-t automatikusan — gondolkodj üzleti műveletekben
  6. Részesítsd előnyben a composition-t az inheritance helyett
  7. Használj @Override annotációt — mindig
  8. Interface-t használj a modul-határokon, abstract class-t a közös implementációhoz
  9. Ha 3+ szint mély az öröklődési hierarchia, refaktorálj composition irányba
  10. A sealed (Java 17+) az öröklődés kontrollált formája — exhaustive pattern matching-gel

🎮 Játékok

10 kérdés