Uma interface que expõe funções assíncronas é uma abstração com vazamento?

13

Estou lendo o livro Princípios, práticas e padrões de injeção de dependência e li sobre o conceito de abstração com vazamento, que é bem descrito no livro.

Hoje em dia estou refatorando uma base de código C # usando injeção de dependência, para que chamadas assíncronas sejam usadas em vez de bloquear. Fazendo isso, estou considerando algumas interfaces que representam abstrações em minha base de código e que precisam ser redesenhadas para que chamadas assíncronas possam ser usadas.

Como exemplo, considere a seguinte interface representando um repositório para usuários do aplicativo:

public interface IUserRepository 
{
  Task<IEnumerable<User>> GetAllAsync();
}

De acordo com a definição do livro, uma abstração com vazamento é uma abstração projetada com uma implementação específica em mente, para que alguns detalhes da implementação "vazem" pela própria abstração.

Minha pergunta é a seguinte: podemos considerar uma interface projetada com async em mente, como IUserRepository, como um exemplo de uma abstração com vazamento?

É claro que nem todas as implementações possíveis têm algo a ver com assincronia: somente as implementações fora de processo (como uma implementação SQL), mas um repositório na memória não requer assincronia (na verdade, implementar uma versão na memória da interface é provavelmente mais difícil se a interface expuser métodos assíncronos, por exemplo, você provavelmente precisará retornar algo como Task.CompletedTask ou Task.FromResult (usuários) nas implementações do método).

O que você acha disso ?

Enrico Massone
fonte
@ Neil Eu provavelmente entendi o ponto. Uma interface que expõe métodos que retornam Tarefa ou Tarefa <T> não é uma abstração com vazamento per se, é simplesmente um contrato com uma assinatura envolvendo tarefas. Um método que retorna uma tarefa ou tarefa <T> não implica em uma implementação assíncrona (por exemplo, se eu criar uma tarefa concluída usando Task.CompletedTask, não estou executando uma implementação assíncrona). Vice-versa, a implementação assíncrona em C # requer que o tipo de retorno de um método assíncrono seja do tipo Tarefa ou Tarefa <T>. Em outras palavras, o único aspecto "com vazamento" da minha interface é o sufixo assíncrono nos nomes
Enrico Massone
@ Neil, na verdade, existe uma diretriz de nomenclatura que afirma que todos os métodos assíncronos devem ter um nome terminado em "Async". Mas isso não implica que um método que retorne uma Tarefa ou uma Tarefa <T> deva ser nomeado com o sufixo Async, pois pode ser implementado usando nenhuma chamada assíncrona.
Enrico Massone
6
Eu argumentaria que a 'assíncrona' de um método é indicada pelo fato de que ele retorna a Task. As diretrizes para o sufixo de métodos assíncronos com a palavra async foram distinguir entre chamadas de API idênticas (o envio de C # não pode ser baseado no tipo de retorno). Na nossa empresa, reunimos tudo.
richzilla
Há várias respostas e comentários explicando por que a natureza assíncrona do método faz parte da abstração. Uma pergunta mais interessante é como uma API de linguagem ou programação pode separar a funcionalidade de um método da maneira como é executado, a ponto de não precisar mais dos valores de retorno de tarefa ou marcadores assíncronos? O pessoal da programação funcional parece ter percebido isso melhor. Considere como os métodos assíncronos são definidos no F # e em outros idiomas.
Frank Hileman
2
:-) -> O "pessoal de programação funcional" ha. O assíncrono não é mais vazado que o síncrono, apenas parece assim porque estamos acostumados a escrever código de sincronização por padrão. Se todos nós codificarmos assíncrono por padrão, uma função síncrona pode parecer com vazamento.
StarTrekRedneck 15/01/19

Respostas:

8

Pode-se, é claro, invocar a lei das abstrações com vazamento , mas isso não é particularmente interessante porque postula que todas as abstrações são com vazamento. Pode-se argumentar a favor e contra essa conjectura, mas não ajuda se não compartilharmos uma compreensão do que queremos dizer com abstração e do que queremos dizer com vazamento . Portanto, primeiro tentarei delinear como vejo cada um destes termos:

Abstrações

Minha definição favorita de abstrações é derivada do APPP de Robert C. Martin :

"Uma abstração é a amplificação do essencial e a eliminação do irrelevante".

Assim, as interfaces não são, em si mesmas, abstrações . Eles são apenas abstrações se trazem à tona o que importa e ocultam o resto.

Leaky

O livro Princípios, padrões e práticas de injeção de dependência define o termo abstração com vazamento no contexto de injeção de dependência (DI). O polimorfismo e os princípios do SOLID desempenham um grande papel nesse contexto.

A partir do Princípio de Inversão de Dependência (DIP), segue-se, novamente citando APPP, que:

"clientes [...] possuem as interfaces abstratas"

O que isso significa é que os clientes (código de chamada) definem as abstrações necessárias e, em seguida, você implementa essa abstração.

Uma abstração com vazamento , na minha opinião, é uma abstração que viola o DIP, incluindo de alguma forma algumas funcionalidades que o cliente não precisa .

Dependências síncronas

Um cliente que implementa uma parte da lógica de negócios normalmente usa o DI para se dissociar de certos detalhes de implementação, como, geralmente, bancos de dados.

Considere um objeto de domínio que lida com uma solicitação de reserva de restaurante:

public class MaîtreD : IMaîtreD
{
    public MaîtreD(int capacity, IReservationsRepository repository)
    {
        Capacity = capacity;
        Repository = repository;
    }

    public int Capacity { get; }
    public IReservationsRepository Repository { get; }

    public int? TryAccept(Reservation reservation)
    {
        var reservations = Repository.ReadReservations(reservation.Date);
        int reservedSeats = reservations.Sum(r => r.Quantity);

        if (Capacity < reservedSeats + reservation.Quantity)
            return null;

        reservation.IsAccepted = true;
        return Repository.Create(reservation);
    }
}

Aqui, a IReservationsRepositorydependência é determinada exclusivamente pelo cliente, a MaîtreDclasse:

public interface IReservationsRepository
{
    Reservation[] ReadReservations(DateTimeOffset date);
    int Create(Reservation reservation);
}

Essa interface é totalmente síncrona, pois a MaîtreDclasse não precisa ser assíncrona.

Dependências assíncronas

Você pode alterar facilmente a interface para ser assíncrona:

public interface IReservationsRepository
{
    Task<Reservation[]> ReadReservations(DateTimeOffset date);
    Task<int> Create(Reservation reservation);
}

A MaîtreDclasse, no entanto, não precisa que esses métodos sejam assíncronos; portanto, agora o DIP é violado. Considero isso uma abstração com vazamento, porque um detalhe de implementação força o cliente a mudar. O TryAcceptmétodo agora também deve se tornar assíncrono:

public async Task<int?> TryAccept(Reservation reservation)
{
    var reservations =
        await Repository.ReadReservations(reservation.Date);
    int reservedSeats = reservations.Sum(r => r.Quantity);

    if (Capacity < reservedSeats + reservation.Quantity)
        return null;

    reservation.IsAccepted = true;
    return await Repository.Create(reservation);
}

Não há lógica inerente para que a lógica do domínio seja assíncrona, mas para oferecer suporte à assincronia da implementação, isso agora é necessário.

Melhores opções

Na NDC Sydney 2018 , dei uma palestra sobre esse tópico . Nele, também descrevo uma alternativa que não vaza. Também darei essa palestra em várias conferências em 2019, mas agora com o novo título de injeção Async .

Pretendo também publicar uma série de postagens no blog para acompanhar a palestra. Esses artigos já estão escritos e estão na minha fila de artigos, aguardando publicação, portanto, fique atento.

Mark Seemann
fonte
Para mim, isso é uma questão de intenção. Se minha abstração parece que deveria se comportar de uma maneira, mas algum detalhe ou restrição interrompe a abstração como apresentada, é uma abstração com vazamento. Mas, neste caso, estou explicitamente apresentando a você que a operação é assíncrona - não é isso que estou tentando abstrair. Na minha opinião, isso é distinto do seu exemplo em que estou (sabiamente ou não) tentando abstrair o fato de que há um banco de dados SQL e ainda exponho uma cadeia de conexão. Talvez seja uma questão de semântica / perspectiva.
Ant P
Portanto, podemos dizer que uma abstração nunca está com vazamento "per se"; em vez disso, é com vazamento se alguns detalhes de uma implementação específica vazarem dos membros expostos e forçar o consumidor a alterar sua implementação, a fim de satisfazer o formato da abstração .
Enrico Massone
2
Curiosamente, o ponto que você destacou em sua explicação é um dos pontos mais incompreendidos de toda a história da injeção de dependência. Às vezes, os desenvolvedores esquecem o princípio da inversão de dependência e tentam projetar a abstração primeiro e depois adaptam o design do consumidor para lidar com a abstração em si. Em vez disso, o processo deve ser feito na ordem inversa.
Enrico Massone
11

Não é uma abstração com vazamento.

Ser assíncrono é uma alteração fundamental na definição de uma função - significa que a tarefa não está concluída quando a chamada retorna, mas também significa que o fluxo do programa continuará quase imediatamente, não com um atraso prolongado. Uma função assíncrona e uma síncrona que executam a mesma tarefa são funções essencialmente diferentes. Ser assíncrono não é um detalhe de implementação. Faz parte da definição de uma função.

Se a função expusesse como a função foi feita assíncrona, isso seria um vazamento. Você (não deve / não precisa se preocupar) com a implementação.

gnasher729
fonte
5

O asyncatributo de um método é uma tag que indica que é necessário cuidado e manuseio específicos. Como tal, ele precisa vazar para o mundo. As operações assíncronas são extremamente difíceis de compor corretamente, portanto, é importante dar um aviso ao usuário da API.

Se, em vez disso, sua biblioteca gerenciava adequadamente toda a atividade assíncrona dentro de si mesma, você poderia se permitir não deixar async"vazar" a API.

Existem quatro dimensões de dificuldade no software: dados, controle, espaço e tempo. As operações assíncronas abrangem todas as quatro dimensões, necessitando, portanto, de mais cuidados.

BobDalgleish
fonte
Concordo com o seu sentimento, mas "vazamento" implica algo ruim, que é a intenção do termo "abstração com vazamento" - algo indesejável na abstração. No caso de async vs sync, nada está vazando.
StarTrekRedneck 15/01/19
2

uma abstração com vazamento é uma abstração projetada com uma implementação específica em mente, para que alguns detalhes da implementação "vazem" pela própria abstração.

Não é bem assim. Uma abstração é uma coisa conceitual que ignora alguns elementos de uma coisa ou problema concreto mais complicado (para tornar a coisa / problema mais simples, tratável ou devido a algum outro benefício). Como tal, é necessariamente diferente da coisa / problema real e, portanto, ficará com vazamento em alguns subconjuntos de casos (ou seja, todas as abstrações são vazantes, a única questão é até que ponto - ou seja, em quais casos a abstração útil para nós, qual é o seu domínio de aplicabilidade).

Dito isto, quando se trata de abstrações de software, às vezes (ou talvez com frequência suficiente?) Os detalhes que escolhemos ignorar não podem ser ignorados porque afetam algum aspecto do software que é importante para nós (desempenho, manutenção, ...) . Portanto, uma abstração com vazamento é uma abstração projetada para ignorar certos detalhes (sob a suposição de que era possível e útil fazê-lo), mas, em seguida, verificou-se que alguns desses detalhes são significativos na prática (eles não podem ser ignorados, então eles "vazar").

Portanto, uma interface que expõe um detalhe de uma implementação não é perceptível por si só (ou melhor, uma interface, vista isoladamente, não é por si só uma abstração com vazamento); em vez disso, o vazamento depende do código que implementa a interface (é realmente capaz de suportar a abstração representada pela interface) e também das suposições feitas pelo código do cliente (que equivalem a uma abstração conceitual que complementa a expressa por a interface, mas não pode ser expressa em código (por exemplo, os recursos da linguagem não são expressivos o suficiente, para que possamos descrevê-la nos documentos, etc.).

Filip Milovanović
fonte
2

Considere os seguintes exemplos:

Este é um método que define o nome antes de retornar:

public void SetName(string name)
{
    _dataLayer.SetName(name);
}

Este é um método que define o nome. O chamador não pode assumir que o nome esteja definido até que a tarefa retornada seja concluída ( IsCompleted= true):

public Task SetName(string name)
{
    return _dataLayer.SetNameAsync(name);
}

Este é um método que define o nome. O chamador não pode assumir que o nome esteja definido até que a tarefa retornada seja concluída ( IsCompleted= true):

public async Task SetName(string name)
{
    await _dataLayer.SetNameAsync(name);
}

P: Qual deles não pertence aos outros dois?

R: O método assíncrono não é o único que está sozinho. O que fica sozinho é o método que retorna nulo.

Para mim, o "vazamento" aqui não é a asyncpalavra - chave; é o fato de que o método retorna uma tarefa. E isso não é um vazamento; faz parte do protótipo e parte da abstração. Um método assíncrono que retorna uma tarefa faz exatamente a mesma promessa feita por um método síncrono que retorna uma tarefa.

Portanto, não, eu não acho que a introdução de asyncformas seja uma abstração com vazamento em si mesma. Mas pode ser necessário alterar o protótipo para retornar uma tarefa, que "vaza" alterando a interface (a abstração). E como faz parte da abstração, não é um vazamento, por definição.

John Wu
fonte
0

Essa é uma abstração com vazamento se e somente se você não pretende que todas as classes de implementação criem uma chamada assíncrona. Você pode criar várias implementações, por exemplo, uma para cada tipo de banco de dados suportado, e isso seria perfeitamente aceitável, supondo que você nunca precise saber a implementação exata que está sendo usada em todo o programa.

E embora você não possa impor estritamente uma implementação assíncrona, o nome indica que deveria ser. Se as circunstâncias mudarem, e pode ser uma chamada síncrona por qualquer motivo, talvez você precise considerar uma mudança de nome; portanto, meu conselho seria fazer isso apenas se você não achar que isso seja muito provável no futuro.

Neil
fonte
0

Aqui está um ponto de vista oposto.

Nós não voltamos de retornar Foopara retornar Task<Foo>porque começamos a querer o em Taskvez de apenas o Foo. É verdade que às vezes interagimos com o Taskcódigo, mas na maioria dos casos o ignoramos e apenas usamos o Foo.

Além disso, geralmente definimos interfaces para oferecer suporte ao comportamento assíncrono, mesmo quando a implementação pode ou não ser assíncrona.

De fato, uma interface que retorna a Task<Foo>informa que a implementação é possivelmente assíncrona, seja ela realmente ou não, mesmo que você possa ou não se importar. Se uma abstração nos diz mais do que precisamos saber sobre sua implementação, é um vazamento.

Se nossa implementação não é assíncrona, nós a alteramos para ser assíncrona e, em seguida, temos que mudar a abstração e tudo o que a usa, isso é uma abstração muito vazada.

Isso não é um julgamento. Como outros já apontaram, todas as abstrações vazam. Este tem um impacto maior porque requer um efeito cascata de async / waiting em todo o nosso código apenas porque em algum lugar no final dele pode haver algo que é realmente assíncrono.

Isso soa como uma reclamação? Essa não é minha intenção, mas acho que é uma observação precisa.

Um ponto relacionado é a afirmação de que "uma interface não é uma abstração". O que Mark Seeman afirmou sucintamente foi abusado um pouco.

A definição de "abstração" não é "interface", mesmo no .NET. As abstrações podem assumir muitas outras formas. Uma interface pode ser uma abstração ruim ou pode espelhar sua implementação tão de perto que, em certo sentido, dificilmente é uma abstração.

Mas nós absolutamente usamos interfaces para criar abstrações. Então, jogar fora "interfaces não são abstrações" porque uma pergunta menciona interfaces e abstrações não é esclarecedor.

Scott Hannen
fonte
-2

É GetAllAsync()realmente assíncrono? Quero dizer que "assíncrono" está no nome, mas isso pode ser removido. Então, pergunto novamente ... É impossível implementar uma função que retorne uma Task<IEnumerable<User>>que seja resolvida de forma síncrona?

Eu não conheço as especificidades do Tasktipo .Net , mas se for impossível implementar a função de forma síncrona, com certeza é uma abstração com vazamento (dessa maneira), mas caso contrário não. Eu não sei que se fosse um IObservable, em vez de uma tarefa, ele poderia ser implementado de forma síncrona ou assíncrona para que nada fora da função conhece e, portanto, não está vazando esse fato particular.

Daniel T.
fonte
Task<T> significa assíncrono. Você começa o objeto tarefa imediatamente, mas pode ter que esperar para a seqüência de usuários
Caleth
Pode ter que esperar não significa que é necessariamente assíncrono. Deve esperar significaria assíncrona. Presumivelmente, se a tarefa subjacente já tiver sido executada, você não precisará esperar.
Daniel T.