As diretrizes do async / await usage em C # não estão em contradição com os conceitos de boa arquitetura e camadas de abstração?

101

Esta questão diz respeito à linguagem C #, mas espero que ela cubra outras linguagens como Java ou TypeScript.

A Microsoft recomenda as práticas recomendadas sobre o uso de chamadas assíncronas na rede. Entre essas recomendações, vamos escolher duas:

  • altere a assinatura dos métodos assíncronos para que eles retornem Tarefa ou Tarefa < > (no TypeScript, isso seria uma promessa < >)
  • altere os nomes dos métodos assíncronos para terminar com xxxAsync ()

Agora, ao substituir um componente síncrono de baixo nível por um async, isso afeta a pilha completa do aplicativo. Como o async / await tem um impacto positivo apenas se for usado "todo o caminho", isso significa que os nomes de assinatura e método de cada camada no aplicativo devem ser alterados.

Uma boa arquitetura geralmente envolve a colocação de abstrações entre cada camada, de modo que a substituição de componentes de baixo nível por outros não seja vista pelos componentes de nível superior. Em C #, as abstrações assumem a forma de interfaces. Se introduzirmos um novo componente assíncrono de baixo nível, cada interface na pilha de chamadas precisará ser modificada ou substituída por uma nova interface. A forma como um problema é resolvido (async ou sync) em uma classe de implementação não é mais oculta (abstraída) para os chamadores. Os chamadores precisam saber se são sincronizados ou assíncronos.

As melhores práticas de async / await estão em contradição com os princípios de "boa arquitetura"?

Isso significa que cada interface (digamos IEnumerable, IDataAccessLayer) precisa de sua contraparte assíncrona (IAsyncEnumerable, IAsyncDataAccessLayer) de forma que eles possam ser substituídos na pilha ao alternar para dependências assíncronas?

Se levarmos o problema um pouco mais longe, não seria mais simples assumir que cada método é assíncrono (para retornar uma Tarefa < & Prom < >), e para os métodos de sincronizar as chamadas assíncronas quando eles não são realmente assíncronos? Isso é algo esperado das futuras linguagens de programação?

    
por corentinaltepe 05.12.2018 / 09:22
fonte

5 respostas

109

Que cor é sua função?

Você pode estar interessado em de Bob Nystrom Qual é a sua função 1 .

Neste artigo, ele descreve uma linguagem fictícia onde:

  • Cada função tem uma cor: azul ou vermelho.
  • Uma função vermelha pode chamar funções azuis ou vermelhas, sem problemas.
  • Uma função azul só pode chamar funções azuis.

Embora fictício, isso acontece com bastante regularidade nas linguagens de programação:

  • Em C ++, um método "const" só pode chamar outros métodos "const" em this .
  • No Haskell, uma função não-IO só pode chamar funções não-IO.
  • Em C #, uma função de sincronização só pode chamar funções de sincronização 2 .

Como você percebeu, por causa dessas regras, as funções vermelhas tendem a espalhar ao redor da base de código. Você insere um, e pouco a pouco coloniza toda a base de código.

1 Bob Nystrom, além de blogar, também faz parte da equipe de Dart e escreveu esta pequena série Crafting Interpreters; altamente recomendado para qualquer aficionado por linguagem de programação / compilador.

2 Não é bem verdade, pois você pode chamar uma função assíncrona e bloquear até que ela retorne, mas ...

Limitação de idioma

Esta é, essencialmente, uma limitação de idioma / tempo de execução.

Linguagem com encadeamento M: N, por exemplo, como Erlang e Go, não tem async funções: cada função é potencialmente assíncrona e sua "fibra" será simplesmente suspensa, trocada e trocada novamente quando está pronto de novo.

O C # foi com um modelo de segmentação 1: 1 e, portanto, decidiu exibir a sincronicidade na linguagem para evitar o bloqueio acidental de threads.

Na presença de limitações de linguagem, as diretrizes de codificação precisam ser adaptadas.

    
por 05.12.2018 / 13:58
fonte
81

Você está certo, há uma contradição aqui, mas não são as "melhores práticas" sendo ruins. É porque a função assíncrona faz algo essencialmente diferente de uma síncrona. Em vez de esperar pelo resultado de suas dependências (geralmente algum IO), ele cria uma tarefa a ser manipulada pelo loop de eventos principal. Esta não é uma diferença que possa ser bem escondida sob abstração.

    
por 05.12.2018 / 10:58
fonte
6

Um método assíncrono se comporta de maneira diferente de um que é síncrono, como tenho certeza de que você está ciente. Em tempo de execução, converter uma chamada assíncrona em síncrona é trivial, mas o oposto não pode ser dito. Assim, portanto, a lógica torna-se, por que não fazemos métodos assíncronos de todos os métodos que podem requerer isso e deixar o chamador "converter" conforme necessário para um método síncrono?

Em certo sentido, é como ter um método que lança exceções e outro que é "seguro" e não será lançado mesmo em caso de erro. Em que ponto o codificador é excessivo para fornecer esses métodos que, de outra forma, podem ser convertidos um para o outro?

Nele existem duas escolas de pensamento: uma é criar múltiplos métodos, cada um chamando outro método possivelmente privado, permitindo a possibilidade de fornecer parâmetros opcionais ou pequenas alterações no comportamento, como ser assíncrono. A outra é minimizar os métodos de interface para o essencial, deixando que o chamador realize as modificações necessárias por conta própria.

Se você é da primeira escola, há uma certa lógica para dedicar uma classe a chamadas síncronas e assíncronas, a fim de evitar a duplicação de todas as chamadas. A Microsoft tende a favorecer essa escola de pensamento e, por convenção, a permanecer consistente com o estilo preferido pela Microsoft, você também teria que ter uma versão Async, da mesma maneira que as interfaces quase sempre começam com um "eu". Deixe-me enfatizar que não está errado , porque é melhor manter um estilo consistente em um projeto do que fazê-lo "da maneira certa" e mudar radicalmente o estilo para o desenvolvimento que você adicionar a um projeto.

Dito isso, tenho tendência a favorecer a segunda escola, que é a de minimizar os métodos de interface. Se eu acho que um método pode ser chamado de maneira assíncrona, o método para mim é assíncrono. O chamador pode decidir se espera ou não que essa tarefa termine antes de prosseguir. Se essa interface for uma interface para uma biblioteca, é mais razoável fazer isso para minimizar o número de métodos que você precisa reprovar ou ajustar. Se a interface for para uso interno no meu projeto, adicionarei um método para cada chamada necessária em todo o meu projeto para os parâmetros fornecidos e nenhum método "extra" e, mesmo assim, somente se o comportamento do método já não estiver coberto por um método existente.

No entanto, como muitas coisas neste campo, é basicamente subjetivo. Ambas as abordagens têm seus prós e contras. A Microsoft também iniciou a convenção de adicionar letras indicativas de tipo no início do nome da variável e "m_" para indicar que é um membro, levando a nomes de variáveis como m_pUser . Meu ponto é que nem mesmo a Microsoft é infalível e pode cometer erros também.

Dito isto, se o seu projeto está seguindo esta convenção Async, aconselho-o a respeitá-lo e continuar com o estilo. E somente quando você receber um projeto próprio, poderá escrevê-lo da melhor maneira que achar melhor.

    
por 05.12.2018 / 10:16
fonte
2

Vamos imaginar que haja uma maneira de permitir que você chame funções de maneira assíncrona sem alterar sua assinatura.

Isso seria muito legal e ninguém recomendaria que você mudasse seus nomes.

Mas, funções assíncronas reais, não apenas aquelas que aguardam outra função assíncrona, mas o nível mais baixo têm alguma estrutura específica para sua natureza assíncrona. eg

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

São esses dois bits de informação de que o código de chamada precisa para executar o código de maneira assíncrona, coisas como Task e async encapsular.

Existe mais de uma maneira de fazer funções assíncronas, async Task é apenas um padrão embutido no compilador via tipos de retorno para que você não precise vincular manualmente os eventos

    
por 05.12.2018 / 12:16
fonte
0

Vou abordar o ponto principal de uma forma menos moderna e mais genérica:

Aren't async/await best practices contradicting with "good architecture" principles?

Eu diria que isso depende da escolha que você faz no design de sua API e do que você permite ao usuário.

Se você quiser que uma função de sua API seja assíncrona, há pouco interesse em seguir a convenção de nomenclatura. Basta sempre retornar Tarefa < / Promessa < / Futuro < > / ... como tipo de retorno, é auto-documentado. Se quiser uma resposta sincronizada, ele ainda será capaz de fazer isso esperando, mas se ele sempre fizer isso, ele fará um pouco de clichê.

No entanto, se você fizer apenas a sincronização da API, isso significa que, se um usuário quiser que ele seja assíncrono, ele terá que gerenciar a parte assíncrona dele mesmo.

Isso pode fazer muito trabalho extra, mas também pode dar mais controle ao usuário sobre quantas chamadas simultâneas ele permite, tempo limite de uso, retry e assim por diante.

Em um sistema grande com uma API enorme, implementar a maioria deles para ser sincronizado por padrão pode ser mais fácil e eficiente do que gerenciar independentemente cada parte da API, especialmente se eles compartilharem recursos (sistema de arquivos, CPU, banco de dados, etc. .).

Na verdade, para as partes mais complexas, você poderia perfeitamente ter duas implementações da mesma parte de sua API, uma síncrona fazendo as coisas úteis, uma assíncrona confiando na síncrona, manipulando coisas e apenas gerenciando simultaneidade, cargas, timeouts e tenta novamente.

Talvez alguém possa compartilhar sua experiência com isso porque não tenho experiência com esses sistemas.

    
por 05.12.2018 / 16:44
fonte