patterns patrones para lista gof95 ejemplos diseño aplicaciones antipatrones antipatron anti dependency-injection initialization async-await simple-injector abstract-factory

dependency injection - lista - Evitar todos los antipatrones DI para los tipos que requieren inicialización asíncrona



patrones de diseño java (2)

Tengo un tipo de Connections que requiere inicialización asíncrona. Una instancia de este tipo es consumida por varios otros tipos (por ejemplo, Storage ), cada uno de los cuales también requiere inicialización asíncrona (estática, no por instancia, y estas inicializaciones también dependen de Connections ). Finalmente, mis tipos de lógica (p. Ej., Logic ) consumen estas instancias de almacenamiento. Actualmente utilizando Simple Injector.

He intentado varias soluciones diferentes, pero siempre hay un presente antipattern.

Inicialización explícita (acoplamiento temporal)

La solución que estoy usando actualmente tiene el antipattern de acoplamiento temporal:

public sealed class Connections { Task InitializeAsync(); } public sealed class Storage : IStorage { public Storage(Connections connections); public static Task InitializeAsync(Connections connections); } public sealed class Logic { public Logic(IStorage storage); } public static class GlobalConfig { public static async Task EnsureInitialized() { var connections = Container.GetInstance<Connections>(); await connections.InitializeAsync(); await Storage.InitializeAsync(connections); } }

He encapsulado el acoplamiento temporal en un método, por lo que no es tan malo como podría ser. Pero aún así, es un antipattern y no es tan fácil de mantener como me gustaría.

Resumen de Fábrica (Sync-Over-Async)

Una solución común propuesta es un patrón de Abstract Factory. Sin embargo, en este caso estamos tratando con inicialización asíncrona. Entonces, podría usar Abstract Factory forzando la inicialización para que se ejecute de forma síncrona, pero esto adopta el antipattern sync over-async. Realmente no me gusta el enfoque de sincronización sobre asíncrono porque tengo varios almacenamientos y en mi código actual todos se inicializan al mismo tiempo; ya que se trata de una aplicación en la nube, cambiarla para que sea sincrónica en serie aumentaría el tiempo de inicio, y la sincronización en paralelo tampoco es ideal debido al consumo de recursos.

Resumen de fábrica asíncrona (uso incorrecto de fábrica abstracta)

También puedo usar Abstract Factory con métodos de fábrica asíncronos. Sin embargo, hay un problema importante con este enfoque. Como Mark Seeman comenta here , "Cualquier contenedor de DI que se precie será capaz de auto-cablear una instancia [de fábrica] para usted si la registra correctamente". Desafortunadamente, esto es completamente falso para las fábricas asíncronas: AFAIK no hay un contenedor DI que lo admita.

Por lo tanto, la solución de fábrica asíncrona abstracta me requeriría usar fábricas explícitas, al menos Func<Task<T>> , y esto termina siendo en todas partes ("Pensamos personalmente que permitir el registro de delegados Func por defecto es un olor de diseño ... Si tiene muchos constructores en su sistema que dependen de un Func, por favor, eche un vistazo a su estrategia de dependencia ".

public sealed class Connections { private Connections(); public static Task<Connections> CreateAsync(); } public sealed class Storage : IStorage { // Use static Lazy internally for my own static initialization public static Task<Storage> CreateAsync(Func<Task<Connections>> connections); } public sealed class Logic { public Logic(Func<Task<IStorage>> storage); }

Esto causa varios problemas propios:

  1. Todos mis registros de fábrica tienen que sacar las dependencias del contenedor explícitamente y pasarlas a CreateAsync . Entonces el contenedor DI ya no está haciendo, ya sabes, inyección de dependencia .
  2. Los resultados de estas llamadas de fábrica tienen tiempos de vida que ya no son administrados por el contenedor DI. Cada fábrica ahora es responsable de la administración de por vida en lugar del contenedor DI. (Con la fábrica abstracta sincrónica, esto no es un problema si la fábrica está registrada correctamente).
  3. Cualquier método que utilice realmente estas dependencias debería ser asíncrono, ya que incluso los métodos lógicos deben esperar a que se complete la inicialización del almacenamiento / conexiones. Esto no es un gran problema para mí en esta aplicación, ya que todos mis métodos de almacenamiento son asíncronos de todos modos, pero puede ser un problema en el caso general.

Autoinicialización (acoplamiento temporal)

Otra solución, menos común, es hacer que cada miembro de un tipo espere su propia inicialización:

public sealed class Connections { private Task InitializeAsync(); // Use Lazy internally // Used to be a property BobConnection public X GetBobConnectionAsync() { await InitializeAsync(); return BobConnection; } } public sealed class Storage : IStorage { public Storage(Connections connections); private static Task InitializeAsync(Connections connections); // Use Lazy internally public async Task<Y> IStorage.GetAsync() { await InitializeAsync(_connections); var connection = await _connections.GetBobConnectionAsync(); return await connection.GetYAsync(); } } public sealed class Logic { public Logic(IStorage storage); public async Task<Y> GetAsync() { return await _storage.GetAsync(); } }

El problema aquí es que estamos de vuelta en el acoplamiento temporal, esta vez repartidos por todo el sistema. Además, este enfoque requiere que todos los miembros públicos sean métodos asíncronos.

Entonces, hay realmente dos perspectivas de diseño DI que están en desacuerdo aquí:

  • Los consumidores quieren poder inyectar instancias que estén listas para usar.
  • Los contenedores DI empujan fuerte para constructores simples .

El problema es, especialmente con la inicialización asíncrona, que si los contenedores DI adoptan una línea dura en el enfoque de "constructores simples", entonces están obligando a los usuarios a realizar su propia inicialización en otro lugar, lo que trae sus propios antipatrones. Por ejemplo, ¿por qué Simple Injector no considera funciones asíncronas ? "No, esta característica no tiene sentido para Simple Injector o cualquier otro contenedor DI, porque viola algunas reglas básicas importantes cuando se trata de la inyección de dependencia". Sin embargo, jugar estrictamente "según las reglas básicas" aparentemente obliga a otros antipatrones que parecen mucho peores.

La pregunta: ¿existe una solución para la inicialización asíncrona que evite todos los antipatrones?

Actualización: firma completa para AzureConnections (referido anteriormente como Connections ):

public sealed class AzureConnections { public AzureConnections(); public CloudStorageAccount CloudStorageAccount { get; } public CloudBlobClient CloudBlobClient { get; } public CloudTableClient CloudTableClient { get; } public async Task InitializeAsync(); }


El problema que tiene, y la aplicación que está creando , es un típico. Es un típico por dos razones:

  1. necesita (o más bien quiere) inicialización asíncrona de arranque, y
  2. El marco de la aplicación (funciones azure) admite la inicialización de inicio asíncrono (o, más bien, parece que hay poco marco que lo rodea). Esto hace que su situación sea un poco diferente de la de los escenarios normales, lo que podría hacer que sea un poco más difícil discutir los patrones comunes.

Sin embargo, incluso en su caso, la solución es bastante simple y elegante:

Extraiga la inicialización de las clases que la contienen y mueva la inicialización a la Raíz de composición . En ese momento, puede crear e inicializar esas clases antes de registrarlas en el contenedor y alimentar esas clases inicializadas en el contenedor como parte de los registros.

Esto funciona bien en su caso particular, porque quiere hacer una inicialización de inicio (una sola vez). La inicialización de inicio se realiza normalmente antes de configurar el contenedor, o algunas veces después, si requiere un gráfico de objetos completamente compuesto. En la mayoría de los casos que he visto, la inicialización se puede hacer antes, como se puede hacer efectivamente en su caso.

Como dije, su caso es un poco peculiar, comparado con la norma. La norma es:

  • La inicialización de arranque es síncrona. Los marcos (como ASP.NET Core) normalmente no admiten la inicialización asíncrona en la fase de inicio
  • A menudo, la inicialización debe realizarse por solicitud, justo a tiempo, en lugar de por aplicación, antes de tiempo. A menudo, los componentes que necesitan inicialización tienen una vida útil corta, lo que significa que normalmente inicializamos dicha instancia en el primer uso (en otras palabras: just-in-time).

Normalmente, no hay un beneficio real de hacer la inicialización de inicio de forma asíncrona. No hay un beneficio práctico de rendimiento, porque en el momento de la puesta en marcha, solo habrá un solo subproceso ejecutándose de todas formas (aunque podríamos paralizarlo, pero eso obviamente no requiere async). También tenga en cuenta que, aunque algunos tipos de aplicaciones pueden interrumpirse en la sincronización con la sincronización, en la Raíz de composición sabemos exactamente qué tipo de aplicación estamos usando y si esto será un problema o no. Una raíz de composición es específica de la aplicación. En otras palabras, cuando tenemos la inicialización en nuestra Raíz de composición, normalmente no hay beneficio de realizar la inicialización de inicio de forma asíncrona.

Debido a que en la Raíz de composición sabemos si la sincronización sobre-asíncrono es un problema o no, incluso podríamos decidir hacer la inicialización en el primer uso y de forma síncrona. Debido a que la cantidad de inicialización es finita (en comparación con la inicialización por solicitud), no hay un impacto práctico en el rendimiento de hacerlo en un subproceso en segundo plano con bloqueo síncrono si lo deseamos. Todo lo que tenemos que hacer es definir una clase Proxy en nuestra Raíz de composición que asegure que la inicialización se realice en el primer uso. Esta es prácticamente la idea que Mark Seemann propuso como respuesta.

No estaba familiarizado en absoluto con las funciones de Azure, por lo que este es realmente el primer tipo de aplicación (excepto las aplicaciones de la Consola) que conozco que realmente admite la inicialización asíncrona. En la mayoría de los tipos de marcos, no hay forma de que los usuarios realicen esta inicialización de inicio de forma asíncrona. Cuando estamos dentro de un evento Application_Start en una aplicación ASP.NET o en la clase de Inicio de una aplicación Core de ASP.NET, por ejemplo, no hay asíncrono. Todo tiene que ser síncrono.

Además de eso, los marcos de aplicaciones no nos permiten construir sus componentes raíz de marcos de forma asíncrona. Incluso si DI Containers apoyaría el concepto de hacer resoluciones asíncronas, esto no funcionaría debido a la ''falta'' de soporte de los marcos de aplicaciones. Tomemos como ejemplo el IControllerActivator ASP.NET Core. Su método Create(ControllerContext) nos permite componer una instancia de Controller, pero el tipo de retorno es object , no Task<object> . En otras palabras, incluso si los Contenedores DI nos proporcionaran un método ResolveAsync , seguiría causando el bloqueo porque ResolveAsync llamadas de ResolveAsync estarían envueltas detrás de abstracciones de estructuras síncronas.

En la mayoría de los casos, verá que la inicialización se realiza por instancia o en tiempo de ejecución. SqlConnections, por ejemplo, normalmente se abren por solicitud, por lo que cada solicitud debe abrir su propia conexión. Cuando queremos abrir la conexión ''justo a tiempo'', esto resulta inevitablemente en interfaces de aplicaciones que son asíncronas. Pero ten cuidado aquí:

Si creamos una implementación que es sincrónica, solo deberíamos hacer que su abstracción sea sincrónica en caso de que estemos seguros de que nunca habrá otra implementación (o proxy, decorador, interceptor, etc.) que sea asíncrona. Si hacemos que la abstracción sea sincrónicamente (es decir, tenemos métodos y propiedades que no exponen la Task<T> ), es muy posible que tengamos una Abstracción con fugas en nuestras manos. Esto podría hacer que realicemos cambios radicales en toda la aplicación, cuando obtengamos una implementación asíncrona más adelante.

En otras palabras, con la introducción de async debemos cuidar aún más el diseño de nuestras abstracciones de aplicaciones. Esto vale para su caso también. Aunque es posible que solo necesite una inicialización de inicio ahora, ¿está seguro de que para las abstracciones que definió (y también de AzureConnections ), nunca necesitará una inicialización asíncrona justo a tiempo? En caso de que el comportamiento síncrono de AzureConnections sea ​​un detalle de la implementación, deberá hacerlo de forma asíncrona de inmediato.

Otro ejemplo de esto es su INugetRepository . Sus miembros son sincrónicos, pero eso es claramente una abstracción con fugas, porque la razón por la que es sincrónica es porque su implementación es sincrónica. Sin embargo, su implementación es sincrónica, ya que utiliza un paquete NuGet NuGet heredado que solo tiene una API síncrona. Es bastante claro que INugetRepository debería ser completamente asíncrono, aunque su implementación sea sincrónica.

En una aplicación que aplica async, la mayoría de las abstracciones de la aplicación tendrán en su mayoría miembros async. Cuando este sea el caso, sería una obviedad hacer que este tipo de lógica de inicialización justo a tiempo también sea asíncrona; Todo ya está asíncrono.

Para resumir:

  • En caso de que necesite una inicialización inicial: hágalo antes o después de configurar el contenedor o después. Esto hace que la composición de gráficos de objetos sea rápida, confiable y verificable.
  • Hacer la inicialización antes de configurar el contenedor previene el acoplamiento temporal, pero puede significar que tendrá que mover la inicialización de las clases que lo requieren (lo que en realidad considero que es una buena cosa).
  • En la mayoría de los tipos de aplicaciones, la inicialización de inicio asíncrono es imposible, en los otros tipos suele ser innecesario.
  • En caso de que necesite una inicialización por solicitud o justo a tiempo, no hay manera de evitar las interfaces asíncronas.
  • Tenga cuidado con las interfaces síncronas si está construyendo una aplicación asíncrona, podría estar perdiendo detalles de la implementación.

Si bien estoy bastante seguro de que lo siguiente no es lo que estás buscando, ¿puedes explicar por qué no responde a tu pregunta?

public sealed class AzureConnections { private readonly Task<CloudStorageAccount> storage; public AzureConnections() { this.storage = Task.Factory.StartNew(InitializeStorageAccount); // Repeat for other cloud } private static CloudStorageAccount InitializeStorageAccount() { // Do any required initialization here... return new CloudStorageAccount( /* Constructor arguments... */ ); } public CloudStorageAccount CloudStorageAccount { get { return this.storage.Result; } } }

Para mantener el diseño claro, solo implementé una de las propiedades de la nube, pero las otras dos se podrían hacer de una manera similar.

El constructor de AzureConnections no se bloqueará, incluso si se tarda mucho tiempo en inicializar los diversos objetos de la nube.

Por otra parte, iniciará el trabajo y, dado que las tareas de .NET se comportan como promesas, la primera vez que intente acceder al valor (utilizando Result ) devolverá el valor producido por InitializeStorageAccount .

Tengo la fuerte impresión de que esto no es lo que quieres, pero como no entiendo qué problema estás tratando de resolver, pensé en dejar esta respuesta para que al menos tuviéramos algo que discutir.