Programare Obiect-Orientata
Curs Avansat de Programare Orientată pe Obiecte¶
Principii, Design Patterns și Arhitectură — cu exemple în Python¶
Cuprins¶
- Fundamente OOP — dincolo de baze
- Clasele în Python — mecanisme avansate
- Principiile SOLID
- Compoziție vs. moștenire
- Clase abstracte, protocoale și interfețe
- Design Patterns — Creaționale
- Design Patterns — Structurale
- Design Patterns — Comportamentale
- Metaprogramare și metaclase
- Descriptori, proprietăți și controlul accesului
- Mixins, moștenire multiplă și MRO
- Generice, tipuri și type hints avansate
- Dependency Injection și Inversion of Control
- Domain-Driven Design (DDD) — noțiuni
- Testarea codului orientat pe obiecte
- Anti-patterns și refactoring
- 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ă.