recursos pattern net namespace example administrados c# decorator ioc-container simple-injector

pattern - recursos no administrados c#



Decoradores e IDisposable. (3)

¿Qué deberían hacer los decoradores con respecto a la implementación de IDisposable?

Esto vuelve al principio general de propiedad. Pregúntate a ti mismo: "¿Quién posee ese tipo desechable?". La respuesta a esta pregunta es: El que posee el tipo es el responsable de disponerlo.

Dado que un tipo desechable se transmite al decorador desde el exterior, el decorador no creó ese tipo y, por lo general, no debería ser responsable de limpiarlo. El decorador no tiene forma de saber si el tipo debe eliminarse (ya que no controla su vida útil) y esto es muy claro en su caso, ya que el decorador puede registrarse como transitorio, mientras que el decorado tiene una vida útil mucho más larga. En su caso, su sistema simplemente se romperá si desecha la decoración desde dentro del decorador.

Por lo tanto, el decorador nunca debe desechar el decorado, simplemente porque no es el dueño del decorado. Es responsabilidad de su Raíz de composición disponer de esa decoración. No importa que estemos hablando de decoradores en este caso; Todavía se trata del principio general de propiedad.

Cada decorador solo debe preocuparse por deshacerse de sí mismo y nunca debe pasar la llamada a la instancia decorada.

Correcto. Sin embargo, el decorador debería disponer de todo lo que posee, pero como está usando la inyección de dependencia, por lo general no crea muchas cosas por sí mismo y, por lo tanto, no posee esas cosas.

Su UnitOfWork por otro lado, crea una nueva clase MyContext y, por lo tanto, tiene la propiedad de esa instancia y debería disponer de ella.

Hay excepciones a esta regla, pero aún se trata de propiedad. A veces se pasa la propiedad de un tipo a otros. Cuando se utiliza un método de fábrica, por ejemplo, por convención, el método de fábrica pasa la propiedad del objeto creado al llamante. A veces, la propiedad se transfiere a un objeto creado, como lo hace la clase StreamReader de .NET. La documentación de la API es clara al respecto, pero dado que el diseño es tan poco intuitivo, los desarrolladores continúan tropezando con este comportamiento. La mayoría de los tipos en el marco .NET no funcionan de esta manera. Por ejemplo, la clase SqlCommand no dispone de SqlConnection , y sería muy molesto si eliminara la conexión.

Una forma diferente de ver este problema es desde la perspectiva de los principios de SOLID . Al permitir que IUnitOfWork implemente IDisposable , está violando el Principio de Inversión de Dependencia porque "las abstracciones no deben depender de los detalles; los detalles deben depender de las abstracciones". Al implementar IDisposable está IDisposable detalles de la implementación en la interfaz IUnitOfWork . Implementar IDisposable significa que la clase tiene recursos no administrados que deben eliminarse, como los manejadores de archivos y las cadenas de conexión. Estos son detalles de la implementación, ya que casi nunca puede darse el caso de que cada implementación de dicha interfaz realmente necesite ser eliminada. Solo tiene que crear una implementación falsa o simulada para sus pruebas unitarias y tiene pruebas de una implementación que no necesita ser eliminada.

Por lo tanto, cuando arregla esta violación DIP eliminando la interfaz IDisposable de IUnitOfWork y moviéndola a la implementación), al decorador le resulta imposible deshacerse del decorado, ya que no tiene forma de saber si el decorado se implementa como no IDisposable . Y esto es bueno, porque de acuerdo con el DIP, el decorador no debería saberlo, y ya establecimos que el decorador no debe deshacerse del decorado.

Tengo una subclase de DbContext

public class MyContext : DbContext { }

y tengo una abstracción IUnitOfWork alrededor de MyContext que implementa IDisposable para asegurar que las referencias como MyContext se eliminen en el momento adecuado

public interface IUnitOfWork : IDisposable { } public class UnitOfWork : IUnitOfWork { private readonly MyContext _context; public UnitOfWork() { _context = new MyContext(); } ~UnitOfWork() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_context != null) _context.Dispose(); } _disposed = true; } }

My UnitOfWork está registrado con un alcance de por vida de solicitud por (web). Tengo decoradores de IUnitOfWork que podrían registrarse como transitorios o con un alcance de por vida y mi pregunta es qué deben hacer con respecto a la implementación de IDisposable , específicamente si deberían o no transmitir la llamada a Dispose() .

public class UnitOfWorkDecorator : IUnitOfWork { private readonly IUnitOfWork _decorated; public UnitOfWorkDecorator(IUnitOfWork decorated) { _decorated = decorated; } public void Dispose() { //do we pass on the call? _decorated.Dispose(); } }

Veo 2 opciones (supongo que la opción 2 es la respuesta correcta):

  1. Se espera que cada Decorador sepa si tiene un alcance transitorio o de por vida. Si un decorador es transitorio, no debe llamar a Dispose() en la instancia decorada. Si es ámbito de por vida, debería.
  2. Cada decorador solo debe preocuparse por deshacerse de sí mismo y nunca debe pasar la llamada a la instancia decorada. El contenedor administrará la llamada a Dispose() para cada objeto en la cadena de llamadas en el momento adecuado. Un objeto solo debe Dispose() de las instancias que encapsula y decorar no es encapsulación.

No es una respuesta, pero su UnitOfWork se puede simplificar mucho.

  • Como la clase en sí no tiene recursos nativos, no es necesario que tenga un finalizador. Por lo tanto, el finalizador puede ser eliminado.
  • El contrato de la interfaz IDisposable establece que es válido que Dispose se llame varias veces. Esto no debe resultar en una excepción o cualquier otro comportamiento observable. Por lo tanto, puede eliminar la _disposed y la comprobación if (_disposed) .
  • El campo _context siempre se inicializará cuando el constructor tenga éxito y no se podrá llamar a _context cuando el constructor lance una excepción. La if (_context != null) es, por lo tanto, redundante. Dado que DbContext puede DbContext forma segura varias veces, no es necesario anularlo.
  • La implementación del Patrón de Disposición (con el método de Dispose(bool) protegido Dispose(bool) ) solo es necesaria cuando el tipo se pretende heredar. El patrón es especialmente útil para los tipos que forman parte de un marco reutilizable, ya que no hay control sobre quién hereda de ese tipo. Si sealed este tipo, puede eliminar de forma segura el método protegido de Dispose(bool) y mover su lógica al método público de Dispose() .
  • Como el tipo no contiene un finalizador y no se puede heredar, puede eliminar la llamada a GC.SuppressFinalize .

Al seguir estos pasos, esto es lo que queda del tipo UnitOfWork :

public sealed class UnitOfWork : IUnitOfWork, IDisposable { private readonly MyContext _context; public UnitOfWork() { _context = new MyContext(); } public void Dispose() { _context.Dispose(); } }

En caso de que mueva la creación de MyContext fuera de UnitOfWork inyectándolo en UnitOfWork , incluso puede simplificar UnitOfWork a lo siguiente:

public sealed class UnitOfWork : IUnitOfWork { private readonly MyContext _context; public UnitOfWork(MyContext context) { _context = context; } }

Dado que UnitOfWork acepta un MyContext que no tiene la propiedad, no está autorizado a eliminar MyContext (ya que es posible que otro consumidor aún requiera su uso, incluso después de que UnitOfWork fuera del alcance). Esto significa que UnitOfWork no necesita desechar nada y, por lo tanto, no necesita implementar IDisposable .

Esto, por supuesto, significa que trasladamos la responsabilidad de disponer MyContext a "otra persona". Este ''alguien'' normalmente será el mismo que tenía el control sobre la creación y eliminación de UnitOfWork también. Típicamente esta es la raíz de la composición .


Personalmente, sospecho que necesita manejar esto caso por caso. Algunos decoradores pueden tener buenas razones para entender el alcance; para la mayoría, es probablemente un buen valor por defecto simplemente pasarlo. Muy pocos nunca deben disponer explícitamente de la cadena; los principales momentos que he visto fueron para contrarrestar específicamente un escenario en el que otro decorador que debería haber considerado el alcance: no (siempre dispuesto).

Como ejemplo relacionado, considere cosas como GZipStream , para la mayoría de las personas, solo están tratando con un fragmento lógico, por lo que la GZipStream "eliminar el flujo"; pero esta decisión está disponible a través de una sobrecarga de constructor que le permite decirle cómo comportarse. En versiones recientes de C # con parámetros opcionales, esto podría hacerse en un solo constructor.

La opción 2 es problemática, ya que requiere que usted (o el contenedor) realice un seguimiento de todos los objetos intermedios; Si su contenedor lo hace convenientemente, entonces está bien, pero también tenga en cuenta que deben desecharse en el orden correcto (exterior a interior). Debido a que en una cadena de decoradores, puede haber operaciones pendientes: programadas para ser desechadas río abajo a pedido, o (como último recurso) durante la eliminación.