← início

Refatorando o MedAlerta: quando simplificar também é arquitetura

🔗 Repositório do projeto — MedAlerta

Refatorando o MedAlerta: quando simplificar também é arquitetura

Durante o desenvolvimento do MedAlerta, eu comecei com uma ideia bem direta: seguir o plano de Modelo de Entidade Relacionamento e Modelo Relacional desenvolvido pelo nosso grupo no Bootcamp I de Engenharia de Software da UNINTER.

O MedAlerta é um sistema CLI em Java com Spring Boot que ajuda usuários a organizar tratamentos, medicamentos e alertas de consumo. Nada muito complexo, mas suficiente para colocar em prática bastante coisa.

O projeto nasceu com JPA, MySQL e uma arquitetura em camadas. A estrutura inicial seguia aquele modelo bem comum:

App.java
  -> Services
  -> Repositories
  -> Entidades JPA
  -> Banco MySQL

Na teoria, estava tudo certo. Tinha separação entre interface de entrada, regra de negócio e persistência. Para um projeto de bootcamp, isso já era uma boa base.

Mas conforme eu fui olhando com mais calma, veio aquela sensação: “será que não tem classe demais para o que essa aplicação realmente faz?”

E essa pergunta virou o ponto de partida da refatoração.

O projeto antes da refatoração

Antes da refatoração, o MedAlerta tinha entidades como:

Usuario
Endereco
Medicamento
Tratamento
TratamentoMedicamento
TratamentoMedicamentoId
Alerta

E, seguindo a estrutura em camadas, quase toda entidade tinha também:

Repository
Service
Menu na CLI

Então uma única entidade a mais já puxava uma pequena fila de arquivos junto.

Isso não é necessariamente errado. Em projetos Spring, é bem comum cada entidade ter seu repository e seu service. O problema é quando a gente faz isso de forma automática, sem parar para pensar se aquela entidade participa mesmo do domínio da aplicação.

Foi aí que o Endereco começou a incomodar.

O primeiro corte: remover Endereço

O MedAlerta é uma aplicação CLI para controle de medicamentos. O usuário cadastra medicamentos, monta tratamentos, cria alertas e registra se tomou ou não tomou o remédio.

Nesse contexto, o endereço do usuário não participa de nenhuma regra importante.

Ele faria sentido se o sistema tivesse entrega de medicamento, atendimento domiciliar, emergência por localização, farmácia vinculada ou algo do tipo. Mas para o escopo atual, ele era só cadastro por cadastro.

Então eu decidi remover:

Endereco.java
EnderecoRepository.java
EnderecoService.java
menu de endereço na CLI
tabela endereco no SQL
seed de endereço
relacionamento Usuario -> Endereco

Esse foi um bom exemplo de uma coisa que eu venho aprendendo: simplificar também é uma decisão de arquitetura.

Não adianta ter uma arquitetura “bonita” se o domínio está carregando coisa que não conversa com o problema principal.

Depois desse corte, o modelo ficou mais direto:

Usuario
  -> Tratamento
       -> Alerta
       -> TratamentoMedicamento
            -> Medicamento

Bem mais próximo do que a aplicação realmente faz.

O problema na associação entre Tratamento e Medicamento

Outro ponto importante estava na relação entre Tratamento e Medicamento.

No modelo inicial, existia uma tabela associativa chamada tratamento_medicamento, o que fazia sentido, porque um tratamento pode ter vários medicamentos e um medicamento pode aparecer em vários tratamentos.

O detalhe é que essa associação não era só uma ponte entre duas tabelas. Ela tinha significado de domínio.

Não era apenas:

tratamento X tem medicamento Y

Era algo mais parecido com:

tratamento X usa medicamento Y
com determinada dose
em determinado horário
com determinada frequência

Por isso, TratamentoMedicamento precisava ser tratado como uma entidade de verdade, não como uma tabela auxiliar qualquer.

Antes, o projeto usava chave composta:

TratamentoMedicamento
TratamentoMedicamentoId

Funciona, e é bem normal em JPA. Mas para esse projeto específico, eu achei que estava adicionando complexidade demais.

Então a decisão foi trocar a chave composta por um ID próprio:

id_tratamento_medicamento

E manter uma constraint única no banco:

UNIQUE (id_tratamento, id_medicamento)

Com isso, eu consegui remover TratamentoMedicamentoId, simplificar o repository e deixar o código mais amigável para manutenção.

Na prática, o mapeamento JPA ficou muito mais simples. Em vez de lidar com @EmbeddedId e @MapsId, a entidade passou a ter um @Id comum:

@Entity
@Table(
    name = "tratamento_medicamento",
    uniqueConstraints = @UniqueConstraint(columnNames = {"id_tratamento", "id_medicamento"})
)
@Getter
@Setter
@NoArgsConstructor
public class TratamentoMedicamento {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id_tratamento_medicamento")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "id_tratamento", nullable = false)
    private Tratamento tratamento;

    @ManyToOne
    @JoinColumn(name = "id_medicamento", nullable = false)
    private Medicamento medicamento;

    @NotNull
    @Column(name = "quantidade")
    private Double quantidade;

    @Column(name = "observacao")
    private String observacao;

    @NotNull
    @Column(name = "horario_uso")
    private LocalTime horarioUso;

    @NotBlank
    @Column(name = "frequencia_uso")
    private String frequenciaUso;
}

Muito mais legível do que seria com chave composta.

Onde colocar horário, frequência e dose?

Esse foi um dos pontos mais importantes da refatoração.

Antes, Tratamento carregava campos como:

horarioUso
frequenciaUso

Mas pensando melhor no domínio, isso parecia limitado.

Um tratamento pode ter mais de um medicamento. E cada medicamento pode ter uma dose, horário e frequência diferente.

Por exemplo:

Tratamento: controle de pressão

Losartana
- 1 comprimido
- 08:00
- 1x ao dia

Atenolol
- 1 comprimido
- 20:00
- 1x ao dia

Se horário e frequência ficam em Tratamento, eu assumo que todos os medicamentos daquele tratamento seguem o mesmo uso. Isso pode até funcionar em alguns casos, mas não representa bem a realidade.

Então eu movi esses campos para TratamentoMedicamento:

quantidade
observacao
horarioUso
frequenciaUso

Agora o tratamento representa o plano geral, e o item do tratamento representa como cada medicamento deve ser usado.

Esse ajuste deixou o modelo muito mais coerente.

Medicamento.quantidade virou unidadeMedida

Outra coisa que estava meio estranha era o campo quantidade em Medicamento.

Ele usava valores como:

UNIDADE
ML

Mas isso não é quantidade. Isso é unidade de medida.

Quantidade seria:

1 comprimido
10 ml
2 gotas

Então eu renomeei conceitualmente o campo para:

unidadeMedida

E deixei a quantidade real no TratamentoMedicamento, porque a dose depende do uso do medicamento dentro de um tratamento.

Mantive layered architecture

Uma coisa importante: eu não abandonei a arquitetura em camadas.

Eu até pensei se faria sentido ir para algo mais próximo de Clean Architecture, mas para o tamanho atual do MedAlerta isso seria exagero.

O projeto continua assim:

app
model
repository
service

A diferença é que agora as classes existem com mais intenção.

Antes parecia um pouco “uma tabela, um service, um menu”. Depois da refatoração, a estrutura ficou mais conectada ao domínio:

UsuarioService
MedicamentoService
TratamentoService
AlertaService

O TratamentoMedicamentoService foi removido, porque associar medicamento a tratamento é responsabilidade natural do TratamentoService.

Ficou mais simples e mais expressivo.

A regra de consumo saiu da CLI

Outro ajuste que achei importante foi tirar regra de negócio da CLI.

Antes, o método que registrava consumo fazia a leitura da resposta do usuário e também alterava o estado do alerta:

confirmacaoConsumo
statusAlerta
dataHorarioConsumo

Só que essa mudança de estado é regra de negócio, não responsabilidade da interface.

Então eu criei um método no AlertaService:

public void registrarConsumo(Long idAlerta, ConfirmacaoConsumo confirmacaoConsumo) {
    Alerta alerta = alertaRepository.findById(idAlerta)
        .orElseThrow(() -> new RuntimeException("Alerta não encontrado"));

    alerta.setConfirmacaoConsumo(confirmacaoConsumo);
    alerta.setDataHorarioConsumo(LocalDateTime.now());
    alerta.setStatusAlerta(
        confirmacaoConsumo == ConfirmacaoConsumo.SIM
            ? StatusAlerta.CONSUMIDO
            : StatusAlerta.NAO_CONSUMIDO
    );

    alertaRepository.save(alerta);
}

Agora a CLI pergunta se o medicamento foi tomado e chama o service. O service decide como atualizar o alerta.

Esse tipo de ajuste parece pequeno, mas melhora bastante a separação de responsabilidades.

Lombok e Bean Validation

Também aproveitei para adicionar Lombok e Bean Validation.

As entidades tinham muito boilerplate:

getters
setters
construtor vazio
toString

Com Lombok, isso ficou bem mais limpo:

@Getter
@Setter
@NoArgsConstructor

Eu evitei usar @Data nas entidades JPA, porque ele gera equals, hashCode e toString automaticamente, e isso pode causar dor de cabeça com relacionamentos JPA — especialmente em entidades com @ManyToMany ou @OneToMany, onde um toString recursivo pode lançar StackOverflowError.

Também adicionei validações como:

@NotBlank
@Email
@Size
@NotNull

Isso deixa as regras básicas mais visíveis no próprio modelo.

O banco também mudou

Como o backend foi construído em cima do MR e do MER, os diagramas e o SQL também precisaram mudar.

O novo modelo relacional remove endereco e deixa tratamento_medicamento assim:

id_tratamento_medicamento
id_tratamento
id_medicamento
quantidade
observacao
horario_uso
frequencia_uso

E medicamento passa a ter:

unidade_medida

em vez de:

quantidade

Também atualizei os dados de seed para refletir esse novo modelo.

O que mudou no fim

Depois da refatoração, o projeto ficou mais enxuto.

Saiu:

Endereco
EnderecoRepository
EnderecoService
TratamentoMedicamentoId
TratamentoMedicamentoService
menus de endereço
campos de horário/frequência em Tratamento

Entrou ou mudou:

TratamentoMedicamento com ID próprio
quantidade/dose no item do tratamento
horário e frequência no item do tratamento
unidadeMedida em Medicamento
Lombok
Bean Validation
AlertaService.registrarConsumo

O resultado foi uma layered architecture mais simples, mas também mais honesta.

O que eu aprendi com essa refatoração

A maior lição para mim foi que arquitetura não é só adicionar camadas, padrões e classes.

Arquitetura também é saber tirar.

Tirar uma entidade que não agrega valor. Tirar uma classe de service que só repassa chamada. Tirar uma chave composta quando ela torna o projeto mais difícil do que precisa ser. Tirar regra de negócio da interface.

No começo, eu estava olhando para a quantidade de classes e achando que talvez a layered architecture fosse o problema.

Depois eu entendi que o problema não era a arquitetura em camadas. O problema era aplicar a estrutura de forma mecânica.

Para o MedAlerta, layered architecture continua fazendo sentido. O que mudou foi o critério.

Agora o projeto está mais alinhado com o domínio:

usuário tem tratamentos
tratamento tem medicamentos com dose, horário e frequência
tratamento gera alertas
alerta registra consumo

Simples, direto e mais fácil de explicar.

E para mim, esse foi o melhor sinal de que a refatoração fez sentido: se o modelo fica mais fácil de contar em voz alta, provavelmente ele ficou mais perto do problema real.