Pular para o conteúdo

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.0
dataforge
// 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 100
dataforge
// 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.5

L — 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 enganou
dataforge
// 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 16

I — 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 1
dataforge
// 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 1

Acoplamento e coesão#

Os cinco princípios servem a duas ideias mais gerais:

Você querSinal de problema
acoplamentobaixo — poucas peças se conhecemmudar A obriga a mudar B, C e D
coesãoalta — o que está junto pertence juntouma classe chamada Utils ou Manager

Ver modelagem.