O gerenciamento de dependências é um grande problema no OOP pelos dois motivos a seguir:
- O forte acoplamento de dados e código.
- Uso onipresente de efeitos colaterais.
A maioria dos programadores de OO considera o acoplamento rígido de dados e código totalmente benéfico, mas isso tem um custo. Gerenciar o fluxo de dados através das camadas é uma parte inevitável da programação em qualquer paradigma. O acoplamento de seus dados e código adiciona o problema adicional de que, se você quiser usar uma função em um determinado momento, precisará encontrar uma maneira de levar seu objeto a esse ponto.
O uso de efeitos colaterais cria dificuldades semelhantes. Se você usa um efeito colateral para algumas funcionalidades, mas deseja poder trocar sua implementação, praticamente não há outra opção a não ser injetar essa dependência.
Considere como exemplo um programa de spammer que rastreia páginas da Web em busca de endereços de e-mail e as envia por e-mail. Se você tem uma mentalidade de DI, agora está pensando nos serviços que irá encapsular atrás das interfaces e quais serviços serão injetados onde. Vou deixar esse design como um exercício para o leitor. Se você tem uma mentalidade de FP, agora está pensando nas entradas e saídas da camada mais baixa de funções, como:
- Insira um endereço de página da web, produza o texto dessa página.
- Insira o texto de uma página, produza uma lista de links dessa página.
- Insira o texto de uma página, produza uma lista de endereços de email nessa página.
- Insira uma lista de endereços de email, produza uma lista de endereços de email com duplicatas removidas.
- Insira um endereço de email, envie um email de spam para esse endereço.
- Insira um email de spam, produza os comandos SMTP para enviar esse email.
Quando você pensa em termos de entradas e saídas, não há dependências de funções, apenas dependências de dados. É isso que os torna tão fáceis de realizar testes unitários. Sua próxima camada organiza a saída de uma função para ser inserida na entrada da próxima e pode facilmente trocar as várias implementações, conforme necessário.
Em um sentido muito real, a programação funcional naturalmente o incentiva a sempre inverter suas dependências de funções e, portanto, você normalmente não precisa tomar nenhuma medida especial para fazê-lo após o fato. Quando você faz isso, ferramentas como funções de ordem superior, fechamentos e aplicativos parciais facilitam a realização com menos clichês.
Observe que não são as próprias dependências que são problemáticas. São as dependências que apontam para o lado errado. A próxima camada acima pode ter uma função como:
processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses
É perfeitamente aceitável que essa camada tenha dependências codificadas dessa maneira, porque seu único objetivo é colar as funções da camada inferior. Trocar uma implementação é tão simples quanto criar uma composição diferente:
processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses
Essa recomposição fácil é possível devido à falta de efeitos colaterais. As funções da camada inferior são completamente independentes uma da outra. A próxima camada acima pode escolher qual processText
é realmente usada com base em algumas configurações do usuário:
actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText
Novamente, não é um problema, porque todas as dependências apontam para um caminho. Não precisamos inverter algumas dependências para que todas apontem da mesma maneira, porque funções puras já nos forçaram a fazê-lo.
Observe que você pode tornar isso muito mais acoplado passando config
para a camada mais baixa, em vez de verificá-la na parte superior. O FP não impede que você faça isso, mas tende a torná-lo muito mais irritante se você tentar.