Muitos desenvolvedores já ouviram falar sobre Clean Code, ou Código Limpo, e quando falamos sobre isso é comum associarmos à um código com fácil manutenção.
Mas, será que Clean Code é só sobre um código fácil de dar manutenção?
Design de Código e o Clean Code?
Se você já passou pela experiência de ter que adicionar algo relativamente simples em um código existente, e percebeu que essa “adição simples” impactaria em vários pontos do projeto, você sabe o que é um código difícil de dar manutenção. Sistemas legados não são exatamente o “sonho de um Desenvolvedor” e existe todo um traquejo para lidar com códigos de softwares assim, que tem pode dar muita, mas muita dor de cabeça. Mas não se preocupe aqui é está um guia para lidar com softwares legados. Tudo o que você precisa saber está aqui 🙂
Mas, se você nunca vivenciou isso, imagine ter que alterar um fragmento de código e essa alteração simplesmente quebrar todo o sistema. Definitivamente não seria legal.
E passar por isso faz a gente pensar que escrever um código totalmente novo é uma tarefa bem mais simples do que dar manutenção em código existente.
Mas, infelizmente, em nossas carreiras gastamos boa parte do tempo dando manutenção em código existente e, se não pensarmos direito no código que estamos escrevendo,vamos passar diversas vezes por situações semelhantes a essa.
Lembre-se sempre que todo código que escrevemos acaba se tornando um Passivo (uma dívida) para a empresa. E quanto menos nos preocuparmos com a manutenção do código maior é o valor desse Passivo.
O que é um código com fácil manutenção ?
Quando falamos de um código com fácil manutenção estamos nos referindo à um código com Baixo Acoplamento, Alta Coesão, usando SOLID, Imutabilidade (quando fizer sentido), aplicando Design Patterns, minimizando Side Effects, maximizar o uso de Funções Puras e várias outras coisas.
Tudo isso pode ser resumido em ter um bom Design de Código,uma parte muito importante em ter um código limpo.
E o que mais meu código precisa ter para ser considerado um código limpo?
SOLID na prática
Indo além da manutenabilidade
Pare 1 minuto olhando para esse código e tente responder: o que ele faz?
@Service
public class MovieSessionService {
private MovieSessionRepository sessionRepository;
private UnavailabilityRepository unavailabilityRepository;
private Converter<MovieSessionDTO, MovieSession> converter;
public MovieSessionService(MovieSessionRepository sessionRepository, UnavailabilityRepository unavailabilityRepository, Converter<MovieSessionDTO, MovieSession> converter) {
this.sessionRepository = sessionRepository;
this.unavailabilityRepository = unavailabilityRepository;
this.converter = converter;
}
public Result<MovieSession> create(MovieSessionDTO dto) {
MovieSession session = converter.convert(dto);
List<MovieSession> sessions = sessionRepository.listAllByTheaterId(dto.getTheaterId());
if (sessions.stream().anyMatch(s -> s.getStart().equals(session.getStart()) && s.getEnd().equals(session.getEnd()))) {
return Result.fail(SessionConflictException.class, session);
}
if (sessions.stream().anyMatch(s -> session.getStart().isBefore(s.getStart()) || session.getStart().isAfter(s.getEnd()))) {
return Result.fail(SessionConflictException.class, session);
}
List<Unavailability> unavailabilities = unavailabilityRepository.listAllByTheaterId(dto.getTheaterId());
if (unavailabilities.stream().anyMatch(u -> u.getStart().equals(session.getStart()) && u.getEnd().equals(session.getEnd()))) {
return Result.fail(UnavailablePeriodException.class, session);
}
if (unavailabilities.stream().anyMatch(u -> session.getStart().isBefore(u.getStart()) || session.getStart().isAfter(u.getEnd()))) {
return Result.fail(UnavailablePeriodException.class, session);
}
sessionRepository.save(session);
return Result.success(session);
}
}
O que você achou dessa sequência de if
s ? E esse monte de expressões sendo avaliada dentro de cada if
? Como poderíamos reduzir a quantidade de código duplicado ?
Perceba que fizemos um esforço tremendo para tentar entender o que esse código faz – e é possível que não tenhamos conseguido entendê-lo.
Esse código que acabei de mostrar tem a função de salvar uma sessão de cinema, desde que a sessão que estamos tentando salvar não tenha conflitos de horários com outras sessões existentes, ou com uma possível indisponibilidade na sala (por exemplo a sala estar indisponível para manutenção).
Toda essa carga cognitiva que fizemos para tentar entender o código traz um cansaço físico e mental. Agora leve em consideração que passamos a maior parte do tempo lendo código.
Então legibilidade conta muito na hora em que estamos escrevendo código.
Então poderíamos refatorar o código para algo mais ou menos assim:
@Service
public class MovieSessionService {
private MovieSessionRepository sessionRepository;
private UnavailabilityRepository unavailabilityRepository;
private Converter<MovieSessionRequest, MovieSession> converter;
public MovieSessionService(MovieSessionRepository sessionRepository, UnavailabilityRepository unavailabilityRepository, Converter<MovieSessionRequest, MovieSession> converter) {
this.sessionRepository = sessionRepository;
this.unavailabilityRepository = unavailabilityRepository;
this.converter = converter;
}
public Result<MovieSession> createMovieSessionBy(MovieSessionRequest movieSessionRequest) {
MovieSession newMovieSession = converter.convert(movieSessionRequest);
Result<MovieSession> overlapResult = checkOverlapsWith(newMovieSession);
if (overlapResult.isFail()) {
return overlapResult;
}
sessionRepository.save(newMovieSession);
return Result.success(newMovieSession);
}
private Result<MovieSession> checkOverlapsWith(MovieSession session) {
if (hasOverlapsWithAnotherMovieSessionsBy(session)) {
return Result.fail(SessionConflictException.class, session);
}
if (hasOverlapsWithUnavailabilitiesBy(session)) {
return Result.fail(SessionConflictException.class, session);
}
return Result.success(session);
}
private boolean hasOverlapsWithAnotherMovieSessionsBy(MovieSession session) {
List<MovieSession> sessions = sessionRepository.listAllByTheater(session.getTheater());
return hasOverlapsBetween(sessions, session);
}
private boolean hasOverlapsWithUnavailabilitiesBy(MovieSession session) {
List<Unavailability> unavailabilities = unavailabilityRepository.listAllByTheater(session.getTheater());
return hasOverlapsBetween(unavailabilities, session);
}
private boolean hasOverlapsBetween(List<? extends Periodable> periods, MovieSession session) {
LocalDateTime startTime = session.getStart();
LocalDateTime endTime = session.getEnd();
if (periods.stream().anyMatch(period -> period.getStart().equals(startTime) && period.getEnd().equals(endTime))) {
return true;
}
return periods.stream().anyMatch(period -> startTime.isBefore(period.getStart()) || startTime.isAfter(period.getEnd()));
}
}
O código agora parece um pouco mais organizado, com alguns nomes melhores para aumentar a semântica e sem boa parte da duplicidade, além de conseguirmos lê-lo de cima para baixo em um fluxo contínuo.
Podíamos continuar refatorando o código infinitamente, movendo as responsabilidades para as classes corretas e assim melhorando ainda mais o Design do Código e a manutenabilidade.
Dessa forma temos muito menos esforço para ler e tentar entender o código.
Lembre-se legibilidade conta muito para um código limpo.
Porém como podemos garantir que após essa alteração nosso código continua funcionando?
Testes, testes e mais testes
Sim, para garantir que seu código continua funcionando precisamos escrever testes.
Testes fazem parte do jogo quando estamos desenvolvendo, e o fato de tê-los não elimina totalmente a possibilidade de termos um bug) mas minimiza bastante.
Com os testes conseguimos garantir que pelo menos os cenários previstos estão funcionando e extrapolar esses cenários é o que torna nossos testes mais eficientes.
Quanto mais níveis de testes (unitários, integração, aceitação, regressão e etc…) tivermos mais segurança teremos na hora de aplicar uma refatoração.
A tarefa mais díficil na hora de se escrever um teste é saber o que devemos testar. E é justamente aí que temos que focar nossos esforços.
Testes são uma parte importante para todo o ciclo de vida de desenvolvimento e sim um código limpo é um código testável.
Agora sim o que é um código limpo?
Um código limpo é a composição de diversas características, como:
- Legível
Um código compreensivo possibilita a identificação de pontos que precisam ser melhorados. Passamos mais tempo lendo código do que escrevendo então, quanto mais fácil for ler o código menos esforço fazemos para entendê-lo. - Testável
Devemos testar nossos código, pois isso vai dar-nos segurança para podermos alterá-los. E garantir que os cenários que previmos estão de acordo com o esperado. - Fácil de ser mantido
Nosso código deve passivo de alteração tanto para adição de novas funcionalidades, quanto para aumentar a legibilidade ou manutenibilidade.
De uma forma bem resumida um código limpo é um código testável, fácil de manter e de ler.