Mejores prácticas que devuelven objetos de solo lectura

11

Tengo una pregunta de "mejores prácticas" sobre OOP en C # (pero se aplica a todos los idiomas).

Considere tener una clase de biblioteca con un objeto que se expondrá al público, digamos a través del descriptor de acceso de propiedad, pero no queremos que el público (las personas que usan esta clase de biblioteca) lo modifiquen.

class A
{
    // Note: List is just example, I am interested in objects in general.
    private List<string> _items = new List<string>() { "hello" }

    public List<string> Items
    {
        get
        {
            // Option A (not read-only), can be modified from outside:
            return _items;

            // Option B (sort-of read-only):
            return new List<string>( _items );

            // Option C, must change return type to ReadOnlyCollection<string>
            return new ReadOnlyCollection<string>( _items );
        }
    }
}

Obviamente, el mejor enfoque es la "opción C", pero muy pocos objetos tienen la variante ReadOnly (y ciertamente ninguna clase definida por el usuario la tiene).

Si fuera el usuario de la clase A, ¿esperaría cambios en

List<string> someList = ( new A() ).Items;

propagarse al objeto original (A)? ¿O está bien devolver un clon siempre que se haya escrito así en comentarios / documentación? Creo que este enfoque de clonación podría conducir a errores bastante difíciles de rastrear.

Recuerdo que en C ++ podríamos devolver objetos const y solo podría llamar a los métodos marcados como const en él. Supongo que no hay tal característica / patrón en C #? Si no, ¿por qué no lo incluirían? Creo que se llama corrección constante.

Pero, de nuevo, mi pregunta principal es sobre "qué esperarías" o cómo manejar la Opción A vs B.

NeverStopLearning
fuente
2
Eche un vistazo a este artículo sobre esta vista previa de nuevas colecciones inmutables para .NET 4.5: blogs.msdn.com/b/bclteam/archive/2012/12/18/… Ya puede obtenerlas a través de NuGet.
Kristof Claes

Respuestas:

10

Además de la respuesta de @ Jesse, que es la mejor solución para las colecciones: la idea general es diseñar su interfaz pública de A de manera que solo regrese

  • tipos de valor (como int, double, struct)

  • objetos inmutables (como "cadena" o clases definidas por el usuario que ofrecen solo métodos de solo lectura, al igual que su clase A)

  • (solo si debe hacerlo): copias de objetos, como lo demuestra su "Opción B"

IEnumerable<immutable_type_here> es inmutable por sí mismo, por lo que este es solo un caso especial de lo anterior.

Doc Brown
fuente
19

También tenga en cuenta que debe programar en interfaces y, en general, lo que desea devolver de esa propiedad es un IEnumerable<string>. A List<string>es un poco un no-no porque generalmente es mutable y voy a ir tan lejos como para decir que, ReadOnlyCollection<string>como implementador, ICollection<string>siente que viola el Principio de sustitución de Liskov.

Jesse C. Slicer
fuente
66
+1, usar un IEnumerable<string>es exactamente lo que uno quiere aquí.
Doc Brown
2
En .Net 4.5, también puede usar IReadOnlyList<T>.
svick
3
Nota para otras partes interesadas. Al considerar el descriptor de acceso a la propiedad: si solo devuelve IEnumerable <string> en lugar de List <string> haciendo return _list (+ conversión implícita a IEnumerable), esta lista se puede volver a convertir en List <string> y cambiará y los cambios serán propagarse en el objeto principal. Puede devolver la nueva Lista <cadena> (_list) (+ conversión implícita posterior a IEnumerable) que protegerá la lista interna. Puede encontrar más información sobre esto aquí: stackoverflow.com/questions/4056013/…
NeverStopLearning
1
¿Es mejor exigir que cualquier destinatario de una colección que necesite acceder a él por índice esté preparado para copiar los datos en un tipo que lo facilite, o es mejor exigir que el código que devuelve la colección lo suministre en una forma que es accesible por índice? A menos que sea probable que el proveedor lo tenga en una forma que no facilite el acceso por índice, consideraría que el valor de liberar a los destinatarios de la necesidad de hacer su propia copia redundante excedería el valor de liberar al proveedor de tener que suministrar un formulario de acceso aleatorio (por ejemplo, solo lectura IList<T>)
supercat
2
Una cosa que no me gusta de IEnumerable es que nunca se sabe si se ejecuta como una rutina o si es solo un objeto de datos. Si se ejecuta como una rutina, podría provocar errores sutiles, por lo que es bueno saberlo a veces. Es por eso que tiendo a preferir ReadOnlyCollection o IReadOnlyList, etc.
Steve Vermeulen
3

Encontré un problema similar hace un tiempo, y mi solución fue abajo, pero realmente solo funciona si controlas la biblioteca también:


Comience con su clase A

public class A
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Luego deriva una interfaz de esto con solo la parte de obtención de las propiedades (y cualquier método de lectura) y haz que A implemente eso

public interface IReadOnlyA
{
    public int N { get; }
}
public class A : IReadOnlyA
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Luego, por supuesto, cuando desee utilizar la versión de solo lectura, un simple lanzamiento es suficiente. Esto es exactamente lo que su solución C es, realmente, pero pensé en ofrecer el método con el que lo logré

Andy Hunt
fuente
2
El problema con esto es que es posible devolverlo a la versión mutable. Y su violización de LSP, porque el estado observable a través de los métodos de la clase base (en este caso, la interfaz) puede cambiarse con nuevos métodos de la subclase. Pero al menos comunica intención, incluso si no la hace cumplir.
user470365
1

Para cualquiera que encuentre esta pregunta y aún esté interesado en la respuesta, ahora hay otra opción que está exponiendo ImmutableArray<T>. Estos son algunos puntos por los que creo que es mejor IEnumerable<T>o ReadOnlyCollection<T>:

  • establece claramente que es inmutable (API expresiva)
  • indica claramente que la colección ya está formada (en otras palabras, no se inicializa perezosamente y está bien iterarla varias veces)
  • si se conoce el recuento de elementos, no oculta ese conocimiento como lo IEnumerable<T>hace
  • ya que el cliente no sabe cuál es la implementación IEnumerableo IReadOnlyCollectno puede asumir que nunca cambia (en otras palabras, no hay garantía de que los elementos de una colección no hayan sido modificados mientras la referencia a esa colección permanece sin cambios)

Además, si APi necesita notificar al cliente sobre los cambios en la colección, expondría un evento como miembro separado.

Tenga en cuenta que no digo que IEnumerable<T>sea ​​malo en general. Todavía lo usaría al pasar la colección como argumento o al devolver la colección inicializada perezosamente (en este caso particular, tiene sentido ocultar el recuento de elementos porque aún no se conoce).

Pavels Ahmadulins
fuente