2010-10-29 12 views
6

Revisando un earlier question en SO, comencé a pensar en la situación en la que una clase expone un valor, como una colección, como una interfaz implementada por el tipo de valor. En el siguiente ejemplo de código, estoy usando una lista y expongo esa lista como IEnumerable.Transmisión desde la interfaz al tipo subyacente

La exposición de la lista a través de la interfaz IEnumerable define la intención de que la lista solo se enumere, no se modifique. Sin embargo, dado que la instancia puede volver a enviarse a una lista, la lista en sí misma puede, por supuesto, modificarse.

También incluyo en la muestra una versión del método que impide la modificación copiando las referencias de los elementos de la lista a una nueva lista cada vez que se llama al método, impidiendo así los cambios en la lista subyacente.

Así que mi pregunta es, ¿debería todo el código exponer un tipo concreto como una interfaz implementada hacerlo por medio de una operación de copia? ¿Habría valor en una construcción de lenguaje que indique explícitamente "Quiero exponer este valor a través de una interfaz, y el código de llamada solo debería poder usar este valor a través de la interfaz"? ¿Qué técnicas usan otras personas para evitar efectos colaterales no deseados como estos al exponer valores concretos a través de sus interfaces?

Tenga en cuenta, entiendo que el comportamiento ilustrado es el comportamiento esperado. No pretendo que este comportamiento sea incorrecto, solo que permite el uso de funcionalidades que no sean las expresadas. Tal vez estoy asignando demasiada importancia a la interfaz, pensando en ella como una restricción de funcionalidad. ¿Pensamientos?

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 

namespace TypeCastTest 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      // Demonstrate casting situation 
      Automobile castAuto = new Automobile(); 

      List<string> doorNamesCast = (List<string>)castAuto.GetDoorNamesUsingCast(); 

      doorNamesCast.Add("Spare Tire"); 

      // Would prefer this prints 4 names, 
      // actually prints 5 because IEnumerable<string> 
      // was cast back to List<string>, exposing the 
      // Add method of the underlying List object 
      // Since the list was cast to IEnumerable before being 
      // returned, the expressed intent is that calling code 
      // should only be able to enumerate over the collection, 
      // not modify it. 
      foreach (string doorName in castAuto.GetDoorNamesUsingCast()) 
      { 
       Console.WriteLine(doorName); 
      } 

      Console.WriteLine(); 

      // -------------------------------------- 

      // Demonstrate casting defense 
      Automobile copyAuto = new Automobile(); 

      List<string> doorNamesCopy = (List<string>)copyAuto.GetDoorNamesUsingCopy(); 

      doorNamesCopy.Add("Spare Tire"); 

      // This returns only 4 names, 
      // because the IEnumerable<string> that is 
      // returned is from a copied List<string>, so 
      // calling the Add method of the List object does 
      // not modify the underlying collection 
      foreach (string doorName in copyAuto.GetDoorNamesUsingCopy()) 
      { 
       Console.WriteLine(doorName); 
      } 

      Console.ReadLine(); 
     } 
    } 

    public class Automobile 
    { 
     private List<string> doors = new List<string>(); 
     public Automobile() 
     { 
      doors.Add("Driver Front"); 
      doors.Add("Passenger Front"); 
      doors.Add("Driver Rear"); 
      doors.Add("Passenger Rear"); 
     } 

     public IEnumerable<string> GetDoorNamesUsingCopy() 
     { 
      return new List<string>(doors).AsEnumerable<string>(); 
     } 
     public IEnumerable<string> GetDoorNamesUsingCast() 
     { 
      return doors.AsEnumerable<string>(); 
     } 
    } 

} 
+6

Y puede usar el reflejo para romper otras reglas y acceder a miembros privados. IMO, lo has declarado como un 'IEnumerable'; si alguien quiere "hacer trampa" y convertirlo en algo que no definió como parte de su API pública, entonces ese es su negocio y su riesgo. –

+0

@Kirk Woll: ese es un buen punto sobre el uso de la reflexión como un medio para "romper las reglas". Como parte de esta pregunta, quería que los comentarios de otras personas sobre si el hecho de convertir la interfaz al tipo concreto constituía una forma de "romper las reglas", o si la mayoría lo consideraba una opción esperada que debería tenerse en cuenta en el diseño de la API. Gracias por el comentario. – JeremyDWill

Respuesta

5

Una manera de evitar esto es mediante el uso de AsReadOnly() para prevenir dicha nefariousness. Creo que la verdadera respuesta es, nunca deberías depender de otra cosa que no sea la interfaz expuesta/contrato en términos de los tipos de devolución, etc. Hacer cualquier otra cosa desafía la encapsulación, evita que cambies tus implementaciones por otras que no lo hacen utilice una lista sino sólo una T [], etc., etc.

Editar:

y abajo de fundición a presión, como usted menciona es básicamente una violación de la Liskov Substition Principle, para obtener todos los aspectos técnicos y esas cosas.

+0

A menos que la API esté basada en hacer que el cliente genere objetos que se devuelven (porque solo pasan el tipo 'object'), el cliente * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * En el caso de IEnumerable, el código debería ser algo como 'var doorNames = new List (castAuto.GetDoorNamesUsingCast());' –

2

En una situación como esta, puede definir su propia clase de colección que implemente IEnumerable<T>. Internamente, su colección podría mantener un List<T> y entonces se podría simplemente devolver el enumerador de la lista subyacente:

public class MyList : IEnumerable<string> 
{ 
    private List<string> internalList; 

    // ... 

    IEnumerator<string> IEnumerable<string>.GetEnumerator() 
    { 
     return this.internalList.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return this.internalList.GetEnumerator(); 
    } 
} 
+0

Esto definitivamente resuelve el problema específico en el ejemplo y comunica más claramente el intento de uso en este escenario. – JeremyDWill

1

Una cosa que he aprendido trabajando con .NET (y con algunas personas que son rápidos para saltar a una solución de hack) es que si nada más, la reflexión a menudo permitirá a las personas pasar sus "protecciones".

Las interfaces no son grilletes de programación, son una promesa que su código hace a cualquier otro código que diga "Definitivamente puedo hacer estas cosas". Si "engañas" y conviertes el objeto de la interfaz en otro objeto porque tú, el programador, sabes algo que el programa no sabe, entonces estás rompiendo ese contrato. La consecuencia es una menor capacidad de mantenimiento y la dependencia de que nadie estropea nunca nada en esa cadena de ejecución, por temor a que se envíe otro objeto que no se emita correctamente.

Otros trucos como hacer cosas de solo lectura o esconder la lista real detrás de un contenedor son solo interrupciones. Podría excavar fácilmente en el tipo utilizando reflexión para sacar la lista privada si realmente lo deseaba.Y creo que hay atributos que puede aplicar a los tipos para evitar que las personas se reflejen en ellos.

Del mismo modo, las listas de solo lectura realmente no lo son. Probablemente podría encontrar una manera de modificar la lista. Y casi con seguridad puedo modificar los elementos en la lista. Entonces, un solo de lectura no es suficiente, ni es una copia o una matriz. Necesita una copia profunda (clon) de la lista original para proteger realmente los datos, hasta cierto punto.

Pero la verdadera pregunta es, ¿por qué estás luchando tan duro contra el contrato que escribiste. En ocasiones, la piratería de reflexión es una solución práctica cuando la biblioteca de otra persona está mal diseñada y no expone algo que necesita (o un error requiere que vaya a buscarla para arreglarla). Pero cuando tiene control sobre la interfaz Y el consumidor de En la interfaz, no hay excusa para no hacer que la interfaz expuesta públicamente sea tan robusta como lo necesite para hacer su trabajo.

O en resumen: Si necesita una lista, no devuelva IEnumerable, devuelva una lista. Si tiene un IEnumerable pero realmente necesita una lista, entonces es más seguro hacer una nueva lista desde ese IEnum y usar eso. Hay muy pocas razones (y aún menos, quizás no, buenas razones) para elegir un tipo simplemente porque "sé que en realidad es una lista, así que esto funcionará".

Sí, puede tomar medidas para tratar de evitar que las personas lo hagan, pero 1) cuanto más lucha contra las personas que insisten en romper el sistema, más difícil es tratar de romperlo y 2) solo buscan por más cuerda, y eventualmente obtendrán lo suficiente como para ahorcarse.

2

Una interfaz es una restricción en la implementación de un conjunto mínimo de cosas que debe hacer (incluso si "hacer" no es más que tirar un NotSupportedException, o incluso un NotImplementedException). No es una restricción que impida que la implementación haga más o en el código de llamada.

Cuestiones relacionadas