- Data Hackers Newsletter
- Posts
- Vertical Slicing: o que é e como aplicar a arquitetura em Python
Vertical Slicing: o que é e como aplicar a arquitetura em Python
Conheça a Vertical Slice Architecture (VSA), alternativa de arquitetura que organiza o código por funcionalidade
Você está cansado de organizar seus projetos em camadas horizontais?
A Vertical Slice Architecture (VSA) é uma alternativa interessante às arquiteturas em camadas tradicionais. Enquanto a maioria dos desenvolvedores está acostumada a pensar em termos de camadas (Apresentação, Aplicação, Domínio), a VSA propõe uma abordagem completamente diferente.
Em vez de camadas horizontais, a VSA organiza o código por funcionalidade. Cada feature engloba tudo o que precisa, desde endpoints de API até acesso a dados. É como cortar um bolo verticalmente, ao invés de horizontalmente - cada fatia contém todos os ingredientes necessários.
Neste artigo, vamos explorar como estruturar vertical slices e aplicar essa arquitetura em projetos Python.
O que são vertical slices?
No núcleo da VSA, um vertical slice representa uma unidade autossuficiente de funcionalidade. É um corte através de toda a stack da aplicação, encapsulando todo o código e componentes necessários para cumprir uma feature específica.
Nas arquiteturas em camadas tradicionais, o código é organizado horizontalmente através de várias camadas. A implementação de uma única feature pode estar espalhada por múltiplas camadas. Modificar uma funcionalidade requer alterações em código distribuído por várias partes do sistema.
A VSA resolve isso agrupando todo o código de uma feature em um único slice.
Esta mudança de perspectiva traz várias vantagens:
Coesão melhorada: código relacionado a uma feature específica reside junto, facilitando compreensão, modificação e testes
Complexidade reduzida: VSA simplifica o modelo mental da aplicação ao evitar a necessidade de navegar por múltiplas camadas
Foco na lógica de negócio: a estrutura naturalmente enfatiza o caso de uso de negócio sobre detalhes técnicos de implementação
Manutenção facilitada: mudanças em uma feature são localizadas dentro do seu slice, reduzindo o risco de efeitos colaterais indesejados
Implementando vertical slice architecture em Python
Vamos ver um exemplo prático de um vertical slice representando a feature CreateProduct. Em Python, podemos organizar isso usando módulos e classes de forma similar:
# features/products/create_product.py
from dataclasses import dataclass
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field, validator
# Request/Response models
class CreateProductRequest(BaseModel):
name: str = Field(..., max_length=100)
price: float = Field(..., ge=0)
@validator('name')
def name_must_not_be_empty(cls, v):
if not v or not v.strip():
raise ValueError('Name cannot be empty')
return v
class CreateProductResponse(BaseModel):
id: int
name: str
price: float
# Endpoint
router = APIRouter(prefix="/products", tags=["Products"])
@router.post("/", response_model=CreateProductResponse)
def create_product(
request: CreateProductRequest,
db: Session = Depends(get_db)
):
product = Product(
name=request.name,
price=request.price
)
db.add(product)
db.commit()
db.refresh(product)
return CreateProductResponse(
id=product.id,
name=product.name,
price=product.price
)
Principais benefícios desta estrutura
O código para toda a feature CreateProduct está agrupado em um único arquivo. Isso torna extremamente fácil localizar, entender e modificar tudo relacionado a esta funcionalidade. Não precisamos navegar por múltiplas camadas (como controllers, services, repositories, etc.).
A estrutura modular do Python facilita ainda mais essa organização. Podemos ter um diretório features/ onde cada feature é um módulo ou package separado:
app/
├── features/
│ ├── products/
│ │ ├── __init__.py
│ │ ├── create_product.py
│ │ ├── update_product.py
│ │ ├── delete_product.py
│ │ └── get_product.py
│ └── orders/
│ ├── __init__.py
│ ├── create_order.py
│ └── get_order.py
└── main.py
Introduzindo validação nos vertical slices
Os vertical slices geralmente precisam resolver preocupações transversais, sendo a validação uma delas. A validação é o guardião, impedindo que dados inválidos ou maliciosos entrem no seu sistema.
Em Python, podemos implementar validação de várias formas. O Pydantic é uma escolha popular que se integra perfeitamente com FastAPI:
from pydantic import BaseModel, Field, validator
from typing import Optional
class CreateProductRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., ge=0)
description: Optional[str] = Field(None, max_length=500)
@validator('name')
def name_must_be_valid(cls, v):
if not v or not v.strip():
raise ValueError('Name cannot be empty or whitespace')
return v.strip()
@validator('price')
def price_must_be_reasonable(cls, v):
if v > 1000000:
raise ValueError('Price seems unreasonably high')
return v
class Config:
schema_extra = {
"example": {
"name": "Laptop",
"price": 999.99,
"description": "High-performance laptop"
}
}
Para validações mais complexas, podemos usar bibliotecas como marshmallow ou criar classes validadoras customizadas:
# features/products/create_product.py
from typing import List
from abc import ABC, abstractmethod
class ValidationError(Exception):
def __init__(self, errors: List[str]):
self.errors = errors
super().__init__(f"Validation failed: {', '.join(errors)}")
class ProductValidator:
def __init__(self, db: Session):
self.db = db
def validate(self, request: CreateProductRequest) -> List[str]:
errors = []
# Validação de nome único
existing_product = self.db.query(Product).filter(
Product.name == request.name
).first()
if existing_product:
errors.append(f"Product with name '{request.name}' already exists")
# Outras validações de negócio
if request.price < 0:
errors.append("Price cannot be negative")
return errors
@router.post("/", response_model=CreateProductResponse)
def create_product(
request: CreateProductRequest,
db: Session = Depends(get_db)
):
validator = ProductValidator(db)
errors = validator.validate(request)
if errors:
raise HTTPException(status_code=400, detail=errors)
# ... criar produto e retornar resposta
Lidando com features complexas e lógica compartilhada
Os exemplos anteriores eram simples. Mas o que fazemos com features complexas e lógica compartilhada?
A VSA se destaca no gerenciamento de features autocontidas. No entanto, aplicações do mundo real frequentemente envolvem interações complexas e lógica compartilhada.
Estratégias para gerenciar complexidade
Aqui estão algumas estratégias que você pode considerar:
Estratégia | Descrição | Quando usar |
|---|---|---|
Decomposição | Quebrar features complexas em slices menores e mais gerenciáveis | Quando uma feature tem múltiplas responsabilidades claras |
Refatoração | Aplicar técnicas como Extract Method e Extract Class | Quando um slice se torna difícil de manter |
Extrair lógica compartilhada | Criar classes ou funções utilitárias separadas | Quando múltiplas features usam a mesma lógica |
Empurrar lógica para baixo | Mover lógica de negócio para entidades de domínio | Quando a lógica pertence naturalmente ao modelo |
Exemplo de lógica compartilhada em Python
# shared/email_service.py
from abc import ABC, abstractmethod
from typing import List
class EmailService(ABC):
@abstractmethod
def send_email(self, to: List[str], subject: str, body: str):
pass
class SmtpEmailService(EmailService):
def __init__(self, smtp_config):
self.smtp_config = smtp_config
def send_email(self, to: List[str], subject: str, body: str):
# Implementação real de envio de e-mail
pass
# features/products/create_product.py
@router.post("/", response_model=CreateProductResponse)
def create_product(
request: CreateProductRequest,
db: Session = Depends(get_db),
email_service: EmailService = Depends(get_email_service)
):
product = Product(name=request.name, price=request.price)
db.add(product)
db.commit()
db.refresh(product)
# Usar serviço compartilhado
email_service.send_email(
to=["[email protected]"],
subject="New Product Created",
body=f"Product '{product.name}' was created"
)
return CreateProductResponse(
id=product.id,
name=product.name,
price=product.price
)
CQRS out of the box
Um benefício natural da VSA é que ela nos dá CQRS (Command Query Responsibility Segregation) praticamente de graça. Cada vertical slice é naturalmente um comando ou uma consulta.
# features/products/queries/get_product.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
router = APIRouter(prefix="/products", tags=["Products"])
class GetProductResponse(BaseModel):
id: int
name: str
price: float
description: Optional[str]
@router.get("/{product_id}", response_model=GetProductResponse)
def get_product(
product_id: int,
db: Session = Depends(get_db)
):
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return GetProductResponse(
id=product.id,
name=product.name,
price=product.price,
description=product.description
)
# features/products/commands/update_product.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
router = APIRouter(prefix="/products", tags=["Products"])
class UpdateProductRequest(BaseModel):
name: Optional[str] = Field(None, max_length=100)
price: Optional[float] = Field(None, ge=0)
description: Optional[str] = Field(None, max_length=500)
@router.put("/{product_id}")
def update_product(
product_id: int,
request: UpdateProductRequest,
db: Session = Depends(get_db)
):
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(status_code=404, detail="Product not found")
if request.name:
product.name = request.name
if request.price is not None:
product.price = request.price
if request.description is not None:
product.description = request.description
db.commit()
db.refresh(product)
return {"message": "Product updated successfully"}
Testando vertical slices
Uma das maiores vantagens da VSA é como ela facilita os testes. Como cada slice é autocontido, podemos testá-lo de forma isolada:
# tests/features/products/test_create_product.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
def test_create_product_success():
# Arrange
client = TestClient(app)
product_data = {
"name": "Test Product",
"price": 99.99
}
# Act
response = client.post("/products/", json=product_data)
# Assert
assert response.status_code == 200
data = response.json()
assert data["name"] == "Test Product"
assert data["price"] == 99.99
assert "id" in data
def test_create_product_invalid_price():
# Arrange
client = TestClient(app)
product_data = {
"name": "Test Product",
"price": -10
}
# Act
response = client.post("/products/", json=product_data)
# Assert
assert response.status_code == 422
def test_create_product_duplicate_name():
# Arrange
client = TestClient(app)
product_data = {"name": "Duplicate", "price": 50}
# Act
client.post("/products/", json=product_data)
response = client.post("/products/", json=product_data)
# Assert
assert response.status_code == 400
Perguntas frequentes
Quando devo usar VSA ao invés de Clean Architecture?
VSA é ideal para aplicações onde as features são bem definidas e relativamente independentes. Clean Architecture pode ser mais apropriada para sistemas com lógica de domínio complexa e muitas regras de negócio interdependentes.
VSA aumenta a duplicação de código?
Não necessariamente. Você ainda pode extrair código compartilhado quando fizer sentido. A diferença é que a duplicação é aceita inicialmente, e a abstração é feita apenas quando um padrão claro emerge.
Como lidar com transações que envolvem múltiplos slices?
Para operações que precisam coordenar múltiplos slices, você pode criar um slice coordenador ou usar padrões como Saga para gerenciar transações distribuídas.
VSA funciona bem com microsserviços?
Sim! Na verdade, VSA se alinha muito bem com a filosofia de microsserviços, já que ambos enfatizam autonomia e baixo acoplamento.
Resumo
A Vertical Slice Architecture é mais do que apenas uma forma de estruturar código. Ao focar em features, a VSA permite criar aplicações coesas e sustentáveis. Os vertical slices são autocontidos, tornando testes unitários e de integração mais diretos.
A VSA traz benefícios em termos de organização de código e velocidade de desenvolvimento, tornando-a uma ferramenta valiosa no seu arsenal. O código é agrupado por feature, facilitando localização e compreensão. A estrutura se alinha com a forma como usuários de negócio pensam sobre funcionalidades. Mudanças são localizadas, reduzindo o risco de regressões e permitindo iterações mais rápidas.
Em Python, frameworks como FastAPI e Flask facilitam muito a implementação de VSA, com suas capacidades de roteamento modular e injeção de dependências. A natureza dinâmica do Python também permite experimentar com diferentes estruturas até encontrar o que funciona melhor para seu time.
Considere adotar a Vertical Slice Architecture no seu próximo projeto Python. É uma grande mudança de mentalidade em relação à Clean Architecture tradicional. No entanto, ambas têm seu lugar e até compartilham ideias similares. O importante é escolher a abordagem que melhor se adequa às necessidades do seu projeto e da sua equipe.