1/1
</> Programare Obiect-Orientata

Programare Obiect-Orientata

Lecția 1 ⏱ 90 min

Curs Avansat de Programare Orientată pe Obiecte

Principii, Design Patterns și Arhitectură — cu exemple în Python


Cuprins

  1. Fundamente OOP — dincolo de baze
  2. Clasele în Python — mecanisme avansate
  3. Principiile SOLID
  4. Compoziție vs. moștenire
  5. Clase abstracte, protocoale și interfețe
  6. Design Patterns — Creaționale
  7. Design Patterns — Structurale
  8. Design Patterns — Comportamentale
  9. Metaprogramare și metaclase
  10. Descriptori, proprietăți și controlul accesului
  11. Mixins, moștenire multiplă și MRO
  12. Generice, tipuri și type hints avansate
  13. Dependency Injection și Inversion of Control
  14. Domain-Driven Design (DDD) — noțiuni
  15. Testarea codului orientat pe obiecte
  16. Anti-patterns și refactoring
  17. Studiu de caz: sistem complet de e-commerce

1. Fundamente OOP — dincolo de baze

1.1 Cele patru piloni revisitați

Programarea orientată pe obiecte se bazează pe patru concepte fundamentale care, combinate corect, produc cod modular, extensibil și mentenabil.

Abstracția nu înseamnă doar „a ascunde detalii” — înseamnă a modela realitatea la nivelul de detaliu relevant pentru problema de rezolvat. O clasă Cont într-o aplicație bancară abstractizează altceva decât într-un sistem de audit.

Încapsularea nu înseamnă doar „câmpuri private” — înseamnă protejarea invarianților unui obiect. Un obiect trebuie să fie mereu într-o stare validă, indiferent de cum este utilizat din exterior.

Moștenirea nu înseamnă „reutilizare de cod” — înseamnă relația IS-A (este-un). Dacă B moștenește A, orice instanță B trebuie să poată înlocui o instanță A fără a strica logica programului (Principiul Liskov).

Polimorfismul nu înseamnă doar „suprascriere de metode” — înseamnă că obiectele diferite răspund la același mesaj în moduri specifice, permițând scrierea de cod generic care funcționează cu tipuri necunoscute la momentul scrierii.

1.2 Obiectele ca cetățeni de primă clasă în Python

# În Python, TOTUL este obiect — inclusiv clasele, funcțiile și modulele

# Funcțiile sunt obiecte:
def greet(name: str) -> str:
    return f"Hello, {name}"

print(type(greet))          # <class 'function'>
print(greet.__name__)       # 'greet'
print(greet.__doc__)        # None

# Clasele sunt obiecte (instanțe ale metaclasei 'type'):
class Dog:
    pass

print(type(Dog))            # <class 'type'>
print(isinstance(Dog, type))  # True
print(isinstance(Dog, object))  # True

# Putem pasa clase ca argumente, le putem stoca în structuri de date:
registry: dict[str, type] = {}

def register(cls: type) -> type:
    registry[cls.__name__] = cls
    return cls

@register
class Cat:
    pass

@register
class Bird:
    pass

print(registry)  # {'Cat': <class 'Cat'>, 'Bird': <class 'Bird'>}
instance = registry["Cat"]()  # Instanțiere dinamică

1.3 Responsabilitatea unui obiect

Un obiect bine proiectat are:

  • Identitate: se distinge de alte obiecte (chiar dacă au aceleași date)
  • Stare: datele interne (atributele) care descriu obiectul la un moment dat
  • Comportament: metodele care operează pe stare și expun funcționalitate
  • Invarianți: reguli care trebuie să fie mereu adevărate despre starea obiectului
class BankAccount:
    """Un cont bancar cu invariantul: soldul nu poate fi negativ."""

    def __init__(self, owner: str, initial_balance: float = 0.0):
        if initial_balance < 0:
            raise ValueError("Initial balance cannot be negative")
        self._owner = owner
        self._balance = initial_balance
        self._transactions: list[tuple[str, float]] = []

    @property
    def balance(self) -> float:
        return self._balance

    @property
    def owner(self) -> str:
        return self._owner

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self._balance += amount
        self._transactions.append(("deposit", amount))

    def withdraw(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self._balance:
            raise InsufficientFundsError(
                f"Cannot withdraw {amount}: balance is {self._balance}"
            )
        self._balance -= amount
        self._transactions.append(("withdrawal", amount))

    def __repr__(self) -> str:
        return f"BankAccount(owner={self._owner!r}, balance={self._balance:.2f})"


class InsufficientFundsError(Exception):
    """Excepție specifică domeniului."""
    pass

# Invariantul este protejat: nu există cale legală de a face soldul negativ
account = BankAccount("Alice", 100.0)
account.deposit(50.0)       # OK
account.withdraw(200.0)     # Raises InsufficientFundsError

2. Clasele în Python — mecanisme avansate

2.1 init vs. new vs. init_subclass

class Singleton:
    """Pattern Singleton prin __new__."""
    _instance = None

    def __new__(cls, *args, **kwargs):
        """Controlează CREAREA instanței (înainte de __init__)."""
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, value: int = 0):
        """Controlează INIȚIALIZAREA instanței (după __new__)."""
        # Atenție: __init__ se apelează la fiecare 'Singleton()'
        # chiar dacă __new__ returnează aceeași instanță
        self.value = value

s1 = Singleton(1)
s2 = Singleton(2)
print(s1 is s2)       # True — aceeași instanță
print(s1.value)        # 2 — __init__ a fost apelat din nou
class PluginBase:
    """Înregistrare automată a subclaselor prin __init_subclass__."""
    _plugins: dict[str, type] = {}

    def __init_subclass__(cls, plugin_name: str = "", **kwargs):
        """Apelat automat când o clasă moștenește PluginBase."""
        super().__init_subclass__(**kwargs)
        name = plugin_name or cls.__name__.lower()
        PluginBase._plugins[name] = cls
        print(f"Registered plugin: {name}{cls.__name__}")

    @classmethod
    def get_plugin(cls, name: str) -> type:
        return cls._plugins[name]

class JSONExporter(PluginBase, plugin_name="json"):
    def export(self, data): return "json..."

class CSVExporter(PluginBase, plugin_name="csv"):
    def export(self, data): return "csv..."

# Output automat:
# Registered plugin: json → JSONExporter
# Registered plugin: csv → CSVExporter

exporter = PluginBase.get_plugin("json")()

2.2 slots — optimizare memorie

# Fără __slots__: fiecare instanță are un __dict__ (dicționar intern)
class PointDict:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

# Cu __slots__: fără __dict__, atribute fixe, memorie redusă ~40%
class PointSlots:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

import sys
p1 = PointDict(1.0, 2.0)
p2 = PointSlots(1.0, 2.0)
print(sys.getsizeof(p1.__dict__))  # ~104 bytes (dict overhead)
# p2 nu are __dict__ — economie semnificativă la milioane de instanțe

# Dezavantaje __slots__:
# - Nu poți adăuga atribute noi dinamic
# - Complică moștenirea (subclasa trebuie să-și declare propriile __slots__)
# - __slots__ = () în subclasă dacă nu adaugă atribute noi

2.3 Dataclasses — boilerplate redus

from dataclasses import dataclass, field, asdict, astuple
from typing import ClassVar

@dataclass(frozen=True, order=True, slots=True)
class Money:
    """Immutable value object pentru bani."""
    # frozen=True: instanța nu poate fi modificată (hashable)
    # order=True: generează __lt__, __le__, __gt__, __ge__
    # slots=True: generează __slots__ (Python 3.10+)

    amount: float
    currency: str = "RON"

    # ClassVar nu e inclusă în __init__ sau comparații
    supported_currencies: ClassVar[set[str]] = {"RON", "EUR", "USD"}

    def __post_init__(self):
        """Validare după __init__ generat automat."""
        if self.currency not in self.supported_currencies:
            raise ValueError(f"Unsupported currency: {self.currency}")
        if self.amount < 0:
            raise ValueError("Amount cannot be negative")

    def __add__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)

    def __mul__(self, factor: float) -> "Money":
        return Money(round(self.amount * factor, 2), self.currency)


@dataclass
class OrderItem:
    product_name: str
    quantity: int
    unit_price: Money
    # Câmp calculat (nu în __init__, nu în comparații):
    total: Money = field(init=False, compare=False)

    def __post_init__(self):
        self.total = self.unit_price * self.quantity


price = Money(29.99, "RON")
item = OrderItem("Widget", 3, price)
print(item.total)      # Money(amount=89.97, currency='RON')
print(asdict(item))    # Conversie la dict (recursiv)

2.4 Dunder methods — protocol complet

from functools import total_ordering

@total_ordering  # Generează restul comparațiilor din __eq__ și __lt__
class Temperature:
    """Exemplu complet de implementare protocol Python."""

    def __init__(self, celsius: float):
        self._celsius = celsius

    # === Reprezentare ===
    def __repr__(self) -> str:
        """Reprezentare neambiguă (pentru debug, eval)."""
        return f"Temperature({self._celsius})"

    def __str__(self) -> str:
        """Reprezentare user-friendly."""
        return f"{self._celsius:.1f}°C"

    def __format__(self, spec: str) -> str:
        """Suport pentru f-strings cu format spec."""
        if spec == "F":
            return f"{self._celsius * 9/5 + 32:.1f}°F"
        return f"{self._celsius:{spec}}°C"

    # === Comparații ===
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Temperature):
            return NotImplemented
        return self._celsius == other._celsius

    def __lt__(self, other: "Temperature") -> bool:
        if not isinstance(other, Temperature):
            return NotImplemented
        return self._celsius < other._celsius

    # === Aritmetică ===
    def __add__(self, other: "Temperature") -> "Temperature":
        if isinstance(other, Temperature):
            return Temperature(self._celsius + other._celsius)
        if isinstance(other, (int, float)):
            return Temperature(self._celsius + other)
        return NotImplemented

    def __radd__(self, other: float) -> "Temperature":
        """Permite: 5 + Temperature(10)"""
        return self.__add__(other)

    def __neg__(self) -> "Temperature":
        return Temperature(-self._celsius)

    # === Hashing (necesar dacă __eq__ e definit) ===
    def __hash__(self) -> int:
        return hash(self._celsius)

    # === Bool ===
    def __bool__(self) -> bool:
        """Temperature(0) este falsy (absolut zero Celsius? Nu, dar exemplificativ)."""
        return self._celsius != 0

    # === Context manager ===
    # (nu are sens pentru Temperature, dar demonstrativ)

    # === Container protocol ===
    # __len__, __getitem__, __setitem__, __delitem__
    # __contains__ (__in__), __iter__, __next__


t = Temperature(23.5)
print(f"Temperature: {t}")          # 23.5°C
print(f"In Fahrenheit: {t:F}")      # 74.3°F
print(repr(t))                       # Temperature(23.5)
print(t + Temperature(6.5))         # Temperature(30.0)
print(sorted([Temperature(30), Temperature(10), Temperature(20)]))
# [Temperature(10), Temperature(20), Temperature(30)]

3. Principiile SOLID

3.1 S — Single Responsibility Principle (SRP)

O clasă trebuie să aibă un singur motiv de schimbare — o singură responsabilitate.

# ❌ GREȘIT: clasa face prea multe
class Report:
    def __init__(self, data: list[dict]):
        self.data = data

    def calculate_statistics(self) -> dict:
        # Logică de calcul
        return {"total": sum(d["amount"] for d in self.data)}

    def format_as_html(self) -> str:
        # Logică de formatare
        return f"<html><body>{self.calculate_statistics()}</body></html>"

    def send_email(self, to: str) -> None:
        # Logică de trimitere email
        import smtplib
        # ...

    def save_to_database(self) -> None:
        # Logică de persistență
        # ...

# ✅ CORECT: fiecare clasă are o singură responsabilitate
class ReportData:
    """Calculul statisticilor din date."""
    def __init__(self, data: list[dict]):
        self.data = data

    def calculate_statistics(self) -> dict:
        return {"total": sum(d["amount"] for d in self.data)}


class HTMLReportFormatter:
    """Formatarea unui raport ca HTML."""
    def format(self, stats: dict) -> str:
        return f"<html><body>{stats}</body></html>"


class EmailSender:
    """Trimiterea de email-uri."""
    def send(self, to: str, subject: str, body: str) -> None:
        pass  # Implementare SMTP


class ReportRepository:
    """Persistența rapoartelor."""
    def save(self, report_data: dict) -> None:
        pass  # Implementare DB

# Compunerea lor:
data = ReportData(raw_data)
stats = data.calculate_statistics()
html = HTMLReportFormatter().format(stats)
EmailSender().send("boss@company.com", "Monthly Report", html)

3.2 O — Open/Closed Principle (OCP)

O clasă trebuie să fie deschisă pentru extindere, dar închisă pentru modificare.

from abc import ABC, abstractmethod

# ❌ GREȘIT: trebuie să modificăm clasa existentă la fiecare format nou
class ReportExporterBad:
    def export(self, data: dict, format: str) -> str:
        if format == "json":
            import json
            return json.dumps(data)
        elif format == "csv":
            return ",".join(f"{k},{v}" for k, v in data.items())
        elif format == "xml":
            # Adăugat ulterior — am modificat clasa existentă!
            return "<data>...</data>"
        else:
            raise ValueError(f"Unknown format: {format}")

# ✅ CORECT: extensibil prin adăugare de clase noi, fără a modifica codul existent
class Exporter(ABC):
    @abstractmethod
    def export(self, data: dict) -> str:
        ...

class JSONExporter(Exporter):
    def export(self, data: dict) -> str:
        import json
        return json.dumps(data, indent=2)

class CSVExporter(Exporter):
    def export(self, data: dict) -> str:
        return "\n".join(f"{k},{v}" for k, v in data.items())

class XMLExporter(Exporter):
    def export(self, data: dict) -> str:
        entries = "".join(f"<{k}>{v}</{k}>" for k, v in data.items())
        return f"<data>{entries}</data>"

# Adăugarea unui format nou = adăugarea unei clase noi (zero modificări în codul existent)
class YAMLExporter(Exporter):
    def export(self, data: dict) -> str:
        import yaml
        return yaml.dump(data)

# Cod client care funcționează cu ORICE exporter:
def generate_report(data: dict, exporter: Exporter) -> str:
    return exporter.export(data)

3.3 L — Liskov Substitution Principle (LSP)

Obiectele unei subclase trebuie să poată înlocui obiectele superclasei fără a strica comportamentul programului.

# ❌ GREȘIT — Încalcă LSP: pătratul nu respectă contractul dreptunghiului
class Rectangle:
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height

    @property
    def width(self) -> float:
        return self._width

    @width.setter
    def width(self, value: float) -> None:
        self._width = value

    @property
    def height(self) -> float:
        return self._height

    @height.setter
    def height(self, value: float) -> None:
        self._height = value

    def area(self) -> float:
        return self._width * self._height


class Square(Rectangle):
    """Pătratul ca subclasă de dreptunghi — VIOLEAZĂ LSP!"""
    def __init__(self, side: float):
        super().__init__(side, side)

    @Rectangle.width.setter
    def width(self, value: float):
        self._width = value
        self._height = value   # Forțează egalitate → surpriză!

    @Rectangle.height.setter
    def height(self, value: float):
        self._width = value
        self._height = value

# Cod client care se bazează pe contractul Rectangle:
def double_width(rect: Rectangle) -> None:
    rect.width = rect.width * 2
    # Așteptare: area = (width*2) * height = area_inițial * 2
    # Cu Square: area = (width*2) * (width*2) = area_inițial * 4 !!! BUG!


# ✅ CORECT — Modelare prin compoziție sau ierarhie corectă:
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

class RectangleOK(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

class SquareOK(Shape):
    def __init__(self, side: float):
        self.side = side

    def area(self) -> float:
        return self.side ** 2

# Acum nu există relație de moștenire problematică.
# Ambele respectă contractul Shape.area() fără surprize.

3.4 I — Interface Segregation Principle (ISP)

Clienții nu trebuie forțați să depindă de metode pe care nu le folosesc.

from abc import ABC, abstractmethod

# ❌ GREȘIT — interfață „grasă": forțează implementarea tuturor metodelor
class Worker(ABC):
    @abstractmethod
    def work(self): ...
    @abstractmethod
    def eat(self): ...
    @abstractmethod
    def sleep(self): ...

class Robot(Worker):
    def work(self): print("Working...")
    def eat(self): raise NotImplementedError("Robots don't eat!")  # Absurd
    def sleep(self): raise NotImplementedError("Robots don't sleep!")  # Absurd

# ✅ CORECT — interfețe segregate, granulare
class Workable(ABC):
    @abstractmethod
    def work(self) -> None: ...

class Feedable(ABC):
    @abstractmethod
    def eat(self) -> None: ...

class Sleepable(ABC):
    @abstractmethod
    def sleep(self) -> None: ...

class Human(Workable, Feedable, Sleepable):
    def work(self): print("Working...")
    def eat(self): print("Eating...")
    def sleep(self): print("Sleeping...")

class RobotOK(Workable):    # Implementează DOAR ce are sens
    def work(self): print("Working...")

# Funcția depinde doar de interfața minimă necesară:
def assign_task(worker: Workable) -> None:
    worker.work()   # Funcționează cu Human ȘI cu Robot

3.5 D — Dependency Inversion Principle (DIP)

Modulele de nivel înalt nu trebuie să depindă de modulele de nivel scăzut. Ambele trebuie să depindă de abstracții.

from abc import ABC, abstractmethod

# ❌ GREȘIT — dependență directă de implementare concretă
class MySQLDatabase:
    def query(self, sql: str) -> list:
        # Implementare specifică MySQL
        return []

class UserServiceBad:
    def __init__(self):
        self.db = MySQLDatabase()   # Cuplare strânsă!
        # Ce facem dacă vrem PostgreSQL? Trebuie modificat UserService.

    def get_user(self, user_id: int) -> dict:
        return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")


# ✅ CORECT — depinde de abstracție (interfață), nu de implementare
class Database(ABC):
    @abstractmethod
    def query(self, sql: str) -> list: ...

    @abstractmethod
    def execute(self, sql: str, params: tuple) -> None: ...

class MySQLDB(Database):
    def query(self, sql: str) -> list:
        return []  # Implementare MySQL

    def execute(self, sql: str, params: tuple) -> None:
        pass

class PostgreSQLDB(Database):
    def query(self, sql: str) -> list:
        return []  # Implementare PostgreSQL

    def execute(self, sql: str, params: tuple) -> None:
        pass

class UserServiceGood:
    def __init__(self, db: Database):   # Depinde de abstracție!
        self.db = db                     # Injectată din exterior

    def get_user(self, user_id: int) -> dict:
        results = self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
        return results[0] if results else {}

# Acum putem schimba implementarea fără a modifica UserService:
service_mysql = UserServiceGood(MySQLDB())
service_postgres = UserServiceGood(PostgreSQLDB())

# Și putem testa cu un mock:
class FakeDB(Database):
    def query(self, sql: str) -> list:
        return [{"id": 1, "name": "Test User"}]
    def execute(self, sql: str, params: tuple) -> None:
        pass

service_test = UserServiceGood(FakeDB())

4. Compoziție vs. moștenire

4.1 Regula de aur: favorizează compoziția

# ❌ Moștenire problematică: relația IS-A nu e corectă
class Engine:
    def start(self): print("Engine started")
    def stop(self): print("Engine stopped")

class Car(Engine):    # Car IS-A Engine? Nu! Car HAS-A Engine.
    pass

# ✅ Compoziție: relația HAS-A, corectă și flexibilă
class ElectricEngine:
    def start(self): print("Electric engine humming")
    def stop(self): print("Electric engine off")

class DieselEngine:
    def start(self): print("Diesel engine roaring")
    def stop(self): print("Diesel engine off")

class Car:
    def __init__(self, engine: "Engine", brand: str):
        self._engine = engine   # HAS-A: mașina are un motor
        self.brand = brand

    def start(self) -> None:
        print(f"Starting {self.brand}...")
        self._engine.start()

    def stop(self) -> None:
        self._engine.stop()
        print(f"{self.brand} stopped")

# Flexibilitate: putem schimba motorul la runtime!
tesla = Car(ElectricEngine(), "Tesla")
tesla.start()   # Starting Tesla... Electric engine humming

truck = Car(DieselEngine(), "Volvo")
truck.start()   # Starting Volvo... Diesel engine roaring

4.2 Delegare explicită

class Logger:
    def __init__(self, name: str):
        self.name = name

    def log(self, message: str, level: str = "INFO") -> None:
        print(f"[{level}] {self.name}: {message}")


class Cache:
    def __init__(self, max_size: int = 100):
        self._store: dict[str, object] = {}
        self._max_size = max_size

    def get(self, key: str) -> object | None:
        return self._store.get(key)

    def set(self, key: str, value: object) -> None:
        if len(self._store) >= self._max_size:
            oldest_key = next(iter(self._store))
            del self._store[oldest_key]
        self._store[key] = value


class UserRepository:
    """Compune Logger și Cache prin delegare — nu moștenește nici unul."""

    def __init__(self, db, logger: Logger, cache: Cache):
        self._db = db
        self._logger = logger
        self._cache = cache

    def get_user(self, user_id: int) -> dict | None:
        # Încearcă din cache
        cached = self._cache.get(f"user:{user_id}")
        if cached:
            self._logger.log(f"Cache hit for user {user_id}")
            return cached

        # Citește din DB
        self._logger.log(f"Cache miss for user {user_id}, querying DB")
        user = self._db.query(f"SELECT * FROM users WHERE id = {user_id}")

        if user:
            self._cache.set(f"user:{user_id}", user)

        return user

5. Clase abstracte, protocoale și interfețe

5.1 ABC — Abstract Base Classes

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    """Clasă abstractă — nu poate fi instanțiată direct."""

    @abstractmethod
    def charge(self, amount: float, currency: str) -> str:
        """Procesează o plată. Returnează transaction ID."""
        ...

    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        """Returnează banii pentru o tranzacție."""
        ...

    def validate_amount(self, amount: float) -> None:
        """Metoda concretă — poate fi moștenită direct."""
        if amount <= 0:
            raise ValueError("Amount must be positive")
        if amount > 10_000:
            raise ValueError("Amount exceeds maximum")

# PaymentProcessor() → TypeError: Can't instantiate abstract class

class StripeProcessor(PaymentProcessor):
    def charge(self, amount: float, currency: str) -> str:
        self.validate_amount(amount)   # Moștenită
        # Logică Stripe API...
        return "txn_stripe_123"

    def refund(self, transaction_id: str) -> bool:
        # Logică refund Stripe...
        return True

class PayPalProcessor(PaymentProcessor):
    def charge(self, amount: float, currency: str) -> str:
        self.validate_amount(amount)
        return "txn_paypal_456"

    def refund(self, transaction_id: str) -> bool:
        return True

5.2 Protocol — Duck typing structural (Python 3.8+)

from typing import Protocol, runtime_checkable

@runtime_checkable
class Renderable(Protocol):
    """Orice obiect care are metoda render() respectă acest protocol.
    NU necesită moștenire — duck typing pur, dar tipizat."""

    def render(self) -> str: ...

class HTMLWidget:
    def render(self) -> str:
        return "<div>Widget</div>"

class MarkdownDoc:
    def render(self) -> str:
        return "# Document"

class PlainText:
    def render(self) -> str:
        return "Just text"

# Funcția acceptă orice Renderable — fără moștenire!
def display(item: Renderable) -> None:
    print(item.render())

display(HTMLWidget())    # OK
display(MarkdownDoc())   # OK
display(PlainText())     # OK

# isinstance funcționează dacă @runtime_checkable:
print(isinstance(HTMLWidget(), Renderable))  # True
print(isinstance("string", Renderable))      # False

5.3 Când să folosești ABC vs. Protocol

Criteriu ABC Protocol
Moștenire necesară? Da (trebuie să moștenească) Nu (duck typing structural)
Metode concrete partajate? Da (template method) Nu
Verificare la instanțiere? Da (TypeError imediat) Doar la type checking (mypy)
Cod legacy / terț? Nu funcționează fără modificare Funcționează (dacă respectă interfața)
Folosire recomandată Framework intern, ierarhii clare API-uri publice, integrare externă

6. Design Patterns — Creaționale

6.1 Factory Method

from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message: str) -> None: ...

class EmailNotification(Notification):
    def __init__(self, email: str):
        self.email = email
    def send(self, message: str) -> None:
        print(f"Email to {self.email}: {message}")

class SMSNotification(Notification):
    def __init__(self, phone: str):
        self.phone = phone
    def send(self, message: str) -> None:
        print(f"SMS to {self.phone}: {message}")

class PushNotification(Notification):
    def __init__(self, device_id: str):
        self.device_id = device_id
    def send(self, message: str) -> None:
        print(f"Push to {self.device_id}: {message}")


class NotificationFactory:
    """Factory Method — creează obiecte fără a expune logica de instanțiere."""

    _creators: dict[str, type[Notification]] = {
        "email": EmailNotification,
        "sms": SMSNotification,
        "push": PushNotification,
    }

    @classmethod
    def register(cls, channel: str, creator: type[Notification]) -> None:
        cls._creators[channel] = creator

    @classmethod
    def create(cls, channel: str, **kwargs) -> Notification:
        creator = cls._creators.get(channel)
        if not creator:
            raise ValueError(f"Unknown channel: {channel}")
        return creator(**kwargs)


# Utilizare:
notif = NotificationFactory.create("email", email="user@example.com")
notif.send("Your order has shipped!")

notif2 = NotificationFactory.create("sms", phone="+40712345678")
notif2.send("Code: 123456")

6.2 Builder

from dataclasses import dataclass, field

@dataclass
class HTTPRequest:
    method: str = "GET"
    url: str = ""
    headers: dict[str, str] = field(default_factory=dict)
    body: str | None = None
    timeout: int = 30
    retries: int = 0
    auth: tuple[str, str] | None = None

class HTTPRequestBuilder:
    """Builder pattern — construcție pas cu pas a obiectelor complexe."""

    def __init__(self):
        self._request = HTTPRequest()

    def method(self, method: str) -> "HTTPRequestBuilder":
        self._request.method = method
        return self   # Fluent interface — permite chaining

    def url(self, url: str) -> "HTTPRequestBuilder":
        self._request.url = url
        return self

    def header(self, key: str, value: str) -> "HTTPRequestBuilder":
        self._request.headers[key] = value
        return self

    def json_body(self, data: dict) -> "HTTPRequestBuilder":
        import json
        self._request.body = json.dumps(data)
        self._request.headers["Content-Type"] = "application/json"
        return self

    def timeout(self, seconds: int) -> "HTTPRequestBuilder":
        self._request.timeout = seconds
        return self

    def with_retry(self, count: int) -> "HTTPRequestBuilder":
        self._request.retries = count
        return self

    def basic_auth(self, user: str, password: str) -> "HTTPRequestBuilder":
        self._request.auth = (user, password)
        return self

    def build(self) -> HTTPRequest:
        if not self._request.url:
            raise ValueError("URL is required")
        return self._request


# Fluent API — citibil ca o propoziție:
request = (
    HTTPRequestBuilder()
    .method("POST")
    .url("https://api.example.com/users")
    .header("Accept", "application/json")
    .json_body({"name": "Alice", "email": "alice@example.com"})
    .basic_auth("api_key", "secret")
    .timeout(10)
    .with_retry(3)
    .build()
)

6.3 Observer

from abc import ABC, abstractmethod
from typing import Any
from weakref import WeakSet

class Event:
    """Eveniment cu date arbitrare."""
    def __init__(self, name: str, data: Any = None):
        self.name = name
        self.data = data

class EventListener(ABC):
    @abstractmethod
    def handle(self, event: Event) -> None: ...

class EventBus:
    """Magistrală de evenimente centralizată."""

    def __init__(self):
        self._listeners: dict[str, WeakSet[EventListener]] = {}

    def subscribe(self, event_name: str, listener: EventListener) -> None:
        if event_name not in self._listeners:
            self._listeners[event_name] = WeakSet()
        self._listeners[event_name].add(listener)

    def publish(self, event: Event) -> None:
        for listener in self._listeners.get(event.name, []):
            listener.handle(event)

    # Decorator syntax:
    def on(self, event_name: str):
        """Decorator pentru a înregistra funcții ca listeneri."""
        def decorator(func):
            class FuncListener(EventListener):
                def handle(self, event: Event) -> None:
                    func(event)
            self.subscribe(event_name, FuncListener())
            return func
        return decorator


# Utilizare:
bus = EventBus()

class EmailService(EventListener):
    def handle(self, event: Event) -> None:
        user = event.data
        print(f"Sending welcome email to {user['email']}")

class AnalyticsService(EventListener):
    def handle(self, event: Event) -> None:
        print(f"Tracking: new user registered — {event.data['name']}")

# Înregistrare
bus.subscribe("user.registered", EmailService())
bus.subscribe("user.registered", AnalyticsService())

# Publicare — toți ascultătorii sunt notificați automat
bus.publish(Event("user.registered", {"name": "Alice", "email": "alice@example.com"}))

7. Design Patterns — Structurale

7.1 Decorator (Wrapper)

from abc import ABC, abstractmethod
import time
import functools

class DataSource(ABC):
    @abstractmethod
    def read(self) -> str: ...
    @abstractmethod
    def write(self, data: str) -> None: ...

class FileDataSource(DataSource):
    def __init__(self, filename: str):
        self.filename = filename
    def read(self) -> str:
        with open(self.filename) as f:
            return f.read()
    def write(self, data: str) -> None:
        with open(self.filename, "w") as f:
            f.write(data)

class DataSourceDecorator(DataSource):
    """Decorator de bază — delegare transparentă."""
    def __init__(self, wrapped: DataSource):
        self._wrapped = wrapped
    def read(self) -> str:
        return self._wrapped.read()
    def write(self, data: str) -> None:
        self._wrapped.write(data)

class EncryptedDataSource(DataSourceDecorator):
    """Adaugă criptare/decriptare transparentă."""
    def read(self) -> str:
        data = super().read()
        return self._decrypt(data)
    def write(self, data: str) -> None:
        super().write(self._encrypt(data))
    def _encrypt(self, data: str) -> str:
        return data[::-1]  # Simplificat — ar fi AES în realitate
    def _decrypt(self, data: str) -> str:
        return data[::-1]

class CompressedDataSource(DataSourceDecorator):
    """Adaugă compresie/decompresie transparentă."""
    def read(self) -> str:
        import zlib, base64
        raw = super().read()
        return zlib.decompress(base64.b64decode(raw)).decode()
    def write(self, data: str) -> None:
        import zlib, base64
        compressed = base64.b64encode(zlib.compress(data.encode())).decode()
        super().write(compressed)

# Stacking decorators — fiecare adaugă un strat de funcționalitate:
source = FileDataSource("data.txt")
source = CompressedDataSource(source)     # + compresie
source = EncryptedDataSource(source)       # + criptare

source.write("Sensitive data here")
# Flux: date → criptare → compresie → fișier
# La read: fișier → decompresie → decriptare → date

7.2 Strategy

from abc import ABC, abstractmethod
from dataclasses import dataclass

# Strategii de pricing
class PricingStrategy(ABC):
    @abstractmethod
    def calculate(self, base_price: float, quantity: int) -> float: ...

class RegularPricing(PricingStrategy):
    def calculate(self, base_price: float, quantity: int) -> float:
        return base_price * quantity

class BulkPricing(PricingStrategy):
    """10% discount peste 10 bucăți, 20% peste 50."""
    def calculate(self, base_price: float, quantity: int) -> float:
        total = base_price * quantity
        if quantity > 50:
            return total * 0.80
        elif quantity > 10:
            return total * 0.90
        return total

class SeasonalPricing(PricingStrategy):
    def __init__(self, discount_pct: float):
        self.discount_pct = discount_pct
    def calculate(self, base_price: float, quantity: int) -> float:
        return base_price * quantity * (1 - self.discount_pct / 100)


@dataclass
class Product:
    name: str
    base_price: float

class ShoppingCart:
    def __init__(self, pricing: PricingStrategy):
        self._items: list[tuple[Product, int]] = []
        self._pricing = pricing

    @property
    def pricing_strategy(self) -> PricingStrategy:
        return self._pricing

    @pricing_strategy.setter
    def pricing_strategy(self, strategy: PricingStrategy) -> None:
        """Strategia poate fi schimbată la runtime."""
        self._pricing = strategy

    def add(self, product: Product, quantity: int) -> None:
        self._items.append((product, quantity))

    def total(self) -> float:
        return sum(
            self._pricing.calculate(product.base_price, qty)
            for product, qty in self._items
        )


cart = ShoppingCart(RegularPricing())
cart.add(Product("Widget", 10.0), 5)
print(f"Regular: {cart.total():.2f}")    # 50.00

cart.pricing_strategy = BulkPricing()
cart.add(Product("Gadget", 20.0), 25)
print(f"Bulk: {cart.total():.2f}")       # 50.00 + 450.00

cart.pricing_strategy = SeasonalPricing(30)
print(f"Seasonal 30% off: {cart.total():.2f}")

7.3 Repository Pattern

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Generic, TypeVar
from uuid import uuid4

T = TypeVar("T")

@dataclass
class Entity:
    id: str = field(default_factory=lambda: str(uuid4()))

@dataclass
class User(Entity):
    name: str = ""
    email: str = ""
    active: bool = True

class Repository(ABC, Generic[T]):
    """Interfață generică pentru persistența entităților."""

    @abstractmethod
    def add(self, entity: T) -> None: ...

    @abstractmethod
    def get(self, entity_id: str) -> T | None: ...

    @abstractmethod
    def list_all(self) -> list[T]: ...

    @abstractmethod
    def update(self, entity: T) -> None: ...

    @abstractmethod
    def delete(self, entity_id: str) -> None: ...


class InMemoryRepository(Repository[T]):
    """Implementare în memorie (pentru teste și prototipare)."""

    def __init__(self):
        self._store: dict[str, T] = {}

    def add(self, entity: T) -> None:
        self._store[entity.id] = entity

    def get(self, entity_id: str) -> T | None:
        return self._store.get(entity_id)

    def list_all(self) -> list[T]:
        return list(self._store.values())

    def update(self, entity: T) -> None:
        if entity.id not in self._store:
            raise KeyError(f"Entity {entity.id} not found")
        self._store[entity.id] = entity

    def delete(self, entity_id: str) -> None:
        self._store.pop(entity_id, None)


class UserService:
    """Service-ul depinde de abstracția Repository, nu de implementare."""

    def __init__(self, repo: Repository[User]):
        self._repo = repo

    def register(self, name: str, email: str) -> User:
        # Validare
        existing = [u for u in self._repo.list_all() if u.email == email]
        if existing:
            raise ValueError(f"Email {email} already registered")

        user = User(name=name, email=email)
        self._repo.add(user)
        return user

    def deactivate(self, user_id: str) -> None:
        user = self._repo.get(user_id)
        if not user:
            raise KeyError(f"User {user_id} not found")
        user.active = False
        self._repo.update(user)


# Utilizare cu repository în memorie (teste):
repo = InMemoryRepository[User]()
service = UserService(repo)
alice = service.register("Alice", "alice@example.com")

# La producție, înlocuim cu SQLAlchemyRepository, MongoRepository etc.
# Codul UserService rămâne NESCHIMBAT.

8. Design Patterns — Comportamentale

8.1 Chain of Responsibility

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Request:
    path: str
    method: str
    headers: dict[str, str]
    body: str = ""
    user: str | None = None

@dataclass
class Response:
    status: int
    body: str

class Middleware(ABC):
    def __init__(self):
        self._next: Middleware | None = None

    def set_next(self, middleware: "Middleware") -> "Middleware":
        self._next = middleware
        return middleware

    def handle(self, request: Request) -> Response:
        if self._next:
            return self._next.handle(request)
        return Response(200, "OK")

class LoggingMiddleware(Middleware):
    def handle(self, request: Request) -> Response:
        print(f"[LOG] {request.method} {request.path}")
        response = super().handle(request)
        print(f"[LOG] Response: {response.status}")
        return response

class AuthMiddleware(Middleware):
    def handle(self, request: Request) -> Response:
        token = request.headers.get("Authorization")
        if not token:
            return Response(401, "Unauthorized")
        request.user = token.split(" ")[-1]  # Simplificat
        return super().handle(request)

class RateLimitMiddleware(Middleware):
    def __init__(self, max_requests: int = 100):
        super().__init__()
        self._counts: dict[str, int] = {}
        self._max = max_requests

    def handle(self, request: Request) -> Response:
        ip = request.headers.get("X-Forwarded-For", "unknown")
        self._counts[ip] = self._counts.get(ip, 0) + 1
        if self._counts[ip] > self._max:
            return Response(429, "Too Many Requests")
        return super().handle(request)

# Construirea lanțului:
logging = LoggingMiddleware()
auth = AuthMiddleware()
rate_limit = RateLimitMiddleware(max_requests=10)

logging.set_next(rate_limit).set_next(auth)

# Request traversează: logging → rate_limit → auth → handler
req = Request("/api/users", "GET", {"Authorization": "Bearer abc123"})
response = logging.handle(req)

8.2 State Machine

from abc import ABC, abstractmethod

class OrderState(ABC):
    @abstractmethod
    def pay(self, order: "Order") -> None: ...
    @abstractmethod
    def ship(self, order: "Order") -> None: ...
    @abstractmethod
    def deliver(self, order: "Order") -> None: ...
    @abstractmethod
    def cancel(self, order: "Order") -> None: ...

class PendingState(OrderState):
    def pay(self, order):
        print("Payment processed")
        order.state = PaidState()
    def ship(self, order):
        raise InvalidTransition("Cannot ship unpaid order")
    def deliver(self, order):
        raise InvalidTransition("Cannot deliver unpaid order")
    def cancel(self, order):
        print("Order cancelled")
        order.state = CancelledState()

class PaidState(OrderState):
    def pay(self, order):
        raise InvalidTransition("Already paid")
    def ship(self, order):
        print("Order shipped")
        order.state = ShippedState()
    def deliver(self, order):
        raise InvalidTransition("Must ship before delivering")
    def cancel(self, order):
        print("Refund issued, order cancelled")
        order.state = CancelledState()

class ShippedState(OrderState):
    def pay(self, order):
        raise InvalidTransition("Already paid")
    def ship(self, order):
        raise InvalidTransition("Already shipped")
    def deliver(self, order):
        print("Order delivered!")
        order.state = DeliveredState()
    def cancel(self, order):
        raise InvalidTransition("Cannot cancel shipped order")

class DeliveredState(OrderState):
    def pay(self, order): raise InvalidTransition("Order completed")
    def ship(self, order): raise InvalidTransition("Order completed")
    def deliver(self, order): raise InvalidTransition("Already delivered")
    def cancel(self, order): raise InvalidTransition("Cannot cancel delivered order")

class CancelledState(OrderState):
    def pay(self, order): raise InvalidTransition("Order cancelled")
    def ship(self, order): raise InvalidTransition("Order cancelled")
    def deliver(self, order): raise InvalidTransition("Order cancelled")
    def cancel(self, order): raise InvalidTransition("Already cancelled")

class InvalidTransition(Exception):
    pass

class Order:
    def __init__(self, order_id: str):
        self.order_id = order_id
        self.state: OrderState = PendingState()

    def pay(self): self.state.pay(self)
    def ship(self): self.state.ship(self)
    def deliver(self): self.state.deliver(self)
    def cancel(self): self.state.cancel(self)

    @property
    def status(self) -> str:
        return type(self.state).__name__.replace("State", "")

order = Order("ORD-001")
print(order.status)        # Pending
order.pay()                # Payment processed
print(order.status)        # Paid
order.ship()               # Order shipped
order.deliver()            # Order delivered!
# order.cancel()           # InvalidTransition: Cannot cancel delivered order

9. Metaprogramare și metaclase

9.1 Decoratori de clasă

import time
import functools

def timer(func):
    """Decorator care măsoară timpul de execuție."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__qualname__} took {elapsed:.4f}s")
        return result
    return wrapper

def singleton(cls):
    """Decorator de clasă care transformă clasa în singleton."""
    instances = {}
    @functools.wraps(cls, updated=[])
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

def auto_repr(cls):
    """Decorator care generează automat __repr__ din __init__ params."""
    import inspect
    params = list(inspect.signature(cls.__init__).parameters.keys())[1:]  # skip 'self'

    def __repr__(self):
        attrs = ", ".join(f"{p}={getattr(self, p)!r}" for p in params)
        return f"{cls.__name__}({attrs})"

    cls.__repr__ = __repr__
    return cls


@auto_repr
class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

print(Point(3, 4))  # Point(x=3, y=4)

9.2 Metaclase — controlul creării claselor

class ValidatedMeta(type):
    """Metaclasă care validează structura claselor la DEFINIRE (nu la instanțiere)."""

    def __new__(mcs, name, bases, namespace):
        # Verifică că clasa are docstring
        if not namespace.get("__doc__"):
            raise TypeError(f"Class {name} must have a docstring")

        # Verifică că toate metodele publice au type hints
        import typing
        for attr_name, attr_value in namespace.items():
            if callable(attr_value) and not attr_name.startswith("_"):
                hints = typing.get_type_hints(attr_value) if hasattr(attr_value, '__annotations__') else {}
                if "return" not in getattr(attr_value, "__annotations__", {}):
                    raise TypeError(
                        f"Method {name}.{attr_name}() must have a return type hint"
                    )

        return super().__new__(mcs, name, bases, namespace)


class Service(metaclass=ValidatedMeta):
    """Baza pentru servicii validate."""

    def health_check(self) -> bool:
        return True

# Aceasta ar genera TypeError la definire:
# class BadService(metaclass=ValidatedMeta):
#     def process(self):    # Lipsește return type hint
#         pass              # Lipsește docstring → TypeError

9.3 init_subclass — alternativa modernă la metaclase

class Serializable:
    """Înregistrare automată + validare subclase fără metaclasă."""

    _registry: dict[str, type] = {}

    def __init_subclass__(cls, type_name: str = "", **kwargs):
        super().__init_subclass__(**kwargs)
        name = type_name or cls.__name__

        # Validare: subclasele TREBUIE să implementeze 'serialize'
        if not hasattr(cls, "serialize") or not callable(getattr(cls, "serialize")):
            raise TypeError(f"{cls.__name__} must implement serialize()")

        Serializable._registry[name] = cls

    @classmethod
    def from_type(cls, type_name: str, **kwargs) -> "Serializable":
        """Factory method bazat pe înregistrare."""
        return cls._registry[type_name](**kwargs)


class UserDTO(Serializable, type_name="user"):
    def __init__(self, name: str = "", email: str = ""):
        self.name = name
        self.email = email

    def serialize(self) -> dict:
        return {"type": "user", "name": self.name, "email": self.email}


class ProductDTO(Serializable, type_name="product"):
    def __init__(self, title: str = "", price: float = 0):
        self.title = title
        self.price = price

    def serialize(self) -> dict:
        return {"type": "product", "title": self.title, "price": self.price}


# Deserializare dinamică:
obj = Serializable.from_type("user", name="Alice", email="alice@example.com")
print(obj.serialize())  # {'type': 'user', 'name': 'Alice', 'email': 'alice@example.com'}

10. Descriptori, proprietăți și controlul accesului

10.1 Descriptori — mecanismul din spatele property

class Validated:
    """Descriptor generic cu validare."""

    def __init__(self, validator=None, default=None):
        self.validator = validator
        self.default = default

    def __set_name__(self, owner, name):
        """Apelat automat la definirea clasei — știm numele atributului."""
        self.public_name = name
        self.private_name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, self.default)

    def __set__(self, obj, value):
        if self.validator:
            self.validator(self.public_name, value)
        setattr(obj, self.private_name, value)


# Validatori reutilizabili:
def positive(name, value):
    if value <= 0:
        raise ValueError(f"{name} must be positive, got {value}")

def non_empty_string(name, value):
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{name} must be a non-empty string, got {value!r}")

def in_range(min_val, max_val):
    def validator(name, value):
        if not (min_val <= value <= max_val):
            raise ValueError(f"{name} must be between {min_val} and {max_val}, got {value}")
    return validator


class Product:
    # Descriptorii sunt ATRIBUTE DE CLASĂ care controlează accesul
    name = Validated(validator=non_empty_string)
    price = Validated(validator=positive)
    rating = Validated(validator=in_range(0, 5), default=0)

    def __init__(self, name: str, price: float, rating: float = 0):
        self.name = name       # Trece prin Validated.__set__
        self.price = price
        self.rating = rating


p = Product("Widget", 29.99, 4.5)
# Product("", 29.99)          # ValueError: name must be a non-empty string
# Product("Widget", -10)      # ValueError: price must be positive
# Product("Widget", 10, 6)    # ValueError: rating must be between 0 and 5

10.2 Proprietăți compute — lazy evaluation

class LazyProperty:
    """Descriptor care calculează valoarea o singură dată, apoi o cache-uiește."""

    def __init__(self, func):
        self.func = func
        self.attr_name = f"_lazy_{func.__name__}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if not hasattr(obj, self.attr_name):
            setattr(obj, self.attr_name, self.func(obj))
        return getattr(obj, self.attr_name)


class DataAnalyzer:
    def __init__(self, data: list[float]):
        self._data = data

    @LazyProperty
    def mean(self) -> float:
        """Calculat o singură dată, apoi memorat."""
        print("Computing mean...")
        return sum(self._data) / len(self._data)

    @LazyProperty
    def std_dev(self) -> float:
        print("Computing std_dev...")
        mean = self.mean  # Reutilizează mean (deja calculat)
        variance = sum((x - mean) ** 2 for x in self._data) / len(self._data)
        return variance ** 0.5


analyzer = DataAnalyzer([1, 2, 3, 4, 5])
print(analyzer.mean)      # Computing mean... → 3.0
print(analyzer.mean)      # 3.0 (din cache, fără recalculare)
print(analyzer.std_dev)   # Computing std_dev... → 1.414...

11. Mixins, moștenire multiplă și MRO

11.1 Mixins — funcționalitate reutilizabilă

import json
from datetime import datetime

class JSONMixin:
    """Mixin care adaugă serializare/deserializare JSON."""

    def to_json(self) -> str:
        return json.dumps(self._to_dict(), default=str, indent=2)

    def _to_dict(self) -> dict:
        return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}

    @classmethod
    def from_json(cls, json_str: str):
        data = json.loads(json_str)
        return cls(**data)


class TimestampMixin:
    """Mixin care adaugă tracking temporal."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.created_at = datetime.now()
        self.updated_at = datetime.now()

    def touch(self) -> None:
        self.updated_at = datetime.now()


class ComparableMixin:
    """Mixin care adaugă comparabilitate pe baza unui câmp cheie."""

    def _compare_key(self):
        raise NotImplementedError

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self._compare_key() == other._compare_key()

    def __lt__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self._compare_key() < other._compare_key()

    def __hash__(self):
        return hash(self._compare_key())


class Article(TimestampMixin, JSONMixin, ComparableMixin):
    def __init__(self, title: str, content: str, author: str):
        self.title = title
        self.content = content
        self.author = author
        super().__init__()  # Apelează TimestampMixin.__init__

    def _compare_key(self):
        return self.title


art = Article("OOP Guide", "Content here...", "Alice")
print(art.to_json())          # JSON serializat
print(art.created_at)         # Timestamp
sorted_articles = sorted([art, Article("A Guide", "...", "Bob")])

11.2 MRO (Method Resolution Order) — algoritmul C3

class A:
    def greet(self): return "A"

class B(A):
    def greet(self): return "B"

class C(A):
    def greet(self): return "C"

class D(B, C):
    pass

# MRO determină ordinea de căutare a metodelor:
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

print(D().greet())  # "B" — prima clasă din MRO care are metoda

# Regula: copilul înaintea părintelui, ordinea din lista de moștenire păstrată
# Algoritmul C3 garantează o linearizare consistentă.

# super() urmează MRO, NU moștenirea directă:
class Base:
    def __init__(self):
        print("Base.__init__")

class Left(Base):
    def __init__(self):
        print("Left.__init__")
        super().__init__()       # Apelează Right.__init__ (nu Base!)

class Right(Base):
    def __init__(self):
        print("Right.__init__")
        super().__init__()       # Apelează Base.__init__

class Child(Left, Right):
    def __init__(self):
        print("Child.__init__")
        super().__init__()       # Apelează Left.__init__

Child()
# Output (urmează MRO: Child → Left → Right → Base):
# Child.__init__
# Left.__init__
# Right.__init__
# Base.__init__

12. Generice, tipuri și type hints avansate

from typing import TypeVar, Generic, Callable, Iterable, overload

T = TypeVar("T")
U = TypeVar("U")
Comparable = TypeVar("Comparable", bound="SupportsLessThan")

class Result(Generic[T]):
    """Monadă Result — înlocuiește excepțiile cu valori."""

    def __init__(self, value: T | None = None, error: str | None = None):
        self._value = value
        self._error = error

    @classmethod
    def ok(cls, value: T) -> "Result[T]":
        return cls(value=value)

    @classmethod
    def fail(cls, error: str) -> "Result[T]":
        return cls(error=error)

    @property
    def is_ok(self) -> bool:
        return self._error is None

    def unwrap(self) -> T:
        if self._error:
            raise ValueError(f"Called unwrap on error: {self._error}")
        return self._value

    def unwrap_or(self, default: T) -> T:
        return self._value if self.is_ok else default

    def map(self, func: Callable[[T], U]) -> "Result[U]":
        """Aplică funcția doar dacă e Ok, propagă eroarea altfel."""
        if self.is_ok:
            try:
                return Result.ok(func(self._value))
            except Exception as e:
                return Result.fail(str(e))
        return Result.fail(self._error)

    def flat_map(self, func: Callable[[T], "Result[U]"]) -> "Result[U]":
        if self.is_ok:
            return func(self._value)
        return Result.fail(self._error)

    def __repr__(self) -> str:
        if self.is_ok:
            return f"Ok({self._value!r})"
        return f"Fail({self._error!r})"


# Utilizare — composability fără excepții:
def parse_int(s: str) -> Result[int]:
    try:
        return Result.ok(int(s))
    except ValueError:
        return Result.fail(f"Cannot parse '{s}' as int")

def divide(a: int, b: int) -> Result[float]:
    if b == 0:
        return Result.fail("Division by zero")
    return Result.ok(a / b)

# Pipeline curat, fără try/except:
result = (
    parse_int("42")
    .flat_map(lambda n: divide(100, n))
    .map(lambda x: round(x, 2))
)
print(result)  # Ok(2.38)

result2 = parse_int("abc").map(lambda n: n * 2)
print(result2)  # Fail("Cannot parse 'abc' as int")

13. Dependency Injection și Inversion of Control

from typing import TypeVar, Callable, Any
import inspect

T = TypeVar("T")

class Container:
    """IoC Container simplu — gestionează dependențele și le injectează."""

    def __init__(self):
        self._factories: dict[type, Callable] = {}
        self._singletons: dict[type, Any] = {}
        self._singleton_types: set[type] = set()

    def register(self, interface: type, factory: Callable,
                 singleton: bool = False) -> None:
        self._factories[interface] = factory
        if singleton:
            self._singleton_types.add(interface)

    def register_instance(self, interface: type, instance: Any) -> None:
        """Înregistrează o instanță existentă (mereu singleton)."""
        self._singletons[interface] = instance
        self._singleton_types.add(interface)

    def resolve(self, interface: type[T]) -> T:
        """Rezolvă o dependență, injectând recursiv sub-dependențele."""
        # Verifică singleton cache
        if interface in self._singletons:
            return self._singletons[interface]

        factory = self._factories.get(interface)
        if not factory:
            raise KeyError(f"No registration for {interface}")

        # Auto-injection: inspectăm parametrii factory-ului
        sig = inspect.signature(factory)
        kwargs = {}
        for param_name, param in sig.parameters.items():
            if param.annotation != inspect.Parameter.empty:
                try:
                    kwargs[param_name] = self.resolve(param.annotation)
                except KeyError:
                    if param.default != inspect.Parameter.empty:
                        kwargs[param_name] = param.default
                    # else: lasă să genereze eroare la apel

        instance = factory(**kwargs)

        if interface in self._singleton_types:
            self._singletons[interface] = instance

        return instance


# === Definirea dependențelor ===
from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, msg: str) -> None: ...

class ConsoleLogger(Logger):
    def log(self, msg: str) -> None:
        print(f"[LOG] {msg}")

class Database(ABC):
    @abstractmethod
    def query(self, sql: str) -> list: ...

class PostgresDB(Database):
    def __init__(self, logger: Logger):
        self.logger = logger
    def query(self, sql: str) -> list:
        self.logger.log(f"Executing: {sql}")
        return [{"id": 1}]

class UserService:
    def __init__(self, db: Database, logger: Logger):
        self.db = db
        self.logger = logger
    def get_user(self, uid: int) -> dict:
        self.logger.log(f"Getting user {uid}")
        return self.db.query(f"SELECT * FROM users WHERE id={uid}")[0]


# === Configurare container ===
container = Container()
container.register(Logger, ConsoleLogger, singleton=True)
container.register(Database, PostgresDB, singleton=True)
container.register(UserService, UserService)

# === Rezolvare (auto-injection!) ===
service = container.resolve(UserService)
# Container rezolvă automat:
# UserService(db=PostgresDB(logger=ConsoleLogger()), logger=ConsoleLogger())

user = service.get_user(42)
# [LOG] Getting user 42
# [LOG] Executing: SELECT * FROM users WHERE id=42

14. Domain-Driven Design (DDD) — noțiuni

14.1 Entități, Value Objects și Agregări

from dataclasses import dataclass, field
from uuid import uuid4
from datetime import datetime
from enum import Enum, auto

# === Value Object (imutabil, egal pe baza valorii) ===
@dataclass(frozen=True)
class Address:
    street: str
    city: str
    zip_code: str
    country: str = "Romania"

# Două adrese cu aceleași valori sunt EGALE
addr1 = Address("Str. Libertății 1", "București", "010101")
addr2 = Address("Str. Libertății 1", "București", "010101")
assert addr1 == addr2  # True — egalitate structurală

# === Entity (identitate unică, egal pe baza ID-ului) ===
class OrderStatus(Enum):
    DRAFT = auto()
    CONFIRMED = auto()
    SHIPPED = auto()
    DELIVERED = auto()
    CANCELLED = auto()

@dataclass
class OrderLine:
    """Value Object — parte a agregării Order."""
    product_id: str
    product_name: str
    quantity: int
    unit_price: float

    @property
    def total(self) -> float:
        return self.quantity * self.unit_price


class Order:
    """Aggregate Root — punctul de intrare pentru întreaga agregare."""

    def __init__(self, customer_id: str, shipping_address: Address):
        self.id: str = str(uuid4())
        self.customer_id = customer_id
        self.shipping_address = shipping_address
        self.status = OrderStatus.DRAFT
        self._lines: list[OrderLine] = []
        self.created_at = datetime.now()
        self._events: list = []  # Domain events

    @property
    def lines(self) -> tuple[OrderLine, ...]:
        """Expune liniile ca tuple imutabilă (protejare invarianți)."""
        return tuple(self._lines)

    @property
    def total(self) -> float:
        return sum(line.total for line in self._lines)

    # === Comenzi (modifică starea, protejează invarianții) ===
    def add_line(self, product_id: str, name: str, qty: int, price: float) -> None:
        if self.status != OrderStatus.DRAFT:
            raise DomainError("Can only modify draft orders")
        if qty <= 0:
            raise DomainError("Quantity must be positive")
        self._lines.append(OrderLine(product_id, name, qty, price))

    def confirm(self) -> None:
        if self.status != OrderStatus.DRAFT:
            raise DomainError(f"Cannot confirm order in {self.status} state")
        if not self._lines:
            raise DomainError("Cannot confirm empty order")
        self.status = OrderStatus.CONFIRMED
        self._events.append(OrderConfirmed(self.id, self.total))

    def ship(self) -> None:
        if self.status != OrderStatus.CONFIRMED:
            raise DomainError("Can only ship confirmed orders")
        self.status = OrderStatus.SHIPPED
        self._events.append(OrderShipped(self.id, self.shipping_address))

    def collect_events(self) -> list:
        events = self._events.copy()
        self._events.clear()
        return events

    def __eq__(self, other):
        if not isinstance(other, Order):
            return NotImplemented
        return self.id == other.id   # Egalitate pe baza identității!

    def __hash__(self):
        return hash(self.id)


# === Domain Events ===
@dataclass(frozen=True)
class OrderConfirmed:
    order_id: str
    total: float

@dataclass(frozen=True)
class OrderShipped:
    order_id: str
    address: Address

class DomainError(Exception):
    """Excepție specifică domeniului."""
    pass

15. Testarea codului orientat pe obiecte

import pytest
from unittest.mock import Mock, patch, MagicMock

# === Unit test cu mock-uri ===
class TestUserService:
    """Testăm UserService izolat, cu dependențe mock-uite."""

    def setup_method(self):
        self.mock_repo = Mock(spec=Repository)
        self.service = UserService(self.mock_repo)

    def test_register_creates_user(self):
        self.mock_repo.list_all.return_value = []

        user = self.service.register("Alice", "alice@example.com")

        assert user.name == "Alice"
        assert user.email == "alice@example.com"
        self.mock_repo.add.assert_called_once_with(user)

    def test_register_rejects_duplicate_email(self):
        existing = User(name="Bob", email="alice@example.com")
        self.mock_repo.list_all.return_value = [existing]

        with pytest.raises(ValueError, match="already registered"):
            self.service.register("Alice", "alice@example.com")

        self.mock_repo.add.assert_not_called()

    def test_deactivate_sets_active_false(self):
        user = User(name="Alice", email="alice@example.com")
        self.mock_repo.get.return_value = user

        self.service.deactivate(user.id)

        assert user.active is False
        self.mock_repo.update.assert_called_once_with(user)

    def test_deactivate_raises_for_unknown_user(self):
        self.mock_repo.get.return_value = None

        with pytest.raises(KeyError):
            self.service.deactivate("nonexistent")


# === Test cu parametrize ===
class TestMoney:
    @pytest.mark.parametrize("amount,currency,expected", [
        (100, "RON", "100.00 RON"),
        (0, "EUR", "0.00 EUR"),
        (99.99, "USD", "99.99 USD"),
    ])
    def test_money_display(self, amount, currency, expected):
        m = Money(amount, currency)
        assert f"{m.amount:.2f} {m.currency}" == expected

    def test_money_addition(self):
        assert Money(10, "RON") + Money(20, "RON") == Money(30, "RON")

    def test_money_different_currencies_raises(self):
        with pytest.raises(ValueError, match="different currencies"):
            Money(10, "RON") + Money(20, "EUR")

    def test_money_negative_raises(self):
        with pytest.raises(ValueError):
            Money(-5, "RON")


# === Test Order (domain logic) ===
class TestOrder:
    def test_full_lifecycle(self):
        addr = Address("Str. X", "București", "010101")
        order = Order("cust-1", addr)

        order.add_line("prod-1", "Widget", 2, 10.0)
        order.add_line("prod-2", "Gadget", 1, 25.0)
        assert order.total == 45.0

        order.confirm()
        assert order.status == OrderStatus.CONFIRMED

        order.ship()
        assert order.status == OrderStatus.SHIPPED

        events = order.collect_events()
        assert len(events) == 2
        assert isinstance(events[0], OrderConfirmed)
        assert isinstance(events[1], OrderShipped)

    def test_cannot_confirm_empty_order(self):
        order = Order("cust-1", Address("X", "Y", "Z"))
        with pytest.raises(DomainError, match="empty order"):
            order.confirm()

    def test_cannot_ship_unconfirmed_order(self):
        order = Order("cust-1", Address("X", "Y", "Z"))
        order.add_line("p1", "X", 1, 10)
        with pytest.raises(DomainError, match="confirmed"):
            order.ship()

16. Anti-patterns și refactoring

16.1 Anti-patterns frecvente

# ❌ God Object — clasă care face TOTUL
class Application:
    def handle_http(self): ...
    def query_database(self): ...
    def send_email(self): ...
    def render_template(self): ...
    def validate_input(self): ...
    def process_payment(self): ...
    def generate_pdf(self): ...
    # → Împarte în clase cu responsabilități clare (SRP)

# ❌ Anemic Domain Model — entități fără comportament
class UserAnemic:
    def __init__(self):
        self.name = ""
        self.email = ""
        self.active = True
    # Doar date, nicio logică. Logica e în „service-uri" externe.
    # → Mută logica de business ÎN entitate

# ❌ Primitive Obsession — folosirea de tipuri primitive în loc de Value Objects
def create_order(customer_id: str, amount: float, currency: str, email: str):
    # Toate sunt string/float — nicio validare, ușor de confundat parametrii
    ...
# → Înlocuiește cu Money(amount, currency), Email(email), CustomerId(customer_id)

# ❌ Feature Envy — o metodă care accesează mai mult altă clasă decât pe a sa
class OrderProcessor:
    def calculate_discount(self, customer):
        if customer.orders_count > 10 and customer.total_spent > 1000:
            if customer.membership == "gold":
                return customer.total_spent * 0.1
        return 0
    # → Mută calculate_discount() în Customer

# ❌ Shotgun Surgery — o modificare necesită schimbări în 10 clase
# → Consolidează logica în locul potrivit

# ❌ Instanceof chains — polimorfism nefolosit
def process(shape):
    if isinstance(shape, Circle):
        return math.pi * shape.radius ** 2
    elif isinstance(shape, Rectangle):
        return shape.width * shape.height
    # → Adaugă metoda area() pe Shape (polimorfism)

17. Studiu de caz: sistem complet de e-commerce

"""
Arhitectură simplificată a unui sistem e-commerce
demonstrând principiile OOP, SOLID și design patterns.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum, auto
from typing import Protocol
from uuid import uuid4


# === Value Objects ===
@dataclass(frozen=True)
class Money:
    amount: float
    currency: str = "RON"
    def __add__(self, other: "Money") -> "Money":
        assert self.currency == other.currency
        return Money(round(self.amount + other.amount, 2), self.currency)
    def __mul__(self, factor: float) -> "Money":
        return Money(round(self.amount * factor, 2), self.currency)
    def __le__(self, other: "Money") -> bool:
        return self.amount <= other.amount
    @classmethod
    def zero(cls, currency: str = "RON") -> "Money":
        return cls(0.0, currency)

@dataclass(frozen=True)
class Email:
    value: str
    def __post_init__(self):
        if "@" not in self.value:
            raise ValueError(f"Invalid email: {self.value}")

# === Interfaces (Protocols) ===
class PaymentGateway(Protocol):
    def charge(self, amount: Money, token: str) -> str: ...
    def refund(self, transaction_id: str) -> bool: ...

class NotificationService(Protocol):
    def notify(self, email: Email, subject: str, body: str) -> None: ...

class InventoryService(Protocol):
    def reserve(self, product_id: str, quantity: int) -> bool: ...
    def release(self, product_id: str, quantity: int) -> None: ...

# === Domain Entities ===
class OrderStatus(Enum):
    DRAFT = auto()
    CONFIRMED = auto()
    PAID = auto()
    SHIPPED = auto()
    DELIVERED = auto()
    CANCELLED = auto()

@dataclass
class OrderLine:
    product_id: str
    product_name: str
    quantity: int
    unit_price: Money
    @property
    def subtotal(self) -> Money: return self.unit_price * self.quantity

class Order:
    def __init__(self, customer_email: Email):
        self.id = str(uuid4())
        self.customer_email = customer_email
        self.status = OrderStatus.DRAFT
        self._lines: list[OrderLine] = []
        self.payment_transaction_id: str | None = None
        self.created_at = datetime.now()

    @property
    def total(self) -> Money:
        if not self._lines:
            return Money.zero()
        return sum((line.subtotal for line in self._lines), Money.zero())

    @property
    def lines(self) -> tuple[OrderLine, ...]:
        return tuple(self._lines)

    def add_line(self, product_id: str, name: str, qty: int, price: Money) -> None:
        if self.status != OrderStatus.DRAFT:
            raise DomainError("Cannot modify non-draft order")
        self._lines.append(OrderLine(product_id, name, qty, price))

    def confirm(self) -> None:
        if not self._lines:
            raise DomainError("Cannot confirm empty order")
        self._transition(OrderStatus.DRAFT, OrderStatus.CONFIRMED)

    def mark_paid(self, transaction_id: str) -> None:
        self._transition(OrderStatus.CONFIRMED, OrderStatus.PAID)
        self.payment_transaction_id = transaction_id

    def mark_shipped(self) -> None:
        self._transition(OrderStatus.PAID, OrderStatus.SHIPPED)

    def cancel(self) -> None:
        allowed = {OrderStatus.DRAFT, OrderStatus.CONFIRMED}
        if self.status not in allowed:
            raise DomainError(f"Cannot cancel order in {self.status.name} state")
        self.status = OrderStatus.CANCELLED

    def _transition(self, expected: OrderStatus, target: OrderStatus) -> None:
        if self.status != expected:
            raise DomainError(
                f"Invalid transition: {self.status.name}{target.name}"
            )
        self.status = target


# === Application Service (orchestrare) ===
class CheckoutService:
    """Orchestrează procesul de checkout.
    Depinde de abstracții (Protocol), nu de implementări concrete."""

    def __init__(
        self,
        payment: PaymentGateway,
        inventory: InventoryService,
        notifications: NotificationService,
        order_repo: "Repository[Order]",
    ):
        self._payment = payment
        self._inventory = inventory
        self._notifications = notifications
        self._orders = order_repo

    def checkout(self, order: Order, payment_token: str) -> str:
        # 1. Validare stare
        order.confirm()

        # 2. Rezervare stocuri
        for line in order.lines:
            if not self._inventory.reserve(line.product_id, line.quantity):
                order.cancel()
                raise DomainError(f"Product {line.product_name} out of stock")

        # 3. Procesare plată
        try:
            txn_id = self._payment.charge(order.total, payment_token)
        except Exception as e:
            # Rollback inventar
            for line in order.lines:
                self._inventory.release(line.product_id, line.quantity)
            order.cancel()
            raise DomainError(f"Payment failed: {e}")

        # 4. Actualizare stare
        order.mark_paid(txn_id)
        self._orders.update(order)

        # 5. Notificare client
        self._notifications.notify(
            order.customer_email,
            "Order Confirmed",
            f"Your order {order.id} ({order.total.amount} {order.total.currency}) "
            f"has been confirmed. Transaction: {txn_id}"
        )

        return txn_id


class DomainError(Exception):
    pass

Anexe

A. Principii de design rezumate

Principiu Esență
SRP O clasă, un motiv de schimbare
OCP Extinde prin adăugare, nu prin modificare
LSP Subclasele înlocuiesc supraclasele fără surprize
ISP Interfețe mici, specifice clientului
DIP Depinde de abstracții, nu de concretizări
DRY Don’t Repeat Yourself
KISS Keep It Simple, Stupid
YAGNI You Aren’t Gonna Need It
Composition over Inherit. Preferă HAS-A față de IS-A
Tell, Don’t Ask Cere obiectului să facă, nu îi cere datele
Law of Demeter Nu vorbi cu străinii (A.B.C.do() = suspect)
Fail Fast Detectează erorile cât mai devreme
Program to Interface Depinde de ce face, nu de cum face

B. Când să folosești ce pattern

Problemă Pattern recomandat
Crearea de obiecte complexe Builder, Factory Method
Familii de obiecte legate Abstract Factory
O singură instanță globală Singleton (sau DI container)
Adăugare funcționalitate fără moștenire Decorator, Mixin
Algoritmi interschimbabili Strategy
Notificare la schimbări Observer / Event Bus
Tranziții de stare complexe State Machine
Procesare în lanț cu priorități Chain of Responsibility
Abstracție peste un subsistem complex Facade
Adaptare interfață incompatibilă Adapter
Separare abstracție de implementare Bridge
Acces controlat la un obiect Proxy
Operații pe structuri de obiecte Visitor
Salvare/restaurare stare Memento
Undo/redo Command

Curs realizat ca material de referință pentru dezvoltatori care doresc să stăpânească principiile OOP, design patterns și arhitectura software modernă.

Pe această pagină