¿Qué quiere decir el autor al transmitir la referencia de interfaz a cualquier implementación?

17

Actualmente estoy en el proceso de tratar de dominar C #, así que estoy leyendo Adaptive Code a través de C # por Gary McLean Hall .

Escribe sobre patrones y antipatrones. En la parte de implementaciones versus interfaces, escribe lo siguiente:

Los desarrolladores que son nuevos en el concepto de programación para interfaces a menudo tienen dificultades para soltar lo que está detrás de la interfaz.

En tiempo de compilación, cualquier cliente de una interfaz no debe tener idea de qué implementación de la interfaz está utilizando. Tal conocimiento puede conducir a suposiciones incorrectas que acoplan al cliente a una implementación específica de la interfaz.

Imagine el ejemplo común en el que una clase necesita guardar un registro en un almacenamiento persistente. Para hacerlo, delega correctamente a una interfaz, que oculta los detalles del mecanismo de almacenamiento persistente utilizado. Sin embargo, no sería correcto hacer suposiciones sobre qué implementación de la interfaz se está utilizando en tiempo de ejecución. Por ejemplo, transmitir la referencia de interfaz a cualquier implementación siempre es una mala idea.

Puede ser la barrera del idioma o mi falta de experiencia, pero no entiendo lo que eso significa. Esto es lo que entiendo:

Tengo un proyecto divertido de tiempo libre para practicar C #. Ahí tengo una clase:

public class SomeClass...

Esta clase se usa en muchos lugares. Mientras aprendía C #, leí que es mejor abstraer con una interfaz, así que hice lo siguiente

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

Así que entré en algunas referencias de clase y las reemplacé con ISomeClass.

Excepto en la construcción, donde escribí:

ISomeClass myClass = new SomeClass();

¿Estoy entendiendo correctamente que esto está mal? En caso afirmativo, ¿por qué y qué debo hacer en su lugar?

Marshall
fuente
25
En ninguna parte de su ejemplo está convirtiendo un objeto de tipo de interfaz en un tipo de implementación. Está asignando algo de tipo de implementación a una variable de interfaz, que está perfectamente bien y es correcta.
Caleth
1
¿Qué quiere decir "en el constructor donde escribí ISomeClass myClass = new SomeClass();? Si realmente quiere decir eso, eso es recurrencia en el constructor, probablemente no lo que quiere. Esperemos que se refiera a" construcción ", es decir, asignación, tal vez, pero no en el propio constructor, ¿verdad? ?
Erik Eidt
@Erik: Sí. En construcción. Estás en lo correcto. Corregirá la pregunta. Gracias
Marshall
Dato curioso: F # tiene una historia mejor que C # en ese sentido: elimina las implementaciones de interfaz implícitas, por lo que cada vez que desee llamar a un método de interfaz, debe actualizar al tipo de interfaz. Esto deja muy claro cuándo y cómo está utilizando las interfaces en su código, y hace que la programación de las interfaces esté mucho más arraigada en el lenguaje.
scrwtp
3
Esto está un poco fuera de tema, pero creo que el autor diagnostica erróneamente el problema que tienen las personas nuevas en el concepto. En mi opinión, el problema es que las personas nuevas en el concepto no saben cómo hacer buenas interfaces. Es muy fácil crear interfaces demasiado específicas que en realidad no proporcionan ninguna generalidad (lo que bien podría estar sucediendo ISomeClass), pero también es fácil crear interfaces demasiado generales para las que es imposible escribir código útil en cuyo momento las únicas opciones son para repensar la interfaz y reescribir el código o para bajar.
Derek Elkins salió del SE

Respuestas:

37

Resumir su clase en una interfaz es algo que debe considerar si tiene la intención de escribir otras implementaciones de dicha interfaz o si existe la posibilidad de hacerlo en el futuro.

Así que tal vez SomeClassy ISomeClasses un mal ejemplo, porque sería como tener una OracleObjectSerializerclase y una IOracleObjectSerializerinterfaz.

Un ejemplo más preciso sería algo así OracleObjectSerializercomo a IObjectSerializer. El único lugar en su programa donde le importa qué implementación usar es cuando se crea la instancia. A veces esto se desacopla aún más mediante el uso de un patrón de fábrica.

En cualquier otro lugar de su programa debe usar IObjectSerializersin importarle cómo funciona. Supongamos por un segundo ahora que también tiene una SQLServerObjectSerializerimplementación adicional OracleObjectSerializer. Ahora suponga que necesita establecer alguna propiedad especial para establecer y ese método solo está presente en OracleObjectSerializer y no en SQLServerObjectSerializer.

Hay dos formas de hacerlo: la forma incorrecta y el enfoque del principio de sustitución de Liskov .

La forma incorrecta

La forma incorrecta, y la misma instancia a la que se hace referencia en su libro, sería tomar una instancia IObjectSerializery lanzarla OracleObjectSerializery luego llamar al método setPropertydisponible solo en OracleObjectSerializer. Esto es malo porque , aunque puede saber que una instancia es OracleObjectSerializer, está introduciendo otro punto en su programa en el que le interesa saber qué implementación es. Cuando esa implementación cambie, y presumiblemente lo hará tarde o temprano si tiene implementaciones múltiples, en el mejor de los casos, necesitará encontrar todos estos lugares y hacer los ajustes correctos. En el peor de los casos, emite una IObjectSerializerinstancia a OracleObjectSerializeray recibe un error de tiempo de ejecución en la producción.

Enfoque del principio de sustitución de Liskov

Liskov dijo que nunca debería necesitar métodos como setPropertyen la clase de implementación como en el caso de my OracleObjectSerializersi se realiza correctamente. Si abstraes una clase OracleObjectSerializera IObjectSerializer, debes abarcar todos los métodos necesarios para usar esa clase, y si no puedes, entonces algo está mal con tu abstracción ( por ejemplo, intentar hacer que una Dogclase funcione como una IPersonimplementación).

El enfoque correcto sería proporcionar un setPropertymétodo para IObjectSerializer. Métodos similares SQLServerObjectSerializerfuncionarían idealmente a través de este setPropertymétodo. Mejor aún, estandariza los nombres de propiedades a través de un lugar Enumdonde cada implementación traduce esa enumeración al equivalente de su propia terminología de base de datos.

En pocas palabras, usar un ISomeClasses solo la mitad. Nunca debería necesitar lanzarlo fuera del método responsable de su creación. Hacerlo es casi seguro un grave error de diseño.

Neil
fuente
1
Me parece que, si se va a elenco IObjectSerializerde OracleObjectSerializerporque “sabe” que esto es lo que es, entonces usted debe ser honesto con uno mismo (y lo más importante, con otros que puedan mantener este código, que puede incluir su futuro self), y usar OracleObjectSerializertodo el camino desde donde se crea hasta donde se usa. Esto hace que sea muy público y claro que está introduciendo una dependencia en una implementación particular, y el trabajo y la fealdad involucrados en hacerlo se convierten, en sí mismos, en una fuerte pista de que algo está mal.
KRyan
(Y, si por alguna razón usted realmente no tiene que depender de una implementación particular, se hace mucho más claro que esto es lo que está haciendo y que lo está haciendo con la intención y el propósito. Esta “” nunca debería suceder, por supuesto, y el 99% de las veces podría parecer como si estuviera sucediendo en realidad no lo es y que debe arreglar las cosas, pero nunca nada es 100% seguro o las cosas como debe ser).
KRyan
@KRyan Absolutamente. La abstracción solo debe usarse si la necesita. Usar la abstracción cuando no es necesario solo sirve para hacer que el código sea un poco más difícil de entender.
Neil
29

La respuesta aceptada es correcta y muy útil, pero me gustaría abordar brevemente específicamente la línea de código que solicitó:

ISomeClass myClass = new SomeClass();

En términos generales, esto no es terrible. Lo que debe evitarse siempre que sea posible sería hacer esto:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

Cuando su código recibe una interfaz externa, pero internamente la convierte en una implementación específica, "Porque sé que solo será esa implementación". Incluso si eso terminó siendo cierto, al usar una interfaz y convertirla en una implementación, renuncia voluntariamente a la seguridad de tipo real solo para que pueda pretender usar la abstracción. Si alguien más trabajara en el código más tarde y viera un método que aceptara un parámetro de interfaz, entonces supondrá que cualquier implementación de esa interfaz es una opción válida para pasar. Incluso podría ser usted mismo un poco línea después de que haya olvidado que un método específico radica en qué parámetros necesita. Si alguna vez siente la necesidad de transmitir desde una interfaz a una implementación específica, entonces la interfaz, la implementación, o el código que los hace referencia está incorrectamente diseñado y debería cambiar. Por ejemplo, si el método solo funciona cuando el argumento pasado es una clase específica, entonces el parámetro solo debe aceptar esa clase.

Ahora, mirando hacia atrás a su llamada de constructor

ISomeClass myClass = new SomeClass();

Los problemas del casting no se aplican realmente. Nada de esto parece estar expuesto externamente, por lo que no hay ningún riesgo particular asociado con él. Esencialmente, esta línea de código en sí es un detalle de implementación que las interfaces están diseñadas para abstraer, para que un observador externo lo vea funcionar de la misma manera, independientemente de lo que hagan. Sin embargo, esto tampoco GANA nada de la existencia de una interfaz. Tu myClasstiene el tipo ISomeClass, pero no tiene ningún motivo, ya que siempre se le asigna una implementación específica,SomeClass. Hay algunas ventajas potenciales menores, como poder intercambiar la implementación en el código cambiando solo la llamada del constructor o reasignar esa variable más tarde a una implementación diferente, pero a menos que haya otro lugar que requiera que la variable se escriba en la interfaz en lugar de La implementación de este patrón hace que su código parezca que las interfaces se usaron solo de memoria, no fuera de la comprensión real de los beneficios de las interfaces.

Kamil Drakari
fuente
1
Apache Math hace esto con Cartesian3D Source, línea 286 , que puede ser realmente molesto.
J_F_B_M
1
Esta es la respuesta realmente correcta que aborda la pregunta original.
Benjamin Gruenbaum
2

Creo que es más fácil mostrar su código con un mal ejemplo:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

El problema es que cuando inicialmente escribe el código, probablemente solo haya una implementación de esa interfaz, por lo que la transmisión aún funcionaría, es solo que en el futuro, podría implementar otra clase y luego (como muestra mi ejemplo), usted intente y acceda a datos que no existen en el objeto que está utilizando.

Neil
fuente
Hola, señor. ¿Alguien te ha dicho lo guapo que eres?
Neil
2

Solo por claridad, definamos el casting.

Lanzar es convertir a la fuerza algo de un tipo a otro. Un ejemplo común es emitir un número de coma flotante a un tipo entero. Se puede especificar una conversión específica al emitir, pero el valor predeterminado es simplemente reinterpretar los bits.

Aquí hay un ejemplo de transmisión desde esta página de documentos de Microsoft .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

podría hacer lo mismo y el reparto algo que implementa una interfaz a una aplicación particular de esa interfaz, pero que no deben porque eso dará como resultado un error o un comportamiento inesperado si se utiliza una implementación diferente de lo esperado.

Ryan1729
fuente
1
"Lanzar es convertir algo de un tipo a otro". - No. Casting es convertir explícitamente algo de un tipo a otro. (Específicamente, "conversión" es el nombre de la sintaxis utilizada para especificar esa conversión). Las conversiones implícitas no son conversiones. "Se puede especificar una conversión específica al emitir, pero el valor predeterminado es simplemente reinterpretar los bits". -- Ciertamente no. Hay muchas conversiones, tanto implícitas como explícitas, que implican cambios significativos en los patrones de bits.
hvd
@hvd He hecho una corrección ahora con respecto a lo explícito del casting. Cuando dije que el valor predeterminado es simplemente reinterpretar los bits, estaba tratando de expresar que si tuviera que hacer su propio tipo, entonces, en los casos en que un molde se define automáticamente, cuando lo convierte en otro tipo, los bits se reinterpretarán . En el Animal/ Giraffeejemplo anterior, si hiciera Animal a = (Animal)g;los bits se reinterpretaría (cualquier dato específico de Giraffe se interpretaría como "no parte de este objeto").
Ryan1729
A pesar de lo que dice hvd, las personas a menudo usan el término "emitir" en referencia a las conversiones implícitas; consulte, por ejemplo, https://www.google.com/search?q="implicit+cast"&tbm=bks . Técnicamente creo que es más correcto reservar el término "emitir" para conversiones explícitas, siempre y cuando no se confunda cuando otros lo usen de manera diferente.
ruakh
0

Mis 5 centavos:

Todos esos ejemplos están bien, pero no son ejemplos del mundo real y no mostraron las intenciones del mundo real.

No conozco C #, así que daré un ejemplo abstracto (mezcla entre Java y C ++). Espero que esté bien.

Supongamos que tiene una interfaz iList:

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

Ahora suponga que hay muchas implementaciones:

  • DynamicArrayList: utiliza una matriz plana, rápida de insertar y eliminar al final.
  • LinkedList: utiliza una lista de doble enlace, rápida de insertar al frente y al final.
  • AVLTreeList: usa AVL Tree, rápido para hacer todo, pero usa mucha memoria
  • SkipList: usa SkipList, rápido para hacer todo, más lento que AVL Tree, pero usa menos memoria.
  • HashList: utiliza HashTable

Uno puede pensar en muchas implementaciones diferentes.

Ahora supongamos que tenemos el siguiente código:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

Muestra claramente nuestra intención que queremos usar iList. Claro que ya no podremos realizar DynamicArrayListoperaciones específicas, pero necesitamos a iList.

Considere el siguiente código:

iList list = factory.getList();

Ahora ni siquiera sabemos cuál es la implementación. Este último ejemplo se usa a menudo en el procesamiento de imágenes cuando carga algún archivo del disco y no necesita su tipo de archivo (gif, jpeg, png, bmp ...), pero todo lo que quiere es manipular algunas imágenes (voltear, escala, guardar como png al final).

Mella
fuente
0

Tiene una interfaz ISomeClass y un objeto myObject del cual no sabe nada de su código, excepto que está declarado para implementar ISomeClass.

Tiene una clase SomeClass, que sabe que implementa la interfaz ISomeClass. Lo sabe porque se declaró que implementa ISomeClass, o lo implementó usted mismo para implementar ISomeClass.

¿Qué hay de malo en enviar myClass a SomeClass? Dos cosas están mal. Uno, realmente no sabes que myClass es algo que se puede convertir a SomeClass (una instancia de SomeClass o una subclase de SomeClass), por lo que el reparto podría salir mal. Dos, no deberías tener que hacer esto. Debería trabajar con myClass declarado como iSomeClass y utilizar métodos ISomeClass.

El punto donde obtiene un objeto SomeClass es cuando se llama a un método de interfaz. En algún momento, llama a myClass.myMethod (), que se declara en la interfaz, pero tiene una implementación en SomeClass y, por supuesto, posiblemente en muchas otras clases que implementan ISomeClass. Si una llamada termina en su código SomeClass.myMethod, entonces sabe que self es una instancia de SomeClass, y en ese momento está absolutamente bien y es correcto usarlo como un objeto SomeClass. Por supuesto, si en realidad es una instancia de OtherClass y no SomeClass, entonces no llegará al código SomeClass.

gnasher729
fuente