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):
- 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. - 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 debeDispose()
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 queDispose
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ónif (_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. Laif (_context != null)
es, por lo tanto, redundante. Dado queDbContext
puedeDbContext
forma segura varias veces, no es necesario anularlo. - La implementación del Patrón de Disposición (con el método de
Dispose(bool)
protegidoDispose(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. Sisealed
este tipo, puede eliminar de forma segura el método protegido deDispose(bool)
y mover su lógica al método público deDispose()
. - 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.