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?
fuente
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? ?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.Respuestas:
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
SomeClass
yISomeClass
es un mal ejemplo, porque sería como tener unaOracleObjectSerializer
clase y unaIOracleObjectSerializer
interfaz.Un ejemplo más preciso sería algo así
OracleObjectSerializer
como aIObjectSerializer
. 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
IObjectSerializer
sin importarle cómo funciona. Supongamos por un segundo ahora que también tiene unaSQLServerObjectSerializer
implementación adicionalOracleObjectSerializer
. 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
IObjectSerializer
y lanzarlaOracleObjectSerializer
y luego llamar al métodosetProperty
disponible solo enOracleObjectSerializer
. Esto es malo porque , aunque puede saber que una instancia esOracleObjectSerializer
, 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 unaIObjectSerializer
instancia aOracleObjectSerializer
ay 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
setProperty
en la clase de implementación como en el caso de myOracleObjectSerializer
si se realiza correctamente. Si abstraes una claseOracleObjectSerializer
aIObjectSerializer
, 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 unaDog
clase funcione como unaIPerson
implementación).El enfoque correcto sería proporcionar un
setProperty
método paraIObjectSerializer
. Métodos similaresSQLServerObjectSerializer
funcionarían idealmente a través de estesetProperty
método. Mejor aún, estandariza los nombres de propiedades a través de un lugarEnum
donde cada implementación traduce esa enumeración al equivalente de su propia terminología de base de datos.En pocas palabras, usar un
ISomeClass
es 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.fuente
IObjectSerializer
deOracleObjectSerializer
porque “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 usarOracleObjectSerializer
todo 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.La respuesta aceptada es correcta y muy útil, pero me gustaría abordar brevemente específicamente la línea de código que solicitó:
En términos generales, esto no es terrible. Lo que debe evitarse siempre que sea posible sería hacer esto:
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
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
myClass
tiene el tipoISomeClass
, 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.fuente
Creo que es más fácil mostrar su código con un mal ejemplo:
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.
fuente
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 .
Tú 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.
fuente
Animal
/Giraffe
ejemplo anterior, si hicieraAnimal a = (Animal)g;
los bits se reinterpretaría (cualquier dato específico de Giraffe se interpretaría como "no parte de este objeto").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
:Ahora suponga que hay muchas implementaciones:
Uno puede pensar en muchas implementaciones diferentes.
Ahora supongamos que tenemos el siguiente código:
Muestra claramente nuestra intención que queremos usar
iList
. Claro que ya no podremos realizarDynamicArrayList
operaciones específicas, pero necesitamos aiList
.Considere el siguiente código:
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).
fuente
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.
fuente