Polanya Desain Python untuk Arsitektur Bersih

Bangun aplikasi Python yang dapat dipelihara dengan pola desain SOLID

Clean Architecture telah merevolusi cara pengembang membangun aplikasi yang skalabel dan dapat dipelihara dengan menekankan pemisahan kepentingan dan manajemen ketergantungan.

Di Python, prinsip-prinsip ini berpadu dengan sifat dinamis bahasa untuk menciptakan sistem yang fleksibel dan dapat diuji yang berkembang sesuai dengan kebutuhan bisnis tanpa menjadi utang teknis.

vibrant tech conference hall

Memahami Clean Architecture dalam Python

Clean Architecture, yang diperkenalkan oleh Robert C. Martin (Uncle Bob), mengorganisasi perangkat lunak menjadi lapisan-lapisan konsentris di mana ketergantungan menunjuk ke dalam logika bisnis inti. Pola arsitektur ini memastikan bahwa aturan bisnis kritis aplikasi tetap independen dari kerangka kerja, database, dan layanan eksternal.

Filosofi Inti

Prinsip dasar sederhana namun kuat: logika bisnis tidak boleh bergantung pada infrastruktur. Entitas domain, kasus penggunaan, dan aturan bisnis Anda harus berfungsi tanpa peduli apakah Anda menggunakan PostgreSQL atau MongoDB, FastAPI atau Flask, AWS atau Azure.

Di Python, filosofi ini sangat cocok dengan sifat “duck typing” dan pemrograman berbasis protokol dari bahasa, memungkinkan pemisahan bersih tanpa upacara yang diperlukan dalam bahasa bertipe statis.

Empat Lapisan Clean Architecture

Lapisan Entitas (Domain): Objek bisnis murni dengan aturan bisnis yang berlaku di seluruh perusahaan. Ini adalah POJO (Plain Old Python Objects) tanpa ketergantungan eksternal.

Lapisan Kasus Penggunaan (Aplikasi): Aturan bisnis spesifik aplikasi yang mengatur alur data antara entitas dan layanan eksternal.

Lapisan Penyesuaian Antarmuka: Mengubah data antara format yang paling nyaman untuk kasus penggunaan dan entitas, dan format yang diperlukan oleh pihak eksternal.

Lapisan Kerangka Kerja & Pengemudi: Semua detail eksternal seperti database, kerangka kerja web, dan API eksternal.

Prinsip SOLID dalam Python

Prinsip SOLID membentuk fondasi dari arsitektur bersih. Mari kita eksplorasi bagaimana setiap prinsip muncul dalam Python. Untuk tinjauan menyeluruh tentang pola desain dalam Python, lihat Panduan Pola Desain Python.

Prinsip Tanggung Jawab Tunggal (SRP)

Setiap kelas harus memiliki satu alasan untuk berubah:

# Buruk: Banyak tanggung jawab
class UserManager:
    def create_user(self, user_data):
        # Membuat pengguna
        pass
    
    def send_welcome_email(self, user):
        # Mengirim email
        pass
    
    def log_creation(self, user):
        # Mencatat ke file
        pass

# Baik: Tanggung jawab terpisah
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"Pengguna dibuat: {user.id}")
        return user

Prinsip Terbuka/Tertutup (OCP)

Entitas perangkat lunak harus terbuka untuk ekstensi tetapi tertutup untuk modifikasi:

from abc import ABC, abstractmethod
from typing import Protocol

# Menggunakan Protocol (Python 3.8+)
class PaymentProcessor(Protocol):
    def process_payment(self, amount: float) -> bool:
        ...

class CreditCardProcessor:
    def process_payment(self, amount: float) -> bool:
        # Logika kartu kredit
        return True

class PayPalProcessor:
    def process_payment(self, amount: float) -> bool:
        # Logika PayPal
        return True

# Mudah diperluas tanpa memodifikasi kode yang ada
class CryptoProcessor:
    def process_payment(self, amount: float) -> bool:
        # Logika kriptocurrency
        return True

Prinsip Substitusi Liskov (LSP)

Objek harus dapat diganti dengan subtipenya tanpa merusak program:

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:
        # Implementasi PostgreSQL
        pass
    
    def get(self, key: str) -> str:
        # Implementasi PostgreSQL
        return ""

class RedisStore(DataStore):
    def save(self, key: str, value: str) -> None:
        # Implementasi Redis
        pass
    
    def get(self, key: str) -> str:
        # Implementasi Redis
        return ""

# Keduanya dapat digunakan secara bergantian
def process_data(store: DataStore, key: str, value: str):
    store.save(key, value)
    return store.get(key)

Prinsip Pemisahan Antarmuka (ISP)

Klien tidak boleh dipaksa bergantung pada antarmuka yang tidak mereka gunakan:

# Buruk: Antarmuka yang lebar
class Worker(ABC):
    @abstractmethod
    def work(self): pass
    
    @abstractmethod
    def eat(self): pass
    
    @abstractmethod
    def sleep(self): pass

# Baik: Antarmuka yang terpisah
class Workable(Protocol):
    def work(self) -> None: ...

class Eatable(Protocol):
    def eat(self) -> None: ...

class Human:
    def work(self) -> None:
        print("Bekerja")
    
    def eat(self) -> None:
        print("Makan")

class Robot:
    def work(self) -> None:
        print("Bekerja")
    # Metode makan tidak diperlukan

Prinsip Inversi Ketergantungan (DIP)

Modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah. Keduanya harus bergantung pada abstraksi:

from typing import Protocol

# Abstraksi
class EmailSender(Protocol):
    def send(self, to: str, subject: str, body: str) -> None:
        ...

# Modul tingkat rendah
class SMTPEmailSender:
    def send(self, to: str, subject: str, body: str) -> None:
        # Implementasi SMTP
        pass

# Modul tingkat tinggi bergantung pada abstraksi
class UserRegistrationService:
    def __init__(self, email_sender: EmailSender):
        self.email_sender = email_sender
    
    def register(self, email: str, name: str):
        # Logika pendaftaran
        self.email_sender.send(
            to=email,
            subject="Selamat Datang!",
            body=f"Hello {name}"
        )

Pola Repository: Mengabstraksi Akses Data

Pola Repository menyediakan antarmuka seperti koleksi untuk mengakses objek domain, menyembunyikan detail penyimpanan data.

Implementasi Repository Dasar

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

Implementasi SQLAlchemy

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

Repository In-Memory untuk Pengujian

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

Lapisan Layanan: Mengatur Logika Bisnis

Lapisan Layanan mengimplementasikan kasus penggunaan dan mengatur alur antara repositori, layanan eksternal, dan logika domain.

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:
        # Memeriksa apakah pengguna sudah ada
        existing_user = self.user_repository.get_by_email(email)
        if existing_user:
            raise UserAlreadyExistsError(f"Pengguna dengan email {email} sudah ada")
        
        # Membuat pengguna baru
        user = User(
            id=uuid4(),
            email=email,
            name=name,
            is_active=True
        )
        
        # Menyimpan ke repositori
        user = self.user_repository.save(user)
        
        # Mengirim email selamat datang
        self.email_service.send(
            to=user.email,
            subject="Selamat Datang!",
            body=f"Hello {user.name}, selamat datang di platform kami!"
        )
        
        # Mem-publish event
        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"Pengguna {user_id} tidak ditemukan")
        
        user.is_active = False
        user = self.user_repository.save(user)
        
        self.event_publisher.publish('user.deactivated', {
            'user_id': str(user.id)
        })
        
        return user

Injeksi Ketergantungan dalam Python

Sifat dinamis Python membuat injeksi ketergantungan menjadi sederhana tanpa memerlukan kerangka kerja berat.

Injeksi Konstruktor

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):
        # Menggunakan ketergantungan yang diinjeksikan
        pass

Kontainer Ketergantungan Sederhana

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"Tidak ada pendaftaran ditemukan untuk {interface}")

# Penggunaan
def create_container() -> Container:
    container = Container()
    
    # Mendaftarkan layanan
    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

Arsitektur Hexagonal (Port dan Adapter)

Arsitektur Hexagonal menempatkan logika bisnis di tengah dengan adapter yang menangani komunikasi eksternal.

Mendefinisikan Port (Antarmuka)

# Port Input (Primair)
class CreateUserUseCase(Protocol):
    def execute(self, request: 'CreateUserRequest') -> 'CreateUserResponse':
        ...

# Port Output (Sekunder)
class UserPersistencePort(Protocol):
    def save(self, user: User) -> User:
        ...
    
    def find_by_email(self, email: str) -> Optional[User]:
        ...

Mengimplementasikan Adapter

from pydantic import BaseModel, EmailStr

# Adapter Input (API REST)
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))

# Adapter Output (Database)
# Sudah diimplementasikan sebagai SQLAlchemyUserRepository

Pola Desain Berbasis Domain

Objek Nilai

Objek tidak dapat diubah yang didefinisikan oleh atributnya:

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"Email tidak valid: {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("Jumlah tidak boleh negatif")
        if self.currency not in ['USD', 'EUR', 'GBP']:
            raise ValueError(f"Mata uang tidak didukung: {self.currency}")
    
    def add(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("Tidak dapat menambahkan mata uang berbeda")
        return Money(self.amount + other.amount, self.currency)

Agregat

Kluster objek domain yang dianggap sebagai unit tunggal:

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("Tidak dapat mengonfirmasi pesanan kosong")
        if self.status != "pending":
            raise ValueError("Pesanan sudah diproses")
        self.status = "confirmed"

Event Domain

Event domain memungkinkan ketergantungan longgar antar komponen dan mendukung arsitektur berbasis event. Untuk sistem berbasis event skala produksi, pertimbangkan mengimplementasikan streaming event dengan layanan seperti AWS Kinesis—lihat Membangun Mikroservis Berbasis Event dengan AWS Kinesis untuk panduan lengkap.

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)

Fitur Python Modern untuk Arsitektur Bersih

Fitur modern Python membuat implementasi arsitektur bersih lebih elegan dan aman tipe. Jika Anda membutuhkan referensi cepat untuk sintaks dan fitur Python, lihat Python Cheatsheet.

Petunjuk Tipe dan Protokol

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 untuk Validasi

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('Nama tidak boleh mengandung angka')
        return v
    
    class Config:
        frozen = True  # Membuat objek tidak dapat diubah

Async/Await untuk Operasi I/O

Sintaks async/await Python sangat kuat untuk operasi I/O-bound dalam arsitektur bersih, memungkinkan interaksi non-blocking dengan database dan layanan eksternal. Saat mendeploy aplikasi Python ke platform serverless, memahami karakteristik kinerja menjadi penting—lihat AWS lambda performance: JavaScript vs Python vs Golang untuk wawasan mengenai optimisasi fungsi serverless Python.

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]

Praktik Terbaik Struktur Proyek

Organisasi proyek yang tepat sangat penting untuk mempertahankan arsitektur bersih. Sebelum mengatur struktur proyek Anda, pastikan Anda menggunakan lingkungan virtual Python untuk isolasi dependensi. venv Cheatsheet mencakup segala sesuatu yang perlu Anda ketahui tentang mengelola lingkungan virtual. Untuk proyek Python modern, pertimbangkan uv - New Python Package, Project, and Environment Manager, yang menyediakan manajemen paket dan pengaturan proyek yang lebih cepat.

my_application/
├── domain/                 # Aturan bisnis perusahaan
│   ├── __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/            # Aturan bisnis aplikasi
│   ├── __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/         # Antarmuka eksternal
│   ├── __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/           # Lapisan UI/API
│   ├── __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                 # Titik masuk aplikasi
├── container.py            # Pengaturan injeksi ketergantungan
├── pyproject.toml
└── README.md

Pengujian Arsitektur Bersih

Pengujian Unit Logika Domain

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

Pengujian Integrasi dengan 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

Pengujian Lapisan Layanan

from unittest.mock import Mock

def test_user_registration():
    # Menyiapkan
    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)
    
    # Melakukan
    user = service.register_user("test@example.com", "Test")
    
    # Memverifikasi
    assert user.email == "test@example.com"
    mock_repository.save.assert_called_once()
    mock_email.send.assert_called_once()
    mock_events.publish.assert_called_once()

Kesalahan Umum dan Cara Menghindarinya

Terlalu Rumit

Jangan implementasikan arsitektur bersih untuk aplikasi CRUD sederhana. Mulailah dengan sederhana dan refactor saat kompleksitas meningkat.

Abstraksi Bocor

Pastikan entitas domain tidak mengandung anotasi database atau kode spesifik framework:

# Buruk
from sqlalchemy import Column

@dataclass
class User:
    id: Column(Integer, primary_key=True)  # Framework bocor ke domain

# Baik
@dataclass
class User:
    id: UUID  # Objek domain murni

Ketergantungan Sirkular

Gunakan injeksi ketergantungan dan antarmuka untuk memecah ketergantungan sirkular antar lapisan.

Mengabaikan Konteks

Arsitektur bersih bukan satu ukuran untuk semua. Sesuaikan ketat lapisan berdasarkan ukuran proyek dan keahlian tim.

Tautan Berguna