¿Es una interfaz que expone funciones asíncronas una abstracción con fugas?

13

Estoy leyendo el libro Principios, prácticas y patrones de inyección de dependencia y leí sobre el concepto de abstracción permeable que está bien descrito en el libro.

En estos días estoy refactorizando una base de código C # usando inyección de dependencia para que se usen llamadas asíncronas en lugar de bloquearlas. Al hacerlo, estoy considerando algunas interfaces que representan abstracciones en mi base de código y que deben rediseñarse para que se puedan usar las llamadas asíncronas.

Como ejemplo, considere la siguiente interfaz que representa un repositorio para usuarios de aplicaciones:

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

De acuerdo con la definición del libro, una abstracción con fugas es una abstracción diseñada con una implementación específica en mente, de modo que algunos detalles de la implementación "se filtran" a través de la abstracción misma.

Mi pregunta es la siguiente: ¿podemos considerar una interfaz diseñada teniendo en cuenta asíncrona, como IUserRepository, como un ejemplo de una abstracción con fugas?

Por supuesto, no todas las implementaciones posibles tienen algo que ver con la asincronía: solo las implementaciones fuera de proceso (como una implementación de SQL), pero un repositorio en memoria no requiere asincronía (en realidad, implementar una versión en memoria de la interfaz es probablemente más difícil si la interfaz expone métodos asincrónicos, por ejemplo, probablemente tenga que devolver algo como Task.CompletedTask o Task.FromResult (usuarios) en las implementaciones del método).

Qué piensas sobre eso ?

Enrico Massone
fuente
@Neil, probablemente entendí el punto. Una interfaz que expone métodos que devuelven Tarea o Tarea <T> no es una abstracción permeable en sí misma, es simplemente un contrato con una firma que involucra tareas. Un método que devuelve una Tarea o Tarea <T> no implica tener una implementación asincrónica (por ejemplo, si creo una tarea completada usando Task.CompletedTask, no estoy haciendo una implementación asincrónica). Viceversa, la implementación asíncrona en C # requiere que el tipo de retorno de un método asíncrono sea del tipo Tarea o Tarea <T>. Dicho de otra manera, el único aspecto "permeable" de mi interfaz es el sufijo asíncrono en los nombres
Enrico Massone
@Neil en realidad hay una directriz de nomenclatura que establece que todos los métodos asincrónicos deben tener un nombre que termine en "Async". Pero esto no implica que un método que devuelve una Tarea o una Tarea <T> deba nombrarse con el sufijo Asíncrono porque podría implementarse sin usar llamadas asíncronas.
Enrico Massone
66
Yo diría que la "asincronía" de un método se indica por el hecho de que devuelve a Task. Las pautas para sufijar los métodos asíncronos con la palabra asíncrono era distinguir entre llamadas API de otro modo idénticas (C # no se puede enviar según el tipo de retorno). En nuestra empresa lo hemos dejado todo junto.
richzilla
Hay varias respuestas y comentarios que explican por qué la naturaleza asincrónica del método es parte de la abstracción. Una pregunta más interesante es cómo un lenguaje o API de programación puede separar la funcionalidad de un método de cómo se ejecuta, hasta el punto en que ya no necesitamos valores de retorno de tareas o marcadores asíncronos. La gente de programación funcional parece haber descubierto esto mejor. Considere cómo se definen los métodos asincrónicos en F # y otros lenguajes.
Frank Hileman
2
:-) -> La "gente de programación funcional" ha. Async no tiene más fugas que síncrono, simplemente parece así porque estamos acostumbrados a escribir código de sincronización de forma predeterminada. Si todos codificamos asíncrono por defecto, una función síncrona puede parecer permeable.
StarTrekRedneck

Respuestas:

8

Por supuesto, se puede invocar la ley de las abstracciones con fugas , pero eso no es particularmente interesante porque plantea que todas las abstracciones tienen fugas. Se puede argumentar a favor y en contra de esa conjetura, pero no ayuda si no compartimos una comprensión de lo que queremos decir con abstracción y lo que queremos decir con fugas . Por lo tanto, primero intentaré delinear cómo veo cada uno de estos términos:

Abstracciones

Mi definición favorita de abstracciones se deriva de la APPP de Robert C. Martin :

"Una abstracción es la amplificación de lo esencial y la eliminación de lo irrelevante".

Por lo tanto, las interfaces no son, en sí mismas, abstracciones . Solo son abstracciones si sacan a la superficie lo que importa y ocultan el resto.

Agujereado

El libro Principios, patrones y prácticas de la inyección de dependencia define el término abstracción permeable en el contexto de la inyección de dependencia (DI). El polimorfismo y los principios SÓLIDOS juegan un papel importante en este contexto.

Del Principio de Inversión de Dependencia (DIP) se desprende, citando nuevamente APPP, que:

"los clientes [...] poseen las interfaces abstractas"

Lo que esto significa es que los clientes (código de llamada) definen las abstracciones que requieren, y luego van e implementan esa abstracción.

Una abstracción con fugas , en mi opinión, es una abstracción que viola el DIP al incluir de alguna manera alguna funcionalidad que el cliente no necesita .

Dependencias sincrónicas

Un cliente que implementa una parte de la lógica empresarial generalmente usará DI para desacoplarse de ciertos detalles de implementación, como, por lo general, las bases de datos.

Considere un objeto de dominio que maneja una solicitud 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);
    }
}

Aquí, la IReservationsRepositorydependencia está determinada exclusivamente por el cliente, la MaîtreDclase:

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

Esta interfaz es completamente sincrónica ya que la MaîtreDclase no necesita que sea asíncrona.

Dependencias asincrónicas

Puede cambiar fácilmente la interfaz para que sea asíncrona:

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

La MaîtreDclase, sin embargo, no necesita esos métodos sean asíncrona, por lo que ahora se viole el DIP. Considero que esto es una abstracción permeable, porque un detalle de implementación obliga al cliente a cambiar. El TryAcceptmétodo ahora también tiene que volverse así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);
}

No existe una lógica inherente para que la lógica de dominio sea asíncrona, pero para admitir la asincronía de la implementación, esto ahora se requiere.

Mejores opciones

En NDC Sydney 2018 di una charla sobre este tema . En él, también describo una alternativa que no tiene fugas. También daré esta charla en varias conferencias en 2019, pero ahora se renombró con el nuevo título de inyección asíncrona .

También planeo publicar una serie de publicaciones de blog para acompañar la charla. Estos artículos ya están escritos y en la lista de espera de mi artículo, esperando ser publicados, así que estad atentos.

Mark Seemann
fuente
En mi opinión, esto es una cuestión de intención. Si mi abstracción parece que debería comportarse de una manera pero algún detalle o restricción rompe la abstracción tal como se presenta, esa es una abstracción permeable. Pero en este caso, te estoy presentando explícitamente que la operación es asíncrona, eso no es lo que estoy tratando de resumir. Eso es distinto en mi mente de su ejemplo en el que estoy (sabiamente o no) tratando de abstraer el hecho de que hay una base de datos SQL y aún expongo una cadena de conexión. Tal vez es una cuestión de semántica / perspectiva.
Ant P
Entonces, podemos decir que una abstracción nunca se filtra "per se", sino que se filtra si algunos detalles de una implementación específica se filtran de los miembros expuestos y obligan al consumidor a cambiar su implementación, a fin de satisfacer la forma de la abstracción .
Enrico Massone
2
Curiosamente, el punto que destacó en su explicación es uno de los puntos más incomprendidos de toda la historia de la inyección de dependencia. A veces los desarrolladores olvidan el principio de inversión de dependencia e intentan diseñar la abstracción primero y luego adaptan el diseño del consumidor para hacer frente a la abstracción misma. En cambio, el proceso debe hacerse en el orden inverso.
Enrico Massone
11

No es una abstracción permeable en absoluto.

Ser asíncrono es un cambio fundamental en la definición de una función: significa que la tarea no finaliza cuando vuelve la llamada, pero también significa que el flujo de su programa continuará casi de inmediato, no con un retraso prolongado. Una función asincrónica y una síncrona que realizan la misma tarea son funciones esencialmente diferentes. Ser asincrónico no es un detalle de implementación. Es parte de la definición de una función.

Si la función expuso cómo se hizo asíncrona, sería permeable. A usted (no / no debería tener que) importarle cómo se implementa.

gnasher729
fuente
5

El asyncatributo de un método es una etiqueta que indica que se requiere un cuidado y manejo particular. Como tal, necesita filtrarse al mundo. Las operaciones asincrónicas son extremadamente difíciles de componer correctamente, por lo que es importante informar al usuario de la API.

Si, en cambio, su biblioteca gestionó adecuadamente toda la actividad asincrónica dentro de sí misma, entonces podría permitirse el lujo de no dejar que asyncla API se filtre.

Hay cuatro dimensiones de dificultad en el software: datos, control, espacio y tiempo. Las operaciones asincrónicas abarcan las cuatro dimensiones, por lo tanto, necesitan la mayor atención.

BobDalgleish
fuente
Estoy de acuerdo con su sentimiento, pero "fuga" implica algo malo, que es la intención del término "abstracción permeable", algo indeseable en la abstracción. En el caso de async vs sync, no hay fugas.
StarTrekRedneck
2

una abstracción con fugas es una abstracción diseñada con una implementación específica en mente, de modo que algunos detalles de implementación "se filtren" a través de la abstracción misma.

No exactamente. Una abstracción es una cosa conceptual que ignora algunos elementos de una cosa o problema concreto más complicado (para hacer la cosa / problema más simple, manejable o debido a algún otro beneficio). Como tal, es necesariamente diferente de la cosa / problema real, y por lo tanto va a tener fugas en algún subconjunto de casos (es decir, todas las abstracciones tienen fugas, la única pregunta es en qué medida, es decir, en qué casos es la abstracción útil para nosotros, cuál es su dominio de aplicabilidad).

Dicho esto, cuando se trata de abstracciones de software, a veces (¿o tal vez con la suficiente frecuencia?) Los detalles que elegimos ignorar en realidad no se pueden ignorar porque afectan algún aspecto del software que es importante para nosotros (rendimiento, mantenibilidad, ...) . Entonces, una abstracción permeable es una abstracción diseñada para ignorar ciertos detalles (bajo el supuesto de que era posible y útil hacerlo), pero luego resultó que algunos de esos detalles son significativos en la práctica (no se pueden ignorar, por lo que "filtrarse").

Por lo tanto, una interfaz que expone un detalle de una implementación no tiene fugas per se (o más bien, una interfaz, vista de forma aislada, no es en sí misma una abstracción con fugas); en cambio, la filtración depende del código que implementa la interfaz (es capaz de soportar la abstracción representada por la interfaz), y también de los supuestos hechos por el código del cliente (que equivale a una abstracción conceptual que complementa la expresada por la interfaz, pero no puede expresarse en código (por ejemplo, las características del lenguaje no son lo suficientemente expresivas, por lo que podemos describirlo en los documentos, etc.)).

Filip Milovanović
fuente
2

Considere los siguientes ejemplos:

Este es un método que establece el nombre antes de que regrese:

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

Este es un método que establece el nombre. La persona que llama no puede asumir que el nombre se establece hasta que se complete la tarea devuelta ( IsCompleted= verdadero):

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

Este es un método que establece el nombre. La persona que llama no puede asumir que el nombre se establece hasta que se complete la tarea devuelta ( IsCompleted= verdadero):

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

P: ¿Cuál no pertenece a los otros dos?

R: El método asíncrono no es el único. El que está solo es el método que devuelve nulo.

Para mí, la "fuga" aquí no es la asyncpalabra clave; es el hecho de que el método devuelve una Tarea. Y eso no es una fuga; Es parte del prototipo y parte de la abstracción. Un método asíncrono que devuelve una tarea hace exactamente la misma promesa hecha por un método sincrónico que devuelve una tarea.

Entonces no, no creo que la introducción de asyncformas sea una abstracción permeable en sí misma. Pero es posible que tenga que cambiar el prototipo para devolver una Tarea, que "pierde" al cambiar la interfaz (la abstracción). Y dado que es parte de la abstracción, no es una fuga, por definición.

John Wu
fuente
0

Esta es una abstracción permeable si y solo si no tiene la intención de que todas las clases implementadoras creen una llamada asincrónica. Podrías crear múltiples implementaciones, por ejemplo, una para cada tipo de base de datos que admitas, y esto estaría perfectamente bien suponiendo que nunca necesites saber la implementación exacta que se utiliza en todo tu programa.

Y aunque no puede aplicar estrictamente una implementación asincrónica, el nombre implica que debería serlo. Si las circunstancias cambian, y puede ser una llamada sincrónica por cualquier razón, entonces es muy posible que deba considerar un cambio de nombre, por lo que mi consejo sería hacerlo solo si no cree que esto sea muy probable en el futuro. futuro.

Neil
fuente
0

Aquí hay un punto de vista opuesto.

No pasamos de regresar Fooa regresar Task<Foo>porque comenzamos a querer el en Tasklugar de solo el Foo. De acuerdo, a veces interactuamos con el Taskcódigo pero en la mayoría de los códigos del mundo real lo ignoramos y simplemente usamos el Foo.

Además, a menudo definimos interfaces para admitir el comportamiento asíncrono incluso cuando la implementación puede o no ser asíncrona.

En efecto, una interfaz que devuelve un Task<Foo>le indica que la implementación es posiblemente asíncrona, lo sea realmente o no, aunque le importe o no. Si una abstracción nos dice más de lo que necesitamos saber sobre su implementación, es permeable.

Si nuestra implementación no es asíncrona, la cambiamos para que sea asíncrona, y luego tenemos que cambiar la abstracción y todo lo que la usa, es una abstracción muy permeable.

Eso no es un juicio. Como otros han señalado, todas las abstracciones se filtran. Este tiene un mayor impacto porque necesita un efecto dominó de asíncrono / espera en todo nuestro código solo porque en algún lugar al final puede haber algo que sea realmente asíncrono.

¿Suena eso como una queja? Esa no es mi intención, pero creo que es una observación precisa.

Un punto relacionado es la afirmación de que "una interfaz no es una abstracción". Lo que Mark Seeman declaró sucintamente se ha abusado un poco.

La definición de "abstracción" no es "interfaz", incluso en .NET. Las abstracciones pueden tomar muchas otras formas. Una interfaz puede ser una abstracción deficiente o puede reflejar su implementación tan estrechamente que, en cierto sentido, difícilmente sea una abstracción.

Pero sí utilizamos interfaces para crear abstracciones. Entonces, descartar "las interfaces no son abstracciones" porque una pregunta menciona interfaces y las abstracciones no son esclarecedoras.

Scott Hannen
fuente
-2

¿Es GetAllAsync()realmente asíncrono? Quiero decir que "async" está en el nombre, pero eso se puede eliminar. Entonces pregunto de nuevo ... ¿Es imposible implementar una función que devuelva una Task<IEnumerable<User>>que se resuelva sincrónicamente?

No sé los detalles del Tasktipo de .Net , pero si es imposible implementar la función de forma síncrona, entonces seguro que es una abstracción con fugas (de esta manera), pero de lo contrario no. Yo no sé que si se trataba de un IObservablelugar de una tarea, ésta podría ser implementado de forma síncrona o asíncrona para que nada fuera de la función conoce y por lo tanto no hay un escape ese hecho en particular.

Daniel T.
fuente
Task<T> significa asíncrono. Obtiene el objeto de tarea de inmediato, pero puede que tenga que esperar la secuencia de usuarios
Caleth
Puede que tenga que esperar no significa que sea necesariamente asíncrono. Deberá esperar que significaría asíncrono. Presumiblemente, si la tarea subyacente ya se ha ejecutado, no tiene que esperar.
Daniel T.