Como melhorar um programa que modifica uma lista de arquivos?

Boa noite a todos,

Eu tenho a seguinte classe, cujo objetivo é basicamente receber uma lista de arquivos e filtrá-la segundo vários critérios. O objetivo final é simplesmente poder printar essas categorias:

public Classificador {
List<Path> listaInicialDeArquivos = new ArrayList<>();
List<Path> arquivosDaCategoria1 = new ArrayList<>();
List<Path> arquivosDaCategoria2 = new ArrayList<>();

public Classificador(String pathDaPasta) {
   listaInicialDeArquivos = lerArquivos(pathDaPasta);
   arquivosDaCategoria1 = filtrarArquivosDaCategoria1();
   arquivosDaCategoria2 = filtrarArquivosDaCategoria2();
   // chamadas de outros métodos que modificam as listaInicialdeArquivos e as Listas de categorias
}

public void filtrarArquivosDaCategoria1 () {
     // método que remove arquivos da listaInicialDeArquivos e os adiciona à arquivosDaCategoria1 para serem printados
}

public static void main(String[] args) {
Classificador c = new Classificador("/minhaPasta");
System.out.print(c.arquivosDaCategoria1);
}
}

Como as listas em que opero são variáveis da minha instância, com escopo de classe, eu (presumo) que nenhum dos meus métodos precisam receber parâmetros nem retornar nenhum valor.

Eu estou achando isso muito estranho? Isso é uma prática ruim? Como eu poderia escrever testes unitários para uma classe assim?

Agradeco desde já qualquer palpite!

É uma boa prática, na minha opinião. Pois vc estabelece responsabilidade para sua classe, o que faz sentido para o escopo dela, e facilita a criação de testes também.

Uma observação que tenho a fazer é sobre esse comentário:

// método que remove arquivos da listaInicialDeArquivos e os adiciona à arquivosDaCategoria1 para serem printados

Nâo há pq remover da lista inicial, pois ela é a base para o funcionamento da classe Classificador. Tudo o que for originado, será com base nessa lista inicial.


Apenas mudaria algumas coisas na sua classe:

  • Colocaria os métodos que filtram como privado e deixaria exposto apenas os getters das listas filtradas;
  • Pensaria em deixar as listas filtradas para serem criadas sob demanda;
  • Colocaria todas as listas como private;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class Classificador {
    private final List<Path> listaInicialDeArquivos;
    private final List<Path> arquivosDaCategoria1;
    private final List<Path> arquivosDaCategoria2;

    public Classificador(String pathDaPasta) {
        listaInicialDeArquivos = lerArquivos(pathDaPasta);

        // filtrar os arquivos que começam com "exemplo"
        arquivosDaCategoria1 = filtrar((Path path) -> path.startsWith("exemplo"));

        // filtrar os arquivos que são PDF (terminam com ".pdf")
        arquivosDaCategoria2 = filtrar((Path path) -> path.endsWith(".pdf"));
    }

    private List<Path> lerArquivos(String pathDaPasta) {
        return Collections.emptyList(); // lista os arquivos do diretório
    }

    private List<Path> filtrar(Predicate<Path> predicate) {
        return listaInicialDeArquivos.stream()
            .filter(predicate)
            .collect(Collectors.toList());
    }

    public List<Path> getArquivosDaCategoria1() {
        return arquivosDaCategoria1;
    }

    public List<Path> getArquivosDaCategoria2() {
        return arquivosDaCategoria2;
    }
}
1 curtida

Nossa, muito lindo essa espécie de Strategy Patter com Streams!
Muito obrigada!

Agora sobre os testes unitários, já que o método de filtrar vai ficar privado eu tenho que testar a Classe como um todo, certo?

No teste, vc criaria a classe Classificador passando um path que tenha alguns arquivos de acordo com o que vc quiser validar e faria os assets usando os getters da classe.

Por exemplo, de acordo com essa classe que criei que filtra os arquivos que começam com “exemplo”, se tu criar uma pasta que tem vários arquivos que começam com exemplo e passar essa pasta para a classe Classificacao, então o método getArquivosDaCategoria1() deve retornar os paths desses arquivos.

Sem ter todos os requisitos de um caso concreto (ou seja, uma necessidade real em um sistema de verdade), podemos ficar debatendo eternamente sobre como melhorar.

Por exemplo, se eu faço new Classificador("/minhaPasta"), então esta instância só vai ler os arquivos daquela pasta (não parece ter como mudar). Se a intenção era essa mesmo, ok. Senão, deveria ter um modo de alterar a pasta.

Se as categorias são fixas e vc só quer elas, e não precisa de todos os outros arquivos, então também não precisaria da lista contendo todos. Outro detalhe é que, da forma atual, vc primeiro cria a lista com todos, e depois filtra ela duas vezes, uma para cada categoria. Ou seja, a lista é percorrida duas vezes. Mas e se tivesse mais critérios? Aí teria que percorrer várias vezes, o que não é uma solução muito escalável.

Enfim, daria para filtrar tudo de uma vez, e sem precisar criar a lista com todos (que não parece ser necessária):

public class Classificador {
    private final List<Path> arquivosDaCategoria1 = new ArrayList<>();
    private final List<Path> arquivosDaCategoria2 = new ArrayList<>();

    public Classificador(String pathDaPasta) {
        try (Stream<Path> paths = Files.walk(Paths.get(pathDaPasta))) {
            paths.forEach(path -> { // para cada arquivo na pasta
                String nome = path.getFileName().toString();
                if (nome.startsWith("exemplo")) { // se começa com "exemplo", adiciona na categoria 1
                    arquivosDaCategoria1.add(path);
                }
                if (nome.endsWith(".pdf")) { // se termina com ".pdf.", adiciona na categoria 2
                    arquivosDaCategoria2.add(path);
                }
            });
        } catch (IOException e) {
            // erro, mostra alguma mensagem (ou relança a exceção, não deixando criar a instância)
        }
    }

    public List<Path> getArquivosDaCategoria1() {
        return arquivosDaCategoria1;
    }
    public List<Path> getArquivosDaCategoria2() {
        return arquivosDaCategoria2;
    }
}

Também arrumei a verificação, pois se eu fizer path.startsWith, ele não se comporta da forma esperada. Por exemplo, se eu verificar a pasta abc, o path do arquivo será abc/exemplo, ou seja, nunca começará com “exemplo”. Por isso precisa fazer o ajuste (saiba mais aqui).


Indo um pouco além, a classe seria mais útil e flexível se pudesse receber os critérios no construtor. Algo assim:

public class Classificador {
    // mapeia cada critério para sua respectiva lista
    private Map<String, List<Path>> categorias = new HashMap<>();
    
    public Classificador(String pathDaPasta, Map<String, Predicate<Path>> criterios) {
        try (Stream<Path> paths = Files.walk(Paths.get(pathDaPasta))) {
            paths.forEach(path -> { // para cada arquivo
                // verifica se ele se encaixa em cada um dos critérios
                // em caso afirmativo, adiciona na respectiva lista
                for (Map.Entry<String, Predicate<Path>> e : criterios.entrySet()) {
                    if (e.getValue().test(path)) {
                        categorias.computeIfAbsent(e.getKey(), k -> new ArrayList<>()).add(path);
                    }
                }
            });
        } catch (IOException e) {
            // erro, mostra alguma mensagem (ou relança a exceção, não deixando criar a instância)
        }
    }

    public List<Path> getArquivos(String categoria) {
        return categorias.getOrDefault(categoria, Collections.emptyList());
    }
}

Um exemplo de uso:

Map<String, Predicate<Path>> criterios = Map.of(
        "categoria 1", path -> path.getFileName().toString().startsWith("exemplo"),
        "categoria 2", path -> path.getFileName().toString().endsWith(".pdf"),
        "categoria 3", path -> path.toFile().length() > 100 // arquivo maior que 100 bytes
        // adicione quantas categorias quiser
);
Classificador c = new Classificador("/tmp/hugo/", criterios);
for (String categoria : criterios.keySet()) {
    System.out.println(categoria + ": " + c.getArquivos(categoria));
}

E como eu já disse, sem saber os requisitos (se precisa mesmo ter vários critérios configuráveis, etc), podemos debater infinitamente sobre formas de melhorar…

1 curtida