Estamos abusando de métodos estáticos?

13

Há alguns meses, comecei a trabalhar em um novo projeto e, ao analisar o código, percebi a quantidade de métodos estáticos usados. Não apenas métodos utilitários collectionToCsvString(Collection<E> elements), mas também muita lógica de negócios é mantida neles.

Quando perguntei ao sujeito responsável pela lógica por trás disso, ele disse que era uma maneira de escapar da tirania de Spring . É algo em torno desse processo de pensamento: para implementar um método de criação de recibos de clientes, poderíamos ter um serviço

@Service
public class CustomerReceiptCreationService {

    public CustomerReceipt createReceipt(Object... args) {
        CustomerReceipt receipt = new CustomerReceipt();
        // creation logic
        return receipt;
    }
}

Agora, o sujeito disse que não gosta de ter classes desnecessariamente gerenciadas pelo Spring, basicamente porque impõe a restrição de que as classes clientes devam ser elas próprias. Acabamos tendo tudo gerenciado pelo Spring, que praticamente nos obriga a trabalhar com objetos sem estado de maneira processual. Mais ou menos o que está indicado aqui https://www.javacodegeeks.com/2011/02/domain-driven-design-spring-aspectj.html

Então, em vez do código acima, ele tem

public class CustomerReceiptCreator {

    public static CustomerReceipt createReceipt(Object... args) {
        CustomerReceipt receipt = new CustomerReceipt();
        // creation logic
        return receipt;
    }
}

Eu poderia argumentar a ponto de evitar que o Spring gerencie nossas aulas sempre que possível, mas o que não vejo é o benefício de ter tudo estático. Esses métodos estáticos também são sem estado, portanto, também não são muito OO. Eu me sentiria mais confortável com algo como

new CustomerReceiptCreator().createReceipt()

Ele afirma que os métodos estáticos têm alguns benefícios extras. Nomeadamente:

  • Mais fácil de ler. Importe o método estático e precisamos nos preocupar apenas com a ação, não qual a classe que está fazendo isso.
  • É obviamente um método livre de chamadas de banco de dados, tão barato em termos de desempenho; e é bom deixar claro, para que o cliente em potencial precise entrar no código e verificar isso.
  • Mais fácil de escrever testes.

Mas sinto que há algo que não está completamente certo nisso, então gostaria de ouvir alguns pensamentos de desenvolvedores mais experientes sobre isso.

Então, minha pergunta é: quais são as possíveis armadilhas dessa maneira de programação?

user3748908
fonte
4
O staticmétodo que você está ilustrando acima é apenas um método comum de fábrica. Tornar estáticos os métodos de fábrica é a convenção geralmente aceita, por vários motivos convincentes. Se o método de fábrica é apropriado aqui é outra questão.
Robert Harvey

Respostas:

23

Qual é a diferença entre new CustomerReceiptCreator().createReceipt()e CustomerReceiptCreator.createReceipt()? Praticamente nenhum. A única diferença significativa é que o primeiro caso tem uma sintaxe consideravelmente mais estranha. Se você segue o primeiro na crença de que, de alguma forma, evitar métodos estáticos torna seu código melhor OO, você está muito enganado. Criar um objeto apenas para chamar um método único é um método estático por sintaxe obtusa.

Onde as coisas ficam diferentes é quando você injeta, em CustomerReceiptCreatorvez de injetar new. Vamos considerar um exemplo:

class OrderProcessor {
    @Inject CustomerReceiptCreator customerReceiptCreator;

    void processOrder(Order order) {
        ...
        CustomerReceipt receipt = customerReceiptCreator.createReceipt(order);
        ...
    }
}

Vamos comparar esta versão do método estático:

void processOrder(Order order) {
    ...
    CustomerReceipt receipt = CustomerReceiptCreator.createReceipt(order);
    ...
}

A vantagem da versão estática é que eu posso lhe dizer rapidamente como ela interage com o resto do sistema. Não faz. Se eu não usei variáveis ​​globais, sei que o resto do sistema não foi alterado de alguma forma. Sei que nenhuma outra parte do sistema pode afetar o recebimento aqui. Se usei objetos imutáveis, sei que a ordem não mudou e o createReceipt é uma função pura. Nesse caso, posso mover / remover / etc livremente essa chamada sem me preocupar com efeitos imprevisíveis aleatórios em outros lugares.

Não posso fazer as mesmas garantias se injetar o CustomerReceiptCreator. Pode ter um estado interno que é alterado pela chamada, posso ser afetado ou alterar outro estado. Pode haver relações imprevisíveis entre declarações em minha função, de modo que alterar a ordem introduza bugs surpreendentes.

Por outro lado, o que acontece se de CustomerReceiptCreatorrepente precisar de uma nova dependência? Digamos que ele precise verificar um sinalizador de recurso. Se estivéssemos injetando, poderíamos fazer algo como:

public class CustomerReceiptCreator {
    @Injected FeatureFlags featureFlags;

    public CustomerReceipt createReceipt(Order order) {
        CustomerReceipt receipt = new CustomerReceipt();
        // creation logic
        if (featureFlags.isFlagSet(Flags::FOOBAR)) {
           ...
        }
        return receipt;
    }
}

Então terminamos, porque o código de chamada será injetado com um CustomerReceiptCreatorque será automaticamente injetado a FeatureFlags.

E se estivéssemos usando um método estático?

public class CustomerReceiptCreator {
    public static CustomerReceipt createReceipt(Order order, FeatureFlags featureFlags) {
        CustomerReceipt receipt = new CustomerReceipt();
        // creation logic
        if (featureFlags.isFlagSet(Flags::FOOBAR)) {
           ...
        }
        return receipt;
    }
}

Mas espere! o código de chamada também precisa ser atualizado:

void processOrder(Order order) {
    ...
    CustomerReceipt receipt = CustomerReceiptCreator.createReceipt(order, featureFlags);
    ...
}

Obviamente, isso ainda deixa a questão de onde processOrderela FeatureFlagsvem. Se tivermos sorte, a trilha termina aqui; caso contrário, a necessidade de passar pelo FeatureFlags é aumentada ainda mais na pilha.

Há uma troca aqui. Métodos estáticos que exigem a passagem explícita das dependências, o que resulta em mais trabalho. O método injetado reduz o trabalho, mas torna as dependências implícitas e, assim, ocultas, dificultando o raciocínio do código.

Winston Ewert
fonte