Refatorando o MedAlerta: quando simplificar também é arquitetura
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.