Os business objects (BOs) são um padrão fundamental no desenvolvimento de aplicações empresariais, encapsulando dados e lógica de negócio numa estrutura reutilizável e fácil de manter. Decidir quando e como implementar business objects pode determinar o sucesso ou o fracasso da viabilidade a longo prazo de um projeto. Este guia aborda a conceção de uma arquitetura eficaz de business objects na perspetiva de um developer sénior.
Porque Business Objects
Os business objects oferecem:
- Abstração: Separar a lógica de negócio da infraestrutura
- Reutilização: A mesma lógica em múltiplas interfaces
- Validação: Regras centralizadas de integridade de dados
- Testabilidade: Unidades isoladas para testes
- Documentação: Modelo de domínio autoexplicativo
Quando Business Objects Fazem Sentido
Bom Enquadramento: Sistemas Empresariais Complexos
Considere um sistema de gestão documental para grandes organizações:
- Múltiplos tipos de entidades: Documentos, utilizadores, permissões, workflows
- Relações complexas: Os documentos pertencem a categorias, têm versões, requerem aprovações
- Regras de negócio: Validação, transições de estado, controlo de acesso
- Múltiplas interfaces: Web UI, app móvel, API, processamento em batch
Aqui, os business objects fornecem:
┌─────────────────────────────────────────────────────────┐
│ User Interface │
├─────────────────────────────────────────────────────────┤
│ Business Objects │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Document │ │ Workflow │ │ User │ │
│ │ - validate│ │ - approve│ │ - auth │ │
│ │ - version │ │ - reject │ │ - perms │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ Data Access Layer (ORM) │
├─────────────────────────────────────────────────────────┤
│ Database │
└─────────────────────────────────────────────────────────┘
Mau Enquadramento: Processamento Simples de Dados
Para um serviço rápido de processamento de SMS:
- Modelo de dados simples: 10 campos, relações mínimas
- Operações diretas: Receber, validar, armazenar
- Interface única: Endpoint de API
- Sem regras de negócio complexas
Usar um framework BO completo (como Symfony com Propel) exige criar mais de 40 ficheiros para operações CRUD simples. O overhead supera os benefícios.
Decisões de Arquitetura
Framework vs. Personalizado
Use Framework Quando:
- A equipa já o conhece
- O projeto tem uma vida útil longa (5+ anos)
- Vários developers irão mantê-lo
- Os padrões standard se adequam às suas necessidades
Construa Personalizado Quando:
- Requisitos únicos não se enquadram em frameworks
- O desempenho é crítico
- A equipa tem conhecimento profundo do domínio
- Aplicação simples e focada
Considerações sobre Geração de Código
Muitos frameworks de BO usam geradores de código (Propel, Entity Framework, etc.):
Prós:
- Desenvolvimento inicial rápido
- Padrões consistentes
- Menos boilerplate
Contras:
- Alterações ao schema tornam-se penosas
- O código gerado frequentemente precisa de personalização
- Atualizações ao gerador podem quebrar extensões
- Mentalidade de copy-paste reduz o pensamento crítico
Boa prática: Prefira herança e composição em vez de modificar código gerado:
// Classe base gerada (não modificar)
abstract class BaseDocument {
protected $id;
protected $title;
protected $content;
// Getters/setters gerados
}
// As suas personalizações
class Document extends BaseDocument {
public function validate(): array {
$errors = [];
if (empty($this->title)) {
$errors[] = 'Title is required';
}
if (strlen($this->content) < 100) {
$errors[] = 'Content must be at least 100 characters';
}
return $errors;
}
public function publish(): void {
if (!$this->validate()) {
$this->status = 'published';
$this->publishedAt = new DateTime();
}
}
}
Conceber Business Objects
Responsabilidade Única
Cada BO deve ter um único motivo para mudar:
// Mau: Document faz demasiado
class Document {
public function save() { /*base de dados*/ }
public function sendEmail() { /*notificação*/ }
public function generatePDF() { /*exportação*/ }
public function validate() { /*regras*/ }
}
// Bom: Responsabilidades separadas
class Document {
public function validate(): bool { /*apenas validação*/ }
public function getData(): array { /*apenas acesso a dados*/ }
}
class DocumentRepository {
public function save(Document $doc): void { /*persistência*/ }
}
class DocumentNotifier {
public function notify(Document $doc): void { /*e-mail*/ }
}
class DocumentExporter {
public function toPDF(Document $doc): string { /*exportação*/ }
}
Modelo de Domínio Rico vs. Anémico
Modelo Anémico (apenas contentores de dados):
class Order {
public int $id;
public string $status;
public float $total;
public array $items;
}
// Lógica no service
class OrderService {
public function calculateTotal(Order $order): float {
return array_sum(array_map(
fn($item) => $item->price * $item->quantity,
$order->items
));
}
public function canCancel(Order $order): bool {
return $order->status === 'pending';
}
}
Modelo Rico (comportamento encapsulado):
class Order {
private int $id;
private string $status;
private array $items = [];
public function addItem(Product $product, int $quantity): void {
if ($this->status !== 'draft') {
throw new DomainException('Cannot modify submitted order');
}
$this->items[] = new OrderItem($product, $quantity);
}
public function getTotal(): float {
return array_sum(array_map(
fn($item) => $item->getSubtotal(),
$this->items
));
}
public function submit(): void {
if (empty($this->items)) {
throw new DomainException('Cannot submit empty order');
}
$this->status = 'pending';
}
public function cancel(): void {
if ($this->status !== 'pending') {
throw new DomainException('Only pending orders can be cancelled');
}
$this->status = 'cancelled';
}
}
Escolha Modelo Rico Quando:
- Transições de estado complexas
- Invariantes têm de ser mantidas
- Regras de negócio são específicas do domínio
Escolha Modelo Anémico Quando:
- Operações CRUD simples
- A lógica de negócio é procedimental
- A equipa prefere transaction scripts
Value Objects
Objetos imutáveis que representam conceitos sem identidade:
class Money {
private readonly float $amount;
private readonly string $currency;
public function __construct(float $amount, string $currency) {
if ($amount < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}
$this->amount = $amount;
$this->currency = $currency;
}
public function add(Money $other): Money {
if ($this->currency !== $other->currency) {
throw new DomainException('Cannot add different currencies');
}
return new Money($this->amount + $other->amount, $this->currency);
}
public function multiply(float $factor): Money {
return new Money($this->amount * $factor, $this->currency);
}
public function equals(Money $other): bool {
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
class Address {
public function __construct(
public readonly string $street,
public readonly string $city,
public readonly string $postalCode,
public readonly string $country
) {}
public function format(): string {
return "{$this->street}\n{$this->city}, {$this->postalCode}\n{$this->country}";
}
}
Aggregate Roots
Agrupe objetos relacionados e imponha limites:
class Order { // Aggregate root
private array $items = [];
public function addItem(Product $product, int $qty): void {
// Todo o acesso a OrderItems passa por Order
$this->items[] = new OrderItem($product, $qty);
}
public function removeItem(int $itemIndex): void {
unset($this->items[$itemIndex]);
}
// OrderItem não é diretamente acessível
}
// O repository trabalha apenas com o aggregate root
interface OrderRepository {
public function save(Order $order): void; // Guarda Order + Items
public function findById(int $id): ?Order; // Carrega Order + Items
}
Lidar com Cross-Cutting Concerns
Validação
interface Validatable {
public function validate(): ValidationResult;
}
class ValidationResult {
private array $errors = [];
public function addError(string $field, string $message): void {
$this->errors[$field][] = $message;
}
public function isValid(): bool {
return empty($this->errors);
}
public function getErrors(): array {
return $this->errors;
}
}
class Document implements Validatable {
public function validate(): ValidationResult {
$result = new ValidationResult();
if (empty($this->title)) {
$result->addError('title', 'Title is required');
}
if (strlen($this->title) > 255) {
$result->addError('title', 'Title must be under 255 characters');
}
return $result;
}
}
Domain Events
interface DomainEvent {
public function occurredAt(): DateTimeImmutable;
}
class OrderSubmitted implements DomainEvent {
public function __construct(
public readonly int $orderId,
public readonly float $total,
private DateTimeImmutable $occurredAt = new DateTimeImmutable()
) {}
public function occurredAt(): DateTimeImmutable {
return $this->occurredAt;
}
}
class Order {
private array $domainEvents = [];
public function submit(): void {
$this->status = 'pending';
$this->domainEvents[] = new OrderSubmitted($this->id, $this->getTotal());
}
public function pullDomainEvents(): array {
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}
Considerações de Desempenho
Lazy Loading
class Order {
private ?array $items = null;
private ItemRepository $itemRepo;
public function getItems(): array {
if ($this->items === null) {
$this->items = $this->itemRepo->findByOrderId($this->id);
}
return $this->items;
}
}
Operações em Massa
// Mau: queries N+1
foreach ($orders as $order) {
$order->save(); // Uma query por order
}
// Bom: operações em batch
$orderRepository->saveAll($orders); // Query única
Principais Conclusões
- Ajuste a complexidade às necessidades: Não faça over-engineering em aplicações simples
- Encapsule o comportamento: Modelos de domínio ricos para lógica de negócio complexa
- Use value objects: Objetos imutáveis para conceitos sem identidade
- Defina limites de agregados: Controle a consistência e as transações
- Separe responsabilidades: Repositories, services e objetos de domínio
- Considere o desempenho: Lazy loading, batching, caching
A arquitetura de business objects exige equilibrar abstração com pragmatismo — escolha padrões que resolvam os seus problemas reais, não os teóricos.