c# - usan - que es una interfaz en programacion orientada a objetos
Declarar IDisposable para la clase o interfaz? (7)
Estoy empezando a pensar que poner IDisposable
en una interfaz puede causar algunos problemas. Implica que la vida útil de todos los objetos que implementan esa interfaz puede finalizarse sincrónicamente. Es decir, permite a cualquier persona escribir código como este y requiere que todas las implementaciones admitan IDisposable
:
using (ISample myInstance = GetISampleInstance())
{
myInstance.DoSomething();
}
Solo el código que está accediendo al tipo concreto puede conocer la forma correcta de controlar la vida útil del objeto. Por ejemplo, un tipo podría no necesitar eliminación en primer lugar, podría admitir IDisposable
, o podría requerir la awaiting
algún proceso de limpieza asincrónica una vez que haya terminado de usarlo (por ejemplo, algo así como la opción 2 aquí ).
Un autor de la interfaz no puede predecir todas las posibles necesidades futuras de gestión del alcance / duración de la implementación de clases. El propósito de una interfaz es permitir que un objeto exponga alguna API para que pueda ser útil para algunos consumidores. Algunas interfaces pueden estar relacionadas con la administración de por vida (como IDisposable
), pero mezclarlas con interfaces no relacionadas con la administración de por vida puede dificultar o imposibilitar la escritura de una implementación de la interfaz. Si tiene muy pocas implementaciones de su interfaz y estructura su código para que el consumidor de la interfaz y el administrador de duración / alcance estén en el mismo método, esta distinción no está clara al principio. Pero si comienzas a pasar tu objeto, esto será más claro.
void ConsumeSample(ISample sample)
{
// INCORRECT CODE!
// It is a developer mistake to write “using” in consumer code.
// “using” should only be used by the code that is managing the lifetime.
using (sample)
{
sample.DoSomething();
}
// CORRECT CODE
sample.DoSomething();
}
async Task ManageObjectLifetimesAsync()
{
SampleB sampleB = new SampleB();
using (SampleA sampleA = new SampleA())
{
DoSomething(sampleA);
DoSomething(sampleB);
DoSomething(sampleA);
}
DoSomething(sampleB);
// In the future you may have an implementation of ISample
// which requires a completely different type of lifetime
// management than IDisposable:
SampleC = new SampleC();
DoSomething(sampleC);
sampleC.Complete();
await sampleC.Completion;
}
class SampleC : ISample
{
public void Complete();
public Task Completion { get; }
}
En el ejemplo de código anterior, demostré tres tipos de escenarios de administración de por vida, que se suman a los dos que proporcionó.
-
SampleA
esIDisposable
con elusing () {}
sincrónico deusing () {}
. -
SampleB
usa recolección de basura pura (no consume ningún recurso). -
SampleC
utiliza recursos que evitan que se eliminen de forma síncrona y requieren unaawait
al final de su vida útil (para poder notificar al código de gestión de por vida que se ha realizado consumiendo recursos y crear cualquier excepción encontrada de forma asincrónica).
Al mantener la administración de por vida separada de sus otras interfaces, puede evitar errores del desarrollador (p. Ej., Llamadas accidentales a Dispose()
) y admitir de forma más limpia futuros patrones de administración de por vida / alcance imprevistos.
A partir de la siguiente situación:
public interface ISample
{
}
public class SampleA : ISample
{
// has some (unmanaged) resources that needs to be disposed
}
public class SampleB : ISample
{
// has no resources that needs to be disposed
}
La clase SampleA debe implementar la interfaz IDisposable para liberar recursos. Podrías resolver esto de dos maneras:
1. Agregue la interfaz requerida a la clase SampleA:
public class SampleA : ISample, IDisposable
{
// has some (unmanaged) resources that needs to be disposed
}
2. Agréguelo a la interfaz ISample y fuerce las clases derivadas para implementarlo:
public interface ISample : IDisposable
{
}
Si lo coloca en la interfaz, fuerza cualquier implementación para implementar IDisposable, incluso si no tienen nada que desechar. Por otro lado, es muy claro ver que la implementación concreta de una interfaz requiere un bloque de disposición / uso y no es necesario convertirlo en IDisposable para la limpieza. Puede haber más ventajas y desventajas en ambos sentidos ... ¿por qué sugeriría usar una forma preferida para la otra?
IDispoable es una interfaz muy común, no hay daño al heredar su interfaz. Así evitará que se verifique el código en su código a un costo único para tener una implementación no operativa en algunas de sus implementaciones de ISample. Entonces, su segunda opción podría ser mejor desde este punto de vista.
Personalmente elegiría 1, a menos que haga un ejemplo concreto para dos. Un buen ejemplo de dos es un IList
.
Un IList
significa que necesita implementar un indexador para su colección. Sin embargo, un IList
realmente también significa que usted es un IEnumerable
, y debe tener un GetEnumerator()
para su clase.
En su caso, duda de que las clases que implementan ISample
necesiten implementar IDisposable
, si no todas las clases que implementan su interfaz tienen que implementar IDisposable
entonces no las fuerce.
Centrándose en IDispoable
específicamente, IDispoable
en particular obliga a los programadores que usan su clase a escribir algún código razonablemente feo. Por ejemplo,
foreach(item in IEnumerable<ISample> items)
{
try
{
// Do stuff with item
}
finally
{
IDisposable amIDisposable = item as IDisposable;
if(amIDisposable != null)
amIDisposable.Dispose();
}
}
No solo es horrible el código, habrá una penalización de rendimiento significativa para garantizar que haya un bloqueo final para eliminar el elemento para cada iteración de esa lista, incluso si Dispose()
simplemente regresa en la implementación.
Pegué el código para responder uno de los comentarios aquí, más fácil de leer.
Personalmente, si todos los ISample
de ISample
fueran desechables lo pondría en la interfaz, si solo algunos lo colocan en las clases donde debería estar.
Parece que tienes el último caso.
Si aplica el patrón using(){}
a todas sus interfaces, es mejor que ISample
derive de IDisposable
porque la regla general al diseñar interfaces es favorecer la "facilidad de uso" sobre la "facilidad de implementación" .
Siguiendo el Principio de Segregación Inteface de SOLID si agrega el IDisposable a la interfaz, está dando métodos a clientes que no están interesados, por lo que debe agregarlo a A.
Aparte de eso, una interfaz nunca es desechable porque la disponibilidad es algo relacionado con la implementación concreta de la interfaz, nunca con la interfaz misma.
Cualquier interfaz puede implementarse potencialmente con o sin elementos que deben eliminarse.
Una interfaz IFoo
probablemente debería implementar IDisposable
si es probable que al menos algunas implementaciones implementen IDisposable
, y en al menos algunas ocasiones la última referencia superviviente de una instancia se almacenará en una variable o campo de tipo IFoo
. Casi seguramente debería implementar IDisposable
si alguna implementación puede implementar IDisposable
y las instancias se crearán a través de la interfaz de fábrica (como es el caso de las instancias de IEnumerator<T>
, que en muchos casos se crean a través de la interfaz de fábrica IEnumerable<T>
).
La comparación de IEnumerable<T>
e IEnumerator<T>
es instructiva. Algunos tipos que implementan IEnumerable<T>
también implementan IDisposable
, pero el código que crea instancias de dichos tipos sabrá cuáles son, saben que necesitan eliminación y los utilizan como sus tipos particulares. Tales instancias se pueden pasar a otras rutinas como tipo IEnumerable<T>
, y esas otras rutinas no tendrían ni idea de que los objetos eventualmente van a necesitar deshacerse, pero esas otras rutinas en la mayoría de los casos no serían las últimas en contener referencias a los objetos . Por el contrario, las instancias de IEnumerator<T>
menudo se crean, utilizan y finalmente abandonan, por código que no sabe nada sobre los tipos subyacentes de esas instancias más allá del hecho de que IEnumerable<T>
. Algunas implementaciones de IEnumerable<T>.GetEnumerator()
implementaciones de retorno de IEnumerator<T>
perderán recursos si su método IDisposable.Dispose
no se llama antes de que se abandonen, y la mayoría del código que acepta parámetros de tipo IEnumerable<T>
no tendrá forma de saber si dichos tipos pueden pasarse a él. Aunque es posible que IEnumerable<T>
incluya una propiedad EnumeratorTypeNeedsDisposal
para indicar si el IEnumerator<T>
devuelto IEnumerator<T>
debería eliminarse, o simplemente requiere que las rutinas que llaman a GetEnumerator()
comprueben el tipo del objeto devuelto para ver si implementa IDisposable
, es más rápido y más fácil llamar incondicionalmente a un método de Dispose
que podría no hacer nada, que determinar si es necesario Dispose
y llamarlo solo en ese caso.