Pregunta

Hay un patrón bastante común usado en .NET para probar las capacidades de una clase. Aquí usaré la clase Stream como ejemplo, pero el problema se aplica a todas las clases que usan este patrón.

El patrón es proporcionar una propiedad booleana llamada CanXXX para indicar que la capacidad XXX está disponible en la clase. Por ejemplo, la clase Stream tiene las propiedades CanRead, CanWrite y CanSeek para indicar que se puede llamar a los métodos Read, Write y Seek. Si el valor de las propiedades es falso, al llamar al método respectivo se generará una excepción NotSupportedException.

De la documentación de MSDN en la clase de transmisión:

  

Dependiendo de la fuente de datos subyacente o del repositorio, las transmisiones podrían admitir solo algunas de estas capacidades. Una aplicación puede consultar una secuencia para conocer sus capacidades utilizando las propiedades CanRead, CanWrite y CanSeek.

Y documentación para la propiedad CanRead:

  

Cuando se reemplaza en una clase derivada, obtiene un valor que indica si la secuencia actual admite lectura.

     

Si una clase derivada de Stream no admite lectura, las llamadas a los métodos Read, ReadByte y BeginRead arrojan una excepción NotSupportedException.

Veo una gran cantidad de código escrito en la línea de lo siguiente:

if (stream.CanRead)
{
    stream.Read(…)
}

Tenga en cuenta que no hay un código de sincronización, por ejemplo, para bloquear el objeto de flujo de ninguna manera; otros subprocesos pueden estar accediendo a él u objetos a los que hace referencia. Tampoco hay código para detectar una excepción NotSupportedException.

La documentación de MSDN no indica que el valor de la propiedad no puede cambiar con el tiempo. De hecho, la propiedad CanSeek cambia a falso cuando se cierra la secuencia, lo que demuestra la naturaleza dinámica de estas propiedades. Como tal, no hay garantía contractual de que llamar a Read () en el fragmento de código anterior no arroje una excepción NotSupportedException.

Espero que haya mucho código por ahí que sufra este problema potencial. Me pregunto cómo los que han identificado este problema lo han abordado. ¿Qué patrones de diseño son apropiados aquí?

También agradecería los comentarios sobre la validez de este patrón (los pares CanXXX, XXX ()). Para mí, al menos en el caso de la clase Stream, esto representa una clase / interfaz que está tratando de hacer demasiado y debería dividirse en partes más fundamentales. ¡La falta de un contrato estricto y documentado hace que las pruebas sean imposibles y la implementación sea aún más difícil!

¿Fue útil?

Solución

Sin conocer las partes internas de un objeto, debe suponer que una " bandera " la propiedad es demasiado volátil para confiar cuando el objeto se modifica en varios subprocesos.

He visto esta pregunta con más frecuencia sobre colecciones de solo lectura que sobre secuencias, pero creo que es otro ejemplo del mismo patrón de diseño, y se aplican los mismos argumentos.

Para aclarar, la interfaz ICollection en .NET tiene la propiedad IsReadOnly, que está destinada a usarse como un indicador de si la colección admite métodos para modificar su contenido. Al igual que las transmisiones, esta propiedad puede cambiar en cualquier momento y provocará la InvalidOperationException o NotSupportedException.

Las discusiones sobre esto generalmente se reducen a:

  • ¿Por qué no hay una interfaz IReadOnlyCollection en su lugar?
  • Si NotSupportedException es una buena idea.
  • Los pros y los contras de tener '' modos '' versus funcionalidad concreta distinta.

Los modos rara vez son algo bueno, ya que se ve obligado a lidiar con más de un "conjunto". de comportamiento; tener algo que puede cambiar de modo en cualquier momento es considerablemente peor, ya que su aplicación ahora tiene que lidiar con más de un '' conjunto '' de comportamiento también. Sin embargo, el hecho de que sea posible dividir algo en una funcionalidad más discreta no significa necesariamente que siempre deba hacerlo, particularmente cuando separarlo no hace nada para reducir la complejidad de la tarea en cuestión.

Mi opinión personal es que debe elegir el patrón más cercano al modelo mental que percibe que comprenderán los consumidores de su clase. Si usted es el único consumidor, elija el modelo que más le guste. En el caso de Stream e ICollection, creo que tener una sola definición de estos está mucho más cerca del modelo mental construido por años de desarrollo en sistemas similares. Cuando habla de secuencias, habla de secuencias de archivos y secuencias de memoria, no si son legibles o grabables. Del mismo modo, cuando habla de colecciones, rara vez se refiere a ellas en términos de "capacidad de escritura".

Mi regla general en este caso: siempre busque una manera de dividir los comportamientos en interfaces más específicas, en lugar de tener "modos". de operación, siempre que complemente un modelo mental simple. Si es difícil pensar en los comportamientos separados como cosas separadas, use un patrón basado en modo y documente muy con claridad.

Otros consejos

Bien, aquí hay otro intento que esperamos sea más útil que mi otra respuesta ...

Es lamentable que MSDN no brinde ninguna garantía específica sobre cómo CanRead / CanWrite / CanSeek puede cambiar con el tiempo. Creo que sería razonable suponer que si una secuencia es legible, seguirá siendo legible hasta que se cierre, y lo mismo se aplicará a las otras propiedades

En algunos casos, creo que sería razonable que una secuencia se volviera a buscar más tarde; por ejemplo, podría almacenar todo lo que lee hasta que llegue al final de los datos subyacentes, y luego permitir la búsqueda dentro de él luego para permitir que los clientes relean los datos. Sin embargo, creo que sería razonable que un adaptador ignore esa posibilidad.

Esto debería ocuparse de todos los casos, excepto los más patológicos. (¡Flujos diseñados para causar estragos!) Agregar estos requisitos a la documentación existente es un cambio teóricamente innovador, aunque sospecho que el 99.9% de las implementaciones ya lo obedecerán. Aún así, puede valer la pena sugerir en Connect .

Ahora, en cuanto a la discusión entre si usar un " basado en capacidades " API (como Stream ) y una interfaz basada ... el problema fundamental que veo es que .NET no proporciona la capacidad de especificar que una variable debe ser una referencia a una implementación de Más de una interfaz. Por ejemplo, no puedo escribir:

public static Foo ReadFoo(IReadable & ISeekable stream)
{
}

Si permitía esto, podría ser razonable, pero sin eso, terminas con una explosión de interfaces potenciales:

IReadable
IWritable
ISeekable
IReadWritable
IReadSeekable
IWriteSeekable
IReadWriteSeekable

Creo que es más complicado que la situación actual, aunque creo que apoyaría la idea de solo IReadable y IWritable además de clase Stream existente. Eso facilitaría a los clientes expresar declarativamente lo que necesitan.

Con Código de contratos , las API pueden declarar lo que proporcionan y qué requieren, ciertamente:

public Stream OpenForReading(string name)
{
    Contract.Ensures(Contract.Result<Stream>().CanRead);

    ...
}

public void ReadFrom(Stream stream)
{
    Contract.Requires(stream.CanRead);

    ...
}

No sé cuánto puede ayudar el comprobador estático con eso, o cómo hace frente al hecho de que las secuencias do se vuelven ilegibles / no escribibles cuando están cerradas.

stream.CanRead solo comprueba si el flujo subyacente tiene posibilidad de lectura. No dice nada sobre si será posible la lectura real (por ejemplo, error de disco).

No es necesario capturar NotImplementedException si usó alguna de las clases * Reader, ya que todas admiten lectura. Solo * Writer tendrá CanRead = False y lanzará esa excepción. Si sabe que la transmisión admite la lectura (por ejemplo, usó StreamReader), en mi humilde opinión, no es necesario realizar una verificación adicional.

Aún necesita detectar excepciones, ya que cualquier error durante la lectura las arrojará (por ejemplo, error de disco).

Observe también que cualquier código que no esté documentado como seguro para subprocesos no lo es. Por lo general, los miembros estáticos son seguros para subprocesos, pero los miembros de instancia no lo son; sin embargo, es necesario verificar la documentación de cada clase.

De su pregunta y de todos los comentarios posteriores, supongo que su problema es con la claridad y la "corrección". del contrato declarado. El contrato declarado es lo que está en la documentación en línea de MSDN.

Lo que ha señalado es que falta algo en la documentación que lo obliga a hacer suposiciones sobre el contrato. Más específicamente, debido a que no se dice nada acerca de la volatilidad de la propiedad de legibilidad de una transmisión, la única suposición que se puede hacer es que es posible que una NotSupportedException sea arrojado, independientemente de cuál sea el valor de la propiedad CanRead correspondiente unos pocos milisegundos (o más) antes.

Creo que hay que ir con la intención de esta interfaz en este caso, es decir:

  1. Si usa más de un hilo, todas las apuestas están desactivadas;
  2. hasta que llame a algo en la interfaz que posiblemente cambie el estado de la transmisión, puede asumir con seguridad que el valor de CanRead es invariable.

Sin perjuicio de lo anterior, los métodos de lectura * pueden potencialmente arrojar una NotSupportedException .

El mismo argumento se puede aplicar a todas las demás propiedades de Can *.

  

También agradecería los comentarios sobre la validez de este patrón (los pares CanXXX, XXX ()).

Cuando veo una instancia de este patrón, generalmente esperaría esto:

  1. Un miembro sin parámetros CanXXX siempre devolverá el mismo valor, a menos que & # 8230;

  2. & # 8230; en presencia de un CanXXXChanged evento , donde un CanXXX sin parámetros puede devolver un valor diferente antes y después de una ocurrencia de ese evento; pero no cambiará sin activar el evento.

  3. Un parámetro CanXXX(…) parametrizado puede devolver diferentes valores para diferentes argumentos; pero para los mismos argumentos, es probable que devuelva el mismo valor. Es decir, es probable que CanXXX (constValue) permanezca constante.

      

    Estoy siendo cauteloso aquí: si stream.CanWriteToDisk (largeConstObject) devuelve true ahora, es razonable suponer que siempre devolverá true en el futuro? Probablemente no, así que quizás depende del contexto si un CanXXX (& # 8230;) parametrizado devolverá el mismo valor para los mismos argumentos o no.

  4. Una llamada a XXX(…) solo puede tener éxito si CanXXX devuelve true .


Dicho esto, estoy de acuerdo en que el uso de Stream de este patrón es algo problemático. Al menos en teoría, si no tanto en la práctica.

Esto suena más como un problema teórico que práctico. Realmente no puedo pensar en ninguna situación en la que un flujo se vuelva ilegible / no escribible otro que no sea debido al cierre.

Puede haber casos de esquina, pero no esperaría que aparecieran a menudo. No creo que la gran mayoría del código deba preocuparse por esto.

Sin embargo, es un problema filosófico interesante.

EDITAR: abordando la pregunta de si CanRead, etc., son útiles, creo que aún lo son, principalmente para la validación de argumentos. Por ejemplo, el hecho de que un método tome una secuencia que va a querer leer en algún momento no significa que quiera leerla justo al comienzo del método, pero ahí es donde idealmente se debe realizar la validación del argumento. Esto realmente no es diferente a verificar si un parámetro es nulo y arrojar ArgumentNullException en lugar de esperar a que se arroje una NullReferenceException cuando se da por primera vez su desreferencia.

Además, CanSeek es ligeramente diferente: en algunos casos, su código puede hacer frente tanto a secuencias buscables como no buscables, pero con más eficiencia en el caso buscable.

Esto se basa en la "capacidad de búsqueda" etc. permanecen consistentes, pero como he dicho, eso parece ser cierto en la vida real.


Bien, intentemos decir esto de otra manera ...

A menos que esté leyendo / buscando dentro de la memoria y ya se haya asegurado de que haya suficientes datos, o esté escribiendo dentro de un búfer preasignado, existe siempre una posibilidad de que las cosas salgan mal. Los discos fallan o se llenan, las redes colapsan, etc. Estas cosas suceden suceden en la vida real, por lo que siempre debe codificar de una manera que sobreviva al fracaso (o elija conscientemente ignorar el problema cuando no lo hace t realmente importa).

Si su código puede hacer lo correcto en el caso de una falla del disco, es probable que pueda sobrevivir a un FileStream pasando de ser escribible a no escribible.

Si Stream tuviera contratos firmes, tendrían que ser increíblemente débiles; no podría usar la comprobación estática para demostrar que su código siempre funcionará. Lo mejor que puede hacer es demostrar que hizo lo correcto ante el fracaso.

No creo que Stream vaya a cambiar pronto. Aunque ciertamente acepto que podría estar mejor documentado, no acepto la idea de que esté "completamente roto". Estaría más roto si no pudiéramos usarlo en la vida real ... y si pudiera estar más roto de lo que está ahora, lógicamente no está completamente roto .

Tengo problemas mucho mayores con el marco, como el estado relativamente pobre de las API de fecha / hora. Se han convertido en un lote mejor en las últimas dos versiones, pero todavía les falta mucha funcionalidad de (digamos) Joda Time . La falta de colecciones inmutables incorporadas, el escaso soporte para la inmutabilidad en el idioma, etc., son problemas reales que me causan dolores de cabeza reales . Prefiero verlos abordados que pasar años en Stream , lo que me parece un problema teórico algo intratable que causa pocos problemas en la vida real.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top