1
resposta

[Projeto] API de busca de livros por titulo com Book API

Este desafio foi um tanto quanto difícil por ser a minha segunda vez mexendo com esse sistema de consumir APIs. Esta é a minha segunda tentativa, agora usando a API do Google Books, e nela utilizei o Gson em vez do Jackson.

E nossa, como foi complicado de entender! O código eu consegui escrever de boa, mas na hora dos testes estava sempre me retornando um NPE. Foi aí que entendi que, para trabalhar com isso, é preciso ir fazendo aos poucos, em vez de tentar resolver tudo de uma vez e depois sofrer para corrigir bugs, como eu sofri.

Contudo, isso me fez aprender bastante. Uma coisa, por exemplo, é sempre testar primeiro o sistema de JSON para verificar se está tudo certo, porque nele você sempre consegue tirar boas dicas. Suas classes, que irão serializar o JSON, precisam ter o mesmo escopo em termos de nomes e abstrações. Era exatamente isso que estava me causando tanta dificuldade, pois sempre me retornava um NPE por conta dos nomes errôneos postos nos atributos do objeto
Class Main

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        HttpClientProvider client = HttpClientProvider.create();

        System.out.println("Enter title:");
        String title = scanner.nextLine().trim();
        String url = BookService.build(title);
        String json = client.getBody(url);

        BookResponse response = BookParser.parse(json, BookResponse.class);

        if (response.getItems().isEmpty()) {
            System.out.println("No books found");
            return;
        }

        response.getItems().forEach(item -> {
            System.out.printf("Title: %s%n", item.getVolumeInfo().getTitle());
            System.out.printf("Authors: %s%n", Arrays.toString(item.getVolumeInfo().getAuthors()));
            System.out.printf("Publisher: %s%n", item.getVolumeInfo().getPublisher());
            System.out.printf("Description: %s%n", item.getVolumeInfo().getDescription());
        });

        scanner.close();
    }
}

Class BookParser

@lombok.experimental.UtilityClass
public class BookParser {

    private final Gson gson = getGson();

    public  <T> T parse(String json, Class<T> clazz) {
       if (json == null) return null;
       try {
           return gson.fromJson(json, clazz);
       } catch (Exception e) {
           throw new RuntimeException(e);
       }
    }

    @Contract(" -> new")
    @ApiStatus.Internal
    private @NotNull Gson getGson() {
        return new GsonBuilder().create();
    }
}

Class BookService

public class BookService {
    
    @Contract("null -> null; !null -> !null")
    public static String build(String title) {
        if (title == null) return null;
        return "https://www.googleapis.com/books/v1/volumes?q="
                + StringUtil.replace(title)
                + "&key="
                + SecretKey.KEY.getKey();
    }
}

Class StringUtil

@lombok.experimental.UtilityClass
public class StringUtil {
    
    public String replace(String title) {
        if (title == null) return null;
        return title.replace(" ", "%20");
    }
}

Record HttpClientProvider

public record HttpClientProvider(@lombok.Getter(value = lombok.AccessLevel.PUBLIC) HttpClient client) {

    @Contract(" -> new")
    public static @NotNull HttpClientProvider create() {
        return new HttpClientProvider(HttpClient.newHttpClient());
    }

    public String getBody(String url) {
        HttpResponse<String> response = null;
        try {
            HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
            response = client.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (Exception e) {
            e.printStackTrace();
        }
        assert response != null;
        return response.body();
    }
}

Class BookResponse

@lombok.Data
public class BookResponse {
    List<Item> items;
}

Class VolumeInfo

@lombok.Data
public class VolumeInfo {
    private String title;
    private String[] authors;
    private String publisher;
    private String description;
}

Class Item

@lombok.Data
public class Item {
    private VolumeInfo volumeInfo;
}

Class Book

@lombok.ToString(of = {"title", "authors", "publisher", "description"})
@lombok.AllArgsConstructor(access = lombok.AccessLevel.PUBLIC)
@lombok.NoArgsConstructor(access = lombok.AccessLevel.PUBLIC)
@lombok.Getter(value = lombok.AccessLevel.PUBLIC)
public class Book {

    @SerializedName("title")
    private String title;

    @SerializedName("authors")
    private String[] authors;

    @SerializedName("publisher")
    private String publisher;

    @SerializedName("description")
    private String description;
}
1 resposta

a sua abordagem é muito boa, pois demonstra que você está pensando em conceitos importantes como separação de responsabilidades e reuso de código. O uso do Lombok e outras anotações mostra que você está familiarizado com ferramentas que simplificam o desenvolvimento Java.

Pontos Fortes
Separação de Responsabilidades: Você dividiu o código em classes lógicas (BookService, HttpClientProvider, BookParser, etc.), o que torna o projeto modular e mais fácil de manter.

A classe Main se concentra apenas na lógica de apresentação e orquestração.

BookService lida com a construção da URL da API.

HttpClientProvider cuida da comunicação HTTP.

BookParser é responsável por desserializar o JSON.

Boas Práticas de Design: O código está bem estruturado com classes que têm um propósito único. Isso segue o Princípio da Responsabilidade Única (SRP), um conceito fundamental em engenharia de software.

Uso de Ferramentas Modernas: A utilização de bibliotecas como Lombok e Gson é excelente. Elas reduzem a quantidade de código clichê (getters, setters, toString, etc.), permitindo que você se concentre na lógica de negócio.

Validação e Tratamento de Erros: Você incluiu verificações de nulidade e tratou exceções, o que é crucial para garantir que a aplicação não quebre em cenários inesperados.

Pontos a Considerar para Melhorar
Apesar do código já ser muito bom, aqui estão algumas sugestões para aprimorá-lo ainda mais, alinhando-o com boas práticas de projetos maiores e mais robustos.

  1. Gerenciamento de Exceções
    Você está usando e.printStackTrace() e throw new RuntimeException(e). Em um ambiente de produção, essas práticas não são ideais. O ideal é capturar as exceções e tratá-las de forma mais controlada, talvez registrando-as em um log ou apresentando uma mensagem de erro mais amigável ao usuário, em vez de interromper a aplicação abruptamente.

  2. Acoplamento e Reusabilidade
    O método BookParser.parse poderia ser mais genérico para lidar com qualquer classe de resposta, o que você já fez de forma excelente! No entanto, a forma como a classe Main orquestra as chamadas ainda é um pouco "rígida". Em cenários mais complexos, o uso de injeção de dependências seria uma evolução natural para tornar a classe Main menos dependente de classes concretas.

  3. Uso de assert
    A linha assert response != null; é geralmente usada para depuração e é desabilitada em ambientes de produção. Se a resposta da API puder ser null, é melhor lidar com esse caso explicitamente com um if/else ou com um retorno antecipado, garantindo que o programa se comporte de maneira previsível.

  4. Chave Secreta
    A classe SecretKey é um bom começo para centralizar sua chave, mas em um projeto real, você nunca deve armazenar chaves de API diretamente no código-fonte. O correto é usar variáveis de ambiente ou um serviço de gerenciamento de segredos.

Resumo
No geral, o seu código é muito bom e está em um excelente caminho para o desenvolvimento de software profissional. Ele demonstra uma compreensão clara de como estruturar um projeto e usar ferramentas modernas para escrever código limpo e modular. As sugestões acima são apenas os próximos passos na sua jornada de aprendizado, mostrando como tornar o código ainda mais robusto e preparado para desafios maiores.