Como a inversão de dependência está relacionada a funções de ordem superior?

41

Hoje, acabei de ver este artigo que descreve a relevância do princípio SOLID em F # development-

F # e princípios de design - SOLID

E enquanto abordava o último - "princípio de inversão de dependência", o autor disse:

Do ponto de vista funcional, esses contêineres e conceitos de injeção podem ser resolvidos com uma função simples de ordem superior ou um padrão do tipo buraco no meio, que é incorporado diretamente no idioma.

Mas ele não explicou mais. Então, minha pergunta é: como a inversão de dependência está relacionada a funções de ordem superior?

Gulshan
fonte

Respostas:

38

Inversão de dependência no OOP significa que você codifica contra uma interface que é fornecida por uma implementação em um objeto.

Os idiomas que suportam funções de idioma superior geralmente podem resolver problemas simples de inversão de dependência, passando o comportamento como uma função em vez de um objeto que implementa uma interface no sentido OO.

Nessas linguagens, a assinatura da função pode se tornar a interface e uma função é passada em vez de um objeto tradicional para fornecer o comportamento desejado. O buraco no padrão do meio é um bom exemplo disso.

Permite obter o mesmo resultado com menos código e mais expressividade, pois você não precisa implementar uma classe inteira que esteja em conformidade com uma interface (OOP) para fornecer o comportamento desejado para o chamador. Em vez disso, você pode simplesmente passar uma definição de função simples. Em resumo: o código geralmente é mais fácil de manter, mais expressivo e mais flexível quando se usa funções de ordem superior.

Um exemplo em c #

Abordagem tradicional:

public IEnumerable<Customer> FilterCustomers(IFilter<Customer> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter.Matches(customer))
        {
            yield return customer;
        }
    }
}

//now you've got to implement all these filters
class CustomerNameFilter : IFilter<Customer> /*...*/
class CustomerBirthdayFilter : IFilter<Customer> /*...*/

//the invocation looks like this
var filteredDataByName = FilterCustomers(new CustomerNameFilter("SomeName"), customers);
var filteredDataBybirthDay = FilterCustomers(new CustomerBirthdayFilter(SomeDate), customers);

Com funções de ordem superior:

public IEnumerable<Customer> FilterCustomers(Func<Customer, bool> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter(customer))
        {
            yield return customer;
        }
    }
}

Agora a implementação e a invocação se tornam menos complicadas. Não precisamos mais fornecer uma implementação do IFilter. Não precisamos mais implementar classes para os filtros.

var filteredDataByName = FilterCustomers(x => x.Name.Equals("CustomerName"), customers);
var filteredDataByBirthday = FilterCustomers(x => x.Birthday == SomeDateTime, customers);

Obviamente, isso já pode ser feito pelo LinQ em C #. Acabei de usar este exemplo para ilustrar que é mais fácil e flexível usar funções de ordem superior em vez de objetos que implementam uma interface.

Falcão
fonte
3
Belo exemplo. No entanto, como Gulshan, estou tentando descobrir mais sobre programação funcional e fiquei imaginando se esse tipo de "DI funcional" não sacrifica algum rigor e significância em comparação com o "DI orientado a objetos". A assinatura de ordem superior indica apenas que a função passada deve usar um Cliente como parâmetro e retornar um bool, enquanto a versão OO reforça o fato de que o objeto passado é um filtro (implementa o IFilter <Cliente>). Também torna explícita a noção de filtro, o que pode ser uma boa coisa se for um conceito central do Domínio (consulte DDD). O que você acha ?
precisa saber é o seguinte
2
@ Ian31: Este é realmente um tópico interessante! Tudo o que for passado para o FilterCustomer se comportará como algum tipo de filtro implicitamente. Quando o conceito de filtro é uma parte essencial do domínio e você precisa de regras de filtro complexas usadas várias vezes no sistema, é melhor encapsulá-las. Se não ou apenas em um grau muito baixo, eu buscaria simplicidade técnica e pragmatismo.
Falcon
5
@ian31: eu discordo completamente. Implementar IFilter<Customer>não é nenhuma aplicação. A função de ordem superior é muito mais flexível, o que é um grande benefício, e poder escrevê-las em linha é outro grande benefício. Lambdas também são muito mais fáceis de capturar variáveis ​​locais.
DeadMG
3
@ian31: A função também pode ser verificada em tempo de compilação. Você também pode escrever uma função, nomeá-la e passá-la como argumento, desde que cumpra o contrato óbvio (aceita cliente, retorna booleano). Você não precisa necessariamente passar uma expressão lambda. Então, você pode cobrir essa falta de expressividade até certo ponto. No entanto, o contrato e sua intenção não são tão claramente expressos. Essa é uma grande desvantagem às vezes. Em suma, é uma questão de expressividade, linguagem e encapsulamento. Eu acho que você tem que julgar cada caso por si só.
Falcon
2
se você se sentir fortemente sobre esclarecendo o significado semântico de uma função injetado, você pode em C assinaturas de função # nome usando delegados: public delegate bool CustomerFilter(Customer customer). em linguagens funcionais puras como Haskell, aliasing tipos é trivial:type customerFilter = Customer -> Bool
sara
8

Se você deseja alterar o comportamento de uma função

doThis(Foo)

você poderia passar outra função

doThisWith(Foo, anotherFunction)

o que implementa o comportamento que você deseja que seja diferente.

"doThisWith" é uma função de ordem superior porque assume outra função como argumento.

Por exemplo, você poderia ter

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)
LennyProgrammers
fonte
5

Resposta curta:

A Injeção de Dependência Clássica / Inversão de Controle usa interfaces de classe como espaço reservado para a funcionalidade dependente. Essa interface é implementada por uma classe.

Em vez de implementação de interface / classe, muitas dependências podem ser implementadas com mais facilidade com uma função delegada.

Você encontra um exemplo para ambos no c # em ioc-factory-pros-e-contras-para-interface-contra-delegados .

k3b
fonte
0

Compare isto:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = new LinkedList<String>();
for (String name : names) {
    if (name.startsWith("S")) {
        namesBeginningWithS.add(name);
    }
}

com:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = names.stream().filter(n <- n.startsWith("S")).collect();

A segunda versão é a maneira do Java 8 de reduzir o código padrão (looping etc.), fornecendo funções de ordem superior, como as filterque permitem passar o mínimo necessário (ou seja, a dependência a ser injetada - a expressão lambda).

Sridhar Sarnobat
fonte
0

Piggy-backing fora do exemplo LennyProgrammers ...

Uma das coisas que os outros exemplos perderam é que você pode usar funções de ordem superior junto com o aplicativo de função parcial (PFA) para vincular (ou "injetar") dependências em uma função (por meio de sua lista de argumentos) para criar uma nova função.

Se em vez de:

doThisWith(Foo, anotherFunction)

nós (para ser convencional na maneira como o PFA geralmente é feito) temos a função de trabalhador de baixo nível como (trocando a ordem do argumento):

doThisWith( anotherFunction, Foo )

Em seguida, podemos aplicar parcialmente o doThisWith da seguinte forma:

doThis = doThisWith( anotherFunction )  // note that "Foo" is still missing, argument list is partial

O que nos permite usar a nova função mais tarde:

doThis(Foo)

Ou até:

doThat = doThisWith( yetAnotherDependencyFunction )
...
doThat( Bar )

Veja também: https://ramdajs.com/docs/#partial

... e, sim, somadores / multiplicadores são exemplos sem imaginação. Um exemplo melhor seria uma função que recebe mensagens e as registra ou envia por e-mail, dependendo do que a função "consumidor" passou como dependência.

Estendendo essa idéia, listas de argumentos ainda mais longas podem ser progressivamente reduzidas a funções cada vez mais especializadas, com listas de argumentos cada vez mais curtas e, é claro, qualquer uma dessas funções pode ser passada para outras funções como dependências a serem parcialmente aplicadas.

OOP é bom se você precisar de um conjunto de coisas com várias operações estreitamente relacionadas, mas se transforma em make-work para criar um monte de classes, cada uma com um único método público "do it", como "O Reino dos Substantivos".

Roboprog
fonte