SOLID
Os cinco princípios, cada um com o problema que resolve — em DataForge.
SOLID não é lei; são cinco observações sobre o que costuma dar errado. Cada uma abaixo vem com o código ruim e o bom, para a diferença ficar concreta.
S — Responsabilidade única#
Uma classe deve ter um só motivo para mudar.
dataforge
// Ruim: muda se a regra de imposto mudar, se o formato do
// relatório mudar, OU se o banco mudar. Três motivos.
blueprint FolhaRuim:
action setup(salario):
self.salario := salario
action calcular_imposto():
yield self.salario * 0.275
action formatar_relatorio():
yield $"Salário: {self.salario}"
action salvar():
yield "gravado"
assert (spawn FolhaRuim(1000)).calcular_imposto() is 275.0dataforge
// Bom: cada peça muda por um motivo só
record Folha:
salario: Float
blueprint CalculadoraDeImposto:
action calcular(folha):
yield folha.salario * 0.275
blueprint RelatorioDeFolha:
action formatar(folha):
yield $"Salário: {folha.salario}"
f := Folha(1000.0)
assert (spawn CalculadoraDeImposto()).calcular(f) is 275.0
assert (spawn RelatorioDeFolha()).formatar(f) is "Salário: 1000.0"O — Aberto/fechado#
Aberto para extensão, fechado para modificação. Acrescentar comportamento não deveria exigir editar o que já funciona.
dataforge
// Ruim: cada meio de pagamento novo mexe aqui
action cobrar_ruim(tipo, valor):
match tipo:
point "pix":
yield valor
point "cartao":
yield valor * 1.05
default:
trigger "meio desconhecido"
assert cobrar_ruim("pix", 100) is 100dataforge
// Bom: um meio novo é um blueprint novo
trait MeioDePagamento:
action cobrar(valor)
blueprint Pix with MeioDePagamento:
action cobrar(valor):
yield valor
blueprint Cartao with MeioDePagamento:
action cobrar(valor):
yield valor * 1.05
blueprint Boleto with MeioDePagamento:
action cobrar(valor):
yield valor + 3.50
action cobrar(meio, valor):
yield meio.cobrar(valor)
assert cobrar(spawn Pix(), 100) is 100
assert cobrar(spawn Boleto(), 100) is 103.5L — Substituição de Liskov#
Onde cabe a mãe, tem de caber a filha. Uma subclasse não pode quebrar o que a mãe prometeu.
dataforge
// Ruim: Quadrado herda de Retangulo e quebra a promessa
blueprint Retangulo:
action setup(largura, altura):
self.largura := largura
self.altura := altura
action area():
yield self.largura * self.altura
blueprint QuadradoRuim extends Retangulo:
action setup(lado):
root.setup(lado, lado)
// quem tem um Retangulo espera mudar um lado só
action mudar_largura(v):
self.largura := v
self.altura := v // surpresa
r := spawn QuadradoRuim(5)
r.mudar_largura(3)
assert r.area() is 9 // quem esperava 15 se enganoudataforge
// Bom: os dois implementam o contrato, sem herdar um do outro
trait Forma:
action area()
blueprint Retangulo(largura, altura) with Forma:
action area():
yield self.largura * self.altura
blueprint Quadrado(lado) with Forma:
action area():
yield self.lado ** 2
assert (spawn Retangulo(3, 5)).area() is 15
assert (spawn Quadrado(4)).area() is 16I — Segregação de interface#
Ninguém deve depender de método que não usa. Traits pequenos e específicos, não um grande.
dataforge
// Ruim: uma impressora simples é obrigada a saber escanear
trait MaquinaRuim:
action imprimir(doc)
action escanear(doc)
action enviar_fax(doc)
blueprint SimplesRuim with MaquinaRuim:
action imprimir(doc):
yield "imprimiu"
action escanear(doc):
trigger "não sei escanear" // implementa para nada
action enviar_fax(doc):
trigger "não sei mandar fax"
assert (spawn SimplesRuim()).imprimir("x") is "imprimiu"dataforge
// Bom: traits separados, cada objeto implementa o que faz
trait Impressora:
action imprimir(doc)
trait Scanner:
action escanear(doc)
blueprint Simples with Impressora:
action imprimir(doc):
yield "imprimiu"
blueprint Multifuncional with Impressora, Scanner:
action imprimir(doc):
yield "imprimiu"
action escanear(doc):
yield "escaneou"
assert (spawn Simples()).imprimir("x") is "imprimiu"
assert (spawn Multifuncional()).escanear("x") is "escaneou"D — Inversão de dependência#
Dependa de abstração, não de implementação. O código de negócio não deveria saber que o banco é PostgreSQL.
dataforge
// Ruim: a regra de negócio conhece o detalhe do armazenamento
blueprint ServicoRuim:
action setup():
self.linhas := []
action cadastrar(nome):
self.linhas.append($"INSERT INTO usuarios VALUES ('{nome}')")
yield len(self.linhas)
assert (spawn ServicoRuim()).cadastrar("Ana") is 1dataforge
// Bom: a regra depende de um contrato; o detalhe entra por fora
trait RepositorioDeUsuarios:
action salvar(nome)
action contar()
blueprint RepositorioEmMemoria with RepositorioDeUsuarios:
action setup():
self.itens := []
action salvar(nome):
self.itens.append(nome)
yield nome
action contar():
yield len(self.itens)
blueprint ServicoDeCadastro:
action setup(repositorio):
self.repositorio := repositorio
action cadastrar(nome):
given len(nome) smaller 2:
trigger "nome curto demais"
yield self.repositorio.salvar(nome)
// o teste usa memória; a produção usaria o Forge — mesma classe
s := spawn ServicoDeCadastro(spawn RepositorioEmMemoria())
s.cadastrar("Ana")
assert s.repositorio.contar() is 1Acoplamento e coesão#
Os cinco princípios servem a duas ideias mais gerais:
| Você quer | Sinal de problema | |
|---|---|---|
| acoplamento | baixo — poucas peças se conhecem | mudar A obriga a mudar B, C e D |
| coesão | alta — o que está junto pertence junto | uma classe chamada Utils ou Manager |
Ver modelagem.