Python Designmönster för ren arkitektur
Bygg underhållbara Python-applikationer med SOLID-designmönster
Rent arkitektur har revolutionerat hur utvecklare bygger skalbara, underhållbara applikationer genom att betona separation av ansvar och beroendestyrning.
I Python kombineras dessa principer med språkets dynamiska natur för att skapa flexibla, testbara system som utvecklas med affärskrav utan att bli teknisk skuld.

Förstå ren arkitektur i Python
Ren arkitektur, introducerad av Robert C. Martin (Uncle Bob), organiserar programvara i koncentriska lager där beroenden pekar inåt mot kärnbusinesslogik. Denna arkitekturmodell säkerställer att dina applikations kritiska affärsregler förblir oberoende av ramverk, databaser och externa tjänster.
Grundläggande filosofi
Den grundläggande principen är enkel men kraftfull: affärslogik ska inte bero av infrastruktur. Dina domänentiteter, användningsfall och affärsregler ska fungera oavsett om du använder PostgreSQL eller MongoDB, FastAPI eller Flask, AWS eller Azure.
I Python passar denna filosofi perfekt med språkets “duck typing” och protokollorienterad programmering, vilket möjliggör ren separation utan den ceremonin som krävs i statiskt typade språk.
De fyra lagren i ren arkitektur
Entitetslager (Domän): Rena affärsobjekt med företagsövergripande affärsregler. Dessa är POJOs (Plain Old Python Objects) utan externa beroenden.
Användningsfallslager (Applikation): Applikationsspecifika affärsregler som orkestrerar datatillflödet mellan entiteter och externa tjänster.
Gränssnittsanpassningslager: Konverterar data mellan det format som är mest lämpligt för användningsfall och entiteter, och det format som krävs av externa enheter.
Ramverk & drivrutiner: Alla externa detaljer som databaser, webramverk och externa API:er.
SOLID-principer i Python
SOLID-principerna utgör grunden för ren arkitektur. Låt oss utforska hur varje princip manifesteras i Python. För en omfattande översikt över designmönster i Python, se Python Design Patterns Guide.
Single Responsibility Principle (SRP)
Varje klass ska ha en anledning att ändras:
# Dåligt: Flera ansvar
class UserManager:
def create_user(self, user_data):
# Skapa användare
pass
def send_welcome_email(self, user):
# Skicka e-post
pass
def log_creation(self, user):
# Logga till fil
pass
# Bra: Separerade ansvar
class UserService:
def __init__(self, repository, email_service, logger):
self.repository = repository
self.email_service = email_service
self.logger = logger
def create_user(self, user_data):
user = User(**user_data)
self.repository.save(user)
self.email_service.send_welcome(user)
self.logger.info(f"Användare skapad: {user.id}")
return user
Open/Closed Principle (OCP)
Programentiteter ska vara öppna för utökning men stängda för modifiering:
from abc import ABC, abstractmethod
from typing import Protocol
# Användning av Protocol (Python 3.8+)
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Kreditkortlogik
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# PayPal-logik
return True
# Lätt att utöka utan att modifiera befintlig kod
class CryptoProcessor:
def process_payment(self, amount: float) -> bool:
# Kryptovalutalogik
return True
Liskov Substitution Principle (LSP)
Objekt ska kunna ersättas med sina undertyper utan att programmet bryts:
from abc import ABC, abstractmethod
class DataStore(ABC):
@abstractmethod
def save(self, key: str, value: str) -> None:
pass
@abstractmethod
def get(self, key: str) -> str:
pass
class PostgreSQLStore(DataStore):
def save(self, key: str, value: str) -> None:
# PostgreSQL-implementering
pass
def get(self, key: str) -> str:
# PostgreSQL-implementering
return ""
class RedisStore(DataStore):
def save(self, key: str, value: str) -> None:
# Redis-implementering
pass
def get(self, key: str) -> str:
# Redis-implementering
return ""
# Båda kan användas utbytbart
def process_data(store: DataStore, key: str, value: str):
store.save(key, value)
return store.get(key)
Interface Segregation Principle (ISP)
Klienter ska inte tvingas bero på gränssnitt de inte använder:
# Dåligt: Fett gränssnitt
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
@abstractmethod
def sleep(self): pass
# Bra: Segregerade gränssnitt
class Workable(Protocol):
def work(self) -> None: ...
class Eatable(Protocol):
def eat(self) -> None: ...
class Human:
def work(self) -> None:
print("Arbetar")
def eat(self) -> None:
print("Äter")
class Robot:
def work(self) -> None:
print("Arbetar")
# Inget ätmetod behövs
Dependency Inversion Principle (DIP)
Högre nivåmoduler ska inte bero på lågnivåmoduler. Båda ska bero på abstraktioner:
from typing import Protocol
# Abstraktion
class EmailSender(Protocol):
def send(self, to: str, subject: str, body: str) -> None:
...
# Lågnivåmodul
class SMTPEmailSender:
def send(self, to: str, subject: str, body: str) -> None:
# SMTP-implementering
pass
# Högre nivåmodul beroende av abstraktion
class UserRegistrationService:
def __init__(self, email_sender: EmailSender):
self.email_sender = email_sender
def register(self, email: str, name: str):
# Registreringslogik
self.email_sender.send(
to=email,
subject="Välkommen!",
body=f"Hej {name}"
)
Repository-mönster: Abstrahering av dataåtkomst
Repository-mönstret ger ett samlingsliknande gränssnitt för att komma åt domänobjekt, döljer detaljerna om datalagring.
Grundläggande repository-implementering
from abc import ABC, abstractmethod
from typing import List, Optional
from dataclasses import dataclass
from uuid import UUID, uuid4
@dataclass
class User:
id: UUID
email: str
name: str
is_active: bool = True
class UserRepository(ABC):
@abstractmethod
def save(self, user: User) -> User:
pass
@abstractmethod
def get_by_id(self, user_id: UUID) -> Optional[User]:
pass
@abstractmethod
def get_by_email(self, email: str) -> Optional[User]:
pass
@abstractmethod
def list_all(self) -> List[User]:
pass
@abstractmethod
def delete(self, user_id: UUID) -> bool:
pass
SQLAlchemy-implementering
from sqlalchemy import create_engine, Column, String, Boolean
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
Base = declarative_base()
class UserModel(Base):
__tablename__ = 'users'
id = Column(PGUUID(as_uuid=True), primary_key=True)
email = Column(String, unique=True, nullable=False)
name = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
class SQLAlchemyUserRepository(UserRepository):
def __init__(self, session: Session):
self.session = session
def save(self, user: User) -> User:
user_model = UserModel(
id=user.id,
email=user.email,
name=user.name,
is_active=user.is_active
)
self.session.add(user_model)
self.session.commit()
return user
def get_by_id(self, user_id: UUID) -> Optional[User]:
user_model = self.session.query(UserModel).filter(
UserModel.id == user_id
).first()
if not user_model:
return None
return User(
id=user_model.id,
email=user_model.email,
name=user_model.name,
is_active=user_model.is_active
)
def get_by_email(self, email: str) -> Optional[User]:
user_model = self.session.query(UserModel).filter(
UserModel.email == email
).first()
if not user_model:
return None
return User(
id=user_model.id,
email=user_model.email,
name=user_model.name,
is_active=user_model.is_active
)
def list_all(self) -> List[User]:
users = self.session.query(UserModel).all()
return [
User(
id=u.id,
email=u.email,
name=u.name,
is_active=u.is_active
)
for u in users
]
def delete(self, user_id: UUID) -> bool:
result = self.session.query(UserModel).filter(
UserModel.id == user_id
).delete()
self.session.commit()
return result > 0
Minnesintern repository för testning
class InMemoryUserRepository(UserRepository):
def __init__(self):
self.users: dict[UUID, User] = {}
def save(self, user: User) -> User:
self.users[user.id] = user
return user
def get_by_id(self, user_id: UUID) -> Optional[User]:
return self.users.get(user_id)
def get_by_email(self, email: str) -> Optional[User]:
for user in self.users.values():
if user.email == email:
return user
return None
def list_all(self) -> List[User]:
return list(self.users.values())
def delete(self, user_id: UUID) -> bool:
if user_id in self.users:
del self.users[user_id]
return True
return False
Service Layer: Orchestrerar Affärslogik
Service Layer implementerar användningsfall och orkestrerar flödet mellan lagringsplatser, externa tjänster och domänlogik.
from typing import Optional
from uuid import uuid4
class UserAlreadyExistsError(Exception):
pass
class UserNotFoundError(Exception):
pass
class UserService:
def __init__(
self,
user_repository: UserRepository,
email_service: EmailSender,
event_publisher: 'EventPublisher'
):
self.user_repository = user_repository
self.email_service = email_service
self.event_publisher = event_publisher
def register_user(self, email: str, name: str) -> User:
# Kontrollera om användare finns
existing_user = self.user_repository.get_by_email(email)
if existing_user:
raise UserAlreadyExistsError(f"Användare med e-post {email} finns redan")
# Skapa ny användare
user = User(
id=uuid4(),
email=email,
name=name,
is_active=True
)
# Spara i lagring
user = self.user_repository.save(user)
# Skicka välkomstmail
self.email_service.send(
to=user.email,
subject="Välkommen!",
body=f"Hej {user.name}, välkommen till vår plattform!"
)
# Publicera händelse
self.event_publisher.publish('user.registered', {
'user_id': str(user.id),
'email': user.email
})
return user
def deactivate_user(self, user_id: UUID) -> User:
user = self.user_repository.get_by_id(user_id)
if not user:
raise UserNotFoundError(f"Användare {user_id} hittades inte")
user.is_active = False
user = self.user_repository.save(user)
self.event_publisher.publish('user.deactivated', {
'user_id': str(user.id)
})
return user
Beroendeinjektion i Python
Pythons dynamiska natur gör beroendeinjektion enkelt utan att kräva tunga ramverk.
Konstruktorinjektion
class OrderService:
def __init__(
self,
order_repository: 'OrderRepository',
payment_processor: PaymentProcessor,
notification_service: 'NotificationService'
):
self.order_repository = order_repository
self.payment_processor = payment_processor
self.notification_service = notification_service
def place_order(self, order_data: dict):
# Använd injicerade beroenden
pass
Enkel beroendekontainer
from typing import Dict, Type, Callable, Any
class Container:
def __init__(self):
self._services: Dict[Type, Callable] = {}
self._singletons: Dict[Type, Any] = {}
def register(self, interface: Type, factory: Callable):
self._services[interface] = factory
def register_singleton(self, interface: Type, instance: Any):
self._singletons[interface] = instance
def resolve(self, interface: Type):
if interface in self._singletons:
return self._singletons[interface]
factory = self._services.get(interface)
if factory:
return factory(self)
raise ValueError(f"Ingen registrering hittad för {interface}")
# Användning
def create_container() -> Container:
container = Container()
# Registrera tjänster
container.register_singleton(
Session,
sessionmaker(bind=create_engine('postgresql://...'))()
)
container.register(
UserRepository,
lambda c: SQLAlchemyUserRepository(c.resolve(Session))
)
container.register(
EmailSender,
lambda c: SMTPEmailSender()
)
container.register(
UserService,
lambda c: UserService(
c.resolve(UserRepository),
c.resolve(EmailSender),
c.resolve(EventPublisher)
)
)
return container
Hexagonal Arkitektur (Portar och Anpassare)
Hexagonal Arkitektur placerar affärslogik i centrum med anpassare som hanterar extern kommunikation.
Definiera Portar (Gränssnitt)
# Input Port (Primär)
class CreateUserUseCase(Protocol):
def execute(self, request: 'CreateUserRequest') -> 'CreateUserResponse':
...
# Output Port (Sekundär)
class UserPersistencePort(Protocol):
def save(self, user: User) -> User:
...
def find_by_email(self, email: str) -> Optional[User]:
...
Implementera Anpassare
from pydantic import BaseModel, EmailStr
# Input Anpassare (REST API)
from fastapi import FastAPI, Depends, HTTPException
class CreateUserRequest(BaseModel):
email: EmailStr
name: str
class CreateUserResponse(BaseModel):
id: str
email: str
name: str
app = FastAPI()
@app.post("/users", response_model=CreateUserResponse)
def create_user(
request: CreateUserRequest,
user_service: UserService = Depends(get_user_service)
):
try:
user = user_service.register_user(
email=request.email,
name=request.name
)
return CreateUserResponse(
id=str(user.id),
email=user.email,
name=user.name
)
except UserAlreadyExistsError as e:
raise HTTPException(status_code=400, detail=str(e))
# Output Anpassare (Databas)
# Redan implementerad som SQLAlchemyUserRepository
Domänorienterade Designmönster
Värdeobjekt
Oföränderliga objekt definierade av sina attribut:
from dataclasses import dataclass
from typing import Pattern
import re
@dataclass(frozen=True)
class Email:
value: str
EMAIL_PATTERN: Pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
def __post_init__(self):
if not self.EMAIL_PATTERN.match(self.value):
raise ValueError(f"Ogiltig e-post: {self.value}")
def __str__(self):
return self.value
@dataclass(frozen=True)
class Money:
amount: float
currency: str
def __post_init__(self):
if self.amount < 0:
raise ValueError("Belopp kan inte vara negativt")
if self.currency not in ['USD', 'EUR', 'GBP']:
raise ValueError(f"Ostödd valuta: {self.currency}")
def add(self, other: 'Money') -> 'Money':
if self.currency != other.currency:
raise ValueError("Kan inte lägga till olika valutor")
return Money(self.amount + other.amount, self.currency)
Aggregat
Kluster av domänobjekt behandlade som en enda enhet:
from dataclasses import dataclass, field
from typing import List
from datetime import datetime
@dataclass
class OrderItem:
product_id: UUID
quantity: int
price: Money
def total(self) -> Money:
return Money(
self.price.amount * self.quantity,
self.price.currency
)
@dataclass
class Order:
id: UUID
customer_id: UUID
items: List[OrderItem] = field(default_factory=list)
status: str = "pending"
created_at: datetime = field(default_factory=datetime.now)
def add_item(self, product_id: UUID, quantity: int, price: Money):
item = OrderItem(product_id, quantity, price)
self.items.append(item)
def remove_item(self, product_id: UUID):
self.items = [
item for item in self.items
if item.product_id != product_id
]
def total(self) -> Money:
if not self.items:
return Money(0, "USD")
return sum(
(item.total() for item in self.items),
Money(0, self.items[0].price.currency)
)
def confirm(self):
if not self.items:
raise ValueError("Kan inte bekräfta tom order")
if self.status != "pending":
raise ValueError("Order redan bearbetad")
self.status = "confirmed"
Domänhändelser
Domänhändelser möjliggör lös koppling mellan komponenter och stöder händelsedrivna arkitekturer. För produktionsskaliga händelsedrivna system, överväg att implementera händelseflöden med tjänster som AWS Kinesis - se Bygga händelsedrivna mikrotjänster med AWS Kinesis för en detaljerad guide.
from dataclasses import dataclass
from datetime import datetime
from typing import List, Callable
@dataclass
class DomainEvent:
occurred_at: datetime = field(default_factory=datetime.now)
@dataclass
class OrderConfirmed(DomainEvent):
order_id: UUID
customer_id: UUID
total: Money
class EventPublisher:
def __init__(self):
self._handlers: Dict[Type, List[Callable]] = {}
def subscribe(self, event_type: Type, handler: Callable):
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler)
def publish(self, event: DomainEvent):
event_type = type(event)
handlers = self._handlers.get(event_type, [])
for handler in handlers:
handler(event)
Moderna Python-funktioner för ren arkitektur
Python’s moderna funktioner gör det mer elegant och typ-säkert att implementera ren arkitektur. Om du behöver en snabb referens för Python-syntax och funktioner, kolla in Python Cheatsheet.
Typhjälpmedel och Protokoll
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict:
...
@classmethod
def from_dict(cls, data: dict) -> 'Serializable':
...
def serialize(obj: Serializable) -> dict:
return obj.to_dict()
Pydantic för validering
from pydantic import BaseModel, Field, validator
from typing import Optional
class CreateUserDTO(BaseModel):
email: EmailStr
name: str = Field(..., min_length=2, max_length=100)
age: Optional[int] = Field(None, ge=0, le=150)
@validator('name')
def name_must_not_contain_numbers(cls, v):
if any(char.isdigit() for char in v):
raise ValueError('Name cannot contain numbers')
return v
class Config:
frozen = True # Make immutable
Async/Await för I/O-operationer
Python’s async/await-syntax är särskilt kraftfull för I/O-bundna operationer i ren arkitektur, vilket tillåter icke-blockerande interaktioner med databaser och externa tjänster. När du distribuerar Python-applikationer till serverlösa plattformar blir det viktigt att förstå prestandaegenskaper - se AWS lambda performance: JavaScript vs Python vs Golang för insikter om hur du optimerar Python-serverlösa funktioner.
from typing import List
import asyncio
class AsyncUserRepository(ABC):
@abstractmethod
async def save(self, user: User) -> User:
pass
@abstractmethod
async def get_by_id(self, user_id: UUID) -> Optional[User]:
pass
class AsyncUserService:
def __init__(self, repository: AsyncUserRepository):
self.repository = repository
async def register_user(self, email: str, name: str) -> User:
user = User(id=uuid4(), email=email, name=name)
return await self.repository.save(user)
async def get_users_batch(self, user_ids: List[UUID]) -> List[User]:
tasks = [self.repository.get_by_id(uid) for uid in user_ids]
results = await asyncio.gather(*tasks)
return [u for u in results if u is not None]
Projektstruktur - bästa praxis
Rätt projektorganisation är avgörande för att upprätthålla ren arkitektur. Innan du sätter upp din projektstruktur, se till att du använder Python-virtualmiljöer för beroendeisolering. venv Cheatsheet täcker allt du behöver veta om att hantera virtualmiljöer. För moderna Python-projekt, överväg att använda uv - New Python Package, Project, and Environment Manager, som erbjuder snabbare pakethantering och projektinställning.
my_application/
├── domain/ # Företagsregler
│ ├── __init__.py
│ ├── entities/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── order.py
│ ├── value_objects/
│ │ ├── __init__.py
│ │ ├── email.py
│ │ └── money.py
│ ├── events/
│ │ ├── __init__.py
│ │ └── user_events.py
│ └── exceptions.py
│
├── application/ # Applikationsregler
│ ├── __init__.py
│ ├── use_cases/
│ │ ├── __init__.py
│ │ ├── create_user.py
│ │ └── place_order.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── user_service.py
│ └── ports/
│ ├── __init__.py
│ ├── repositories.py
│ └── external_services.py
│
├── infrastructure/ # Externa gränssnitt
│ ├── __init__.py
│ ├── persistence/
│ │ ├── __init__.py
│ │ ├── sqlalchemy/
│ │ │ ├── models.py
│ │ │ └── repositories.py
│ │ └── mongodb/
│ │ └── repositories.py
│ ├── messaging/
│ │ ├── __init__.py
│ │ └── rabbitmq_publisher.py
│ ├── external_services/
│ │ ├── __init__.py
│ │ └── email_service.py
│ └── config.py
│
├── presentation/ # UI/API-lager
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── dependencies.py
│ │ ├── routes/
│ │ │ ├── __init__.py
│ │ │ ├── users.py
│ │ │ └── orders.py
│ │ └── schemas/
│ │ ├── __init__.py
│ │ └── user_schemas.py
│ └── cli/
│ └── commands.py
│
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
│
├── main.py # Applikationens ingångspunkt
├── container.py # Beroendeinjektion
├── pyproject.toml
└── README.md
Testning av ren arkitektur
Enhetstestning av domänlogik
import pytest
from uuid import uuid4
def test_user_creation():
user = User(
id=uuid4(),
email="test@example.com",
name="Test User"
)
assert user.email == "test@example.com"
assert user.is_active is True
def test_order_total_calculation():
order = Order(id=uuid4(), customer_id=uuid4())
order.add_item(
uuid4(),
quantity=2,
price=Money(10.0, "USD")
)
order.add_item(
uuid4(),
quantity=1,
price=Money(5.0, "USD")
)
assert order.total().amount == 25.0
Integrationstestning med repository
@pytest.fixture
def in_memory_repository():
return InMemoryUserRepository()
def test_user_repository_save_and_retrieve(in_memory_repository):
user = User(
id=uuid4(),
email="test@example.com",
name="Test User"
)
saved_user = in_memory_repository.save(user)
retrieved_user = in_memory_repository.get_by_id(user.id)
assert retrieved_user is not None
assert retrieved_user.email == user.email
Testning av servicelager
from unittest.mock import Mock
def test_user_registration():
# Arrange
mock_repository = Mock(spec=UserRepository)
mock_repository.get_by_email.return_value = None
mock_repository.save.return_value = User(
id=uuid4(),
email="test@example.com",
name="Test"
)
mock_email = Mock(spec=EmailSender)
mock_events = Mock(spec=EventPublisher)
service = UserService(mock_repository, mock_email, mock_events)
# Act
user = service.register_user("test@example.com", "Test")
# Assert
assert user.email == "test@example.com"
mock_repository.save.assert_called_once()
mock_email.send.assert_called_once()
mock_events.publish.assert_called_once()
Vanliga fallgropar och hur man undviker dem
Överdimensionering
Implementera inte ren arkitektur för enkla CRUD-applikationer. Börja enkelt och refaktorera när komplexiteten ökar.
Läckande abstraktioner
Se till att domänentiteter inte innehåller databasannotationer eller ramverksspecifik kod:
# Dåligt
from sqlalchemy import Column
@dataclass
class User:
id: Column(Integer, primary_key=True) # Ramverk läcker in i domänen
# Bra
@dataclass
class User:
id: UUID # Ren domänobjekt
Cirkulära beroenden
Använd beroendeinjektion och gränssnitt för att bryta cirkulära beroenden mellan lagren.
Att ignorera kontext
Ren arkitektur är inte en passform för alla. Anpassa lagrets strikthet baserat på projektets storlek och teamets expertis.
Användbara länkar
- Clean Architecture av Robert C. Martin
- Python Type Hints Documentation
- Pydantic Documentation
- FastAPI Official Docs
- SQLAlchemy ORM Documentation
- Dependency Injector Library
- Domain-Driven Design Reference
- Architecture Patterns with Python
- Martin Fowler’s Blog on Architecture
- Python Design Patterns Guide
- Python Cheatsheet
- venv Cheatsheet
- uv - New Python Package, Project, and Environment Manager
- AWS lambda performance: JavaScript vs Python vs Golang
- Building Event-Driven Microservices with AWS Kinesis