El is
operador de C # y el instanceof
operador de Java le permiten ramificarse en la interfaz (o, más groseramente, su clase base) que una instancia de objeto ha implementado.
¿Es apropiado usar esta función para la ramificación de alto nivel en función de las capacidades que proporciona una interfaz?
¿O debería una clase base proporcionar variables booleanas para proporcionar una interfaz para describir las capacidades que tiene un objeto?
Ejemplo:
if (account is IResetsPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
vs.
if (account.CanResetPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
¿Hay alguna dificultad para usar la implementación de la interfaz para la identificación de capacidades?
Este ejemplo fue solo eso, un ejemplo. Me pregunto sobre una aplicación más general.
object-oriented
TheCatWhisperer
fuente
fuente
CanResetPassword
solo será cierto cuando algo se implementeIResetsPassword
. Ahora está haciendo que una propiedad determine algo que el sistema de tipos debería controlar (y finalmente lo hará cuando realice la conversión).Respuestas:
Con la advertencia de que es mejor evitar el downcasting si es posible, entonces la primera forma es preferible por dos razones:
is
dispara, el downcast definitivamente funcionará. En el segundo formulario, debe asegurarse manualmente de que solo los objetos que implementanIResetsPassword
devuelven verdadero para esa propiedadEso no quiere decir que no pueda mejorar estos problemas de alguna manera. Podría, por ejemplo, hacer que la clase base tenga un conjunto de interfaces publicadas en las que pueda verificar la inclusión. Pero en realidad solo está implementando manualmente una parte del sistema de tipos existente que es redundante.
Por cierto, mi preferencia natural es la composición sobre la herencia, pero principalmente en lo que respecta al estado. La herencia de interfaz no es tan mala, por lo general. Además, si te encuentras implementando la jerarquía de tipos de un hombre pobre, es mejor que uses la que ya está allí, ya que el compilador te ayudará.
fuente
(account as IResettable)?.ResetPassword();
ovar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Las capacidades de un objeto no deben identificarse en absoluto.
El cliente que usa un objeto no debería tener que saber nada sobre cómo funciona. El cliente solo debe saber cosas que puede decirle al objeto que haga. Lo que hace el objeto, una vez que se le ha dicho, no es el problema del cliente.
Entonces en lugar de
o
considerar
o
porque
account
ya sabe si esto funcionará. ¿Por qué preguntarlo? Solo dile lo que quieres y deja que haga lo que sea que haga.Imagine que esto se hace de tal manera que al cliente no le importa si funcionó o no porque eso es algo más que un problema. El trabajo del cliente era solo hacer el intento. Ya lo ha hecho y tiene otras cosas que tratar. Este estilo tiene muchos nombres, pero el nombre que más me gusta es decir, no preguntar .
Al escribir al cliente es muy tentador pensar que tiene que hacer un seguimiento de todo y, por lo tanto, atraer hacia usted todo lo que cree que necesita saber. Cuando haces eso, giras objetos al revés. Valora tu ignorancia. Aleja los detalles y deja que los objetos se encarguen de ellos. Entre menos sepas, mejor.
fuente
if (!account.AttemptPasswordReset()) /* attempt failed */;
De esta manera, el cliente conoce el resultado pero evita algunos de los problemas con la lógica de decisión en la pregunta. Si el intento es asíncrono, pasaría una devolución de llamada.account
cuando se construyó. En este punto, podrían estar surgiendo ventanas emergentes, registros, impresiones y alertas de audio. El trabajo del cliente es decir "hazlo ahora". No tiene que hacer más que eso. No tienes que hacer todo en un solo lugar.Fondo
La herencia es una herramienta poderosa que tiene un propósito en la programación orientada a objetos. Sin embargo, no resuelve todos los problemas con elegancia: a veces, otras soluciones son mejores.
Si recuerda sus primeras clases de ciencias de la computación (suponiendo que tiene un título de CS), puede recordar que un profesor le dio un párrafo que establece lo que el cliente quiere que haga el software. Su trabajo es leer el párrafo, identificar a los actores y las acciones, y obtener un bosquejo de cuáles son las clases y los métodos. Habrá algunas pistas vagas allí que parecen importantes, pero no lo son. Existe una posibilidad muy real de malinterpretar los requisitos también.
Esta es una habilidad importante que incluso los más experimentados nos equivocamos: identificar adecuadamente los requisitos y traducirlos a lenguajes de máquina.
Tu pregunta
Según lo que ha escrito, creo que puede estar malinterpretando el caso general de las acciones opcionales que las clases pueden realizar. Sí, sé que su código es solo un ejemplo y está interesado en el caso general. Sin embargo, parece que quiere saber cómo manejar la situación en la que ciertos subtipos de un objeto pueden realizar una acción, pero otros no.
El hecho de que un objeto como un
account
tenga un tipo de cuenta no significa que se traduzca en un tipo en un lenguaje OO. "Escribir" en un lenguaje humano no siempre significa "clase". En el contexto de una cuenta, "tipo" puede correlacionarse más estrechamente con "conjunto de permisos". Desea utilizar una cuenta de usuario para realizar una acción, pero esa acción puede o no ser ejecutada por esa cuenta. En lugar de usar la herencia, usaría un delegado o token de seguridad.Mi solución
Considere una clase de cuenta que tiene varias acciones opcionales que puede realizar. En lugar de definir "puede realizar la acción X" a través de la herencia, ¿por qué no hacer que la cuenta devuelva un objeto delegado (restablecimiento de contraseña, remitente de formulario, etc.) o un token de acceso?
El beneficio de la última opción es ¿qué sucede si deseo usar las credenciales de mi cuenta para restablecer la contraseña de otra persona ?
El sistema no solo es más flexible, no solo he eliminado los tipos de yeso, sino que el diseño es más expresivo . Puedo leer esto y entender fácilmente lo que está haciendo y cómo se puede usar.
TL; DR: esto se lee como un problema XY . En general, cuando nos enfrentamos a dos opciones que no son óptimas, vale la pena dar un paso atrás y pensar "¿qué es lo que realmente estoy tratando de lograr aquí? eliminar el texto completo por completo? "
fuente
account.getPasswordResetter().resetPassword();
.The benefit to the last option there is what if I want to use my account credentials to reset someone else's password?
.. fácil. Cree un objeto de dominio de Cuenta para la otra cuenta y use ese. Las clases estáticas son problemáticas para las pruebas unitarias (ese método necesitará, por definición, acceso a algo de almacenamiento, por lo que ahora no puede probar fácilmente ningún código que toque esa clase) y es mejor evitarlo. La opción de devolver alguna otra interfaz a menudo es una buena idea, pero en realidad no trata el problema subyacente (simplemente movió el problema a la otra interfaz).Bill Venner dice que su enfoque está absolutamente bien ; salte a la sección titulada 'Cuándo usar instanceof'. Está cambiando a un tipo / interfaz específico que solo implementa ese comportamiento específico, por lo que tiene toda la razón de ser selectivo al respecto.
Sin embargo, si desea utilizar otro enfoque, hay muchas maneras de afeitarse un yak.
Existe el enfoque del polimorfismo; podría argumentar que todas las cuentas tienen una contraseña, por lo tanto, al menos todas las cuentas deberían poder intentar restablecer una contraseña. Esto significa que, en la clase de cuenta base, deberíamos tener un
resetPassword()
método que todas las cuentas implementen pero a su manera. La pregunta es cómo debe comportarse ese método cuando la capacidad no está allí.Podría devolver un vacío y completar silenciosamente si restablece la contraseña o no, tal vez tomando la responsabilidad de imprimir el mensaje internamente si no restablece la contraseña. No es la mejor idea.
Podría devolver un valor booleano que indica si el restablecimiento fue exitoso. Al encender ese booleano, podríamos relacionar que el restablecimiento de contraseña falló.
Podría devolver una Cadena que indica el resultado del intento de restablecimiento de contraseña. La cadena podría dar más detalles sobre por qué falló un reinicio y podría emitirse.
Podría devolver un objeto ResetResult que transmite más detalles y combina todos los elementos de retorno anteriores.
Podría devolver un vacío y, en su lugar, arrojar una excepción si intenta restablecer una cuenta que no tiene esa capacidad (no haga esto, ya que usar el manejo de excepciones para el control de flujo normal es una mala práctica por varias razones).
Tener un
canResetPassword()
método de correspondencia puede no parecer lo peor del mundo, ya que, en teoría, es una capacidad estática incorporada en la clase cuando se escribió. Sin embargo, esta es una pista de por qué el enfoque del método es una mala idea, ya que sugiere que la capacidad es dinámica y quecanResetPassword()
puede cambiar, lo que también crea la posibilidad adicional de que pueda cambiar entre pedir permiso y hacer la llamada. Como se mencionó en otra parte, diga en lugar de pedir permiso.La composición sobre la herencia podría ser una opción: podría tener un
passwordResetter
campo final (o un captador equivalente) y clases equivalentes para las que puede verificar nulo antes de llamarlo. Si bien actúa un poco como pedir permiso, la finalidad podría evitar cualquier naturaleza dinámica inferida.Puede pensar en externalizar la funcionalidad en su propia clase, que podría tomar una cuenta como parámetro y actuar en consecuencia (p
resetter.reset(account)
. Ej. ), Aunque a menudo esto también es una mala práctica (una analogía común es un comerciante que busca dinero en su billetera).En idiomas con la capacidad, puede usar mixins o rasgos, sin embargo, termina en la posición donde comenzó, donde podría estar comprobando la existencia de esas capacidades.
fuente
Para este ejemplo específico de " puede restablecer una contraseña ", recomendaría usar la composición sobre la herencia (en este caso, la herencia de una interfaz / contrato). Porque, al hacer esto:
Inmediatamente está especificando (en tiempo de compilación) que su clase ' puede restablecer una contraseña '. Pero, si en su escenario, la presencia de la capacidad es condicional y depende de otras cosas, entonces ya no puede especificar las cosas en tiempo de compilación. Entonces, sugiero hacer esto:
Ahora, en tiempo de ejecución, puede verificar si
myFoo.passwordResetter != null
antes de realizar esta operación. Si desea desacoplar las cosas aún más (y planea agregar muchas más capacidades), podría:ACTUALIZAR
Después de leer algunas otras respuestas y comentarios de OP, entendí que la pregunta no es sobre la solución compositiva. Así que creo que la pregunta es sobre cómo identificar mejor las capacidades de los objetos en general, en un escenario como el siguiente:
Ahora, intentaré analizar todas las opciones mencionadas:
OP segundo ejemplo:
Digamos que este código está recibiendo una clase de cuenta básica (por ejemplo:
BaseAccount
en mi ejemplo); Esto es malo ya que está insertando booleanos en la clase base, contaminándolo con código que no tiene ningún sentido estar allí.OP primer ejemplo:
Para responder a la pregunta, esto es más apropiado que la opción anterior, pero dependiendo de la implementación romperá el principio L de sólido, y probablemente las comprobaciones de este tipo se extenderían a través del código y dificultarían más el mantenimiento.
Respuesta de CandiedOrange:
Si este
ResetPassword
método se inserta en laBaseAccount
clase, también está contaminando la clase base con código inapropiado, como en el segundo ejemplo de OPLa respuesta de Snowman:
Esta es una buena solución, pero considera que las capacidades son dinámicas (y pueden cambiar con el tiempo). Sin embargo, después de leer varios comentarios de OP, supongo que la charla aquí es sobre el polimorfismo y las clases estáticamente definidas (aunque el ejemplo de cuentas apunta intuitivamente al escenario dinámico). EG: en este
AccountManager
ejemplo, la verificación de permiso sería consultas a DB; En la pregunta OP, las comprobaciones son intentos de lanzar los objetos.Otra sugerencia mía:
Use el patrón Método de plantilla para ramificaciones de alto nivel. La jerarquía de clases mencionada se mantiene como está; solo creamos controladores más apropiados para los objetos, a fin de evitar conversiones y propiedades / métodos inapropiados que contaminan la clase base.
Puede vincular la operación a la clase de cuenta adecuada mediante el uso de un diccionario / tabla hash, o realizar operaciones en tiempo de ejecución utilizando métodos de extensión, utilizar una
dynamic
palabra clave o, como última opción, utilizar solo un reparto para pasar el objeto de la cuenta a la operación (en en este caso, el número de lanzamientos es solo uno, al comienzo de la operación).fuente
Ninguna de las opciones que ha presentado son buenas OO. Si está escribiendo declaraciones sobre el tipo de un objeto, lo más probable es que esté haciendo mal OO (hay excepciones, esta no es una). Aquí está la respuesta simple de OO a su pregunta (puede no ser C # válido):
He modificado este ejemplo para que coincida con lo que estaba haciendo el código original. Según algunos de los comentarios, parece que la gente está obsesionada con saber si este es el código específico "correcto" aquí. Ese no es el punto de este ejemplo. Toda la sobrecarga polimórfica consiste esencialmente en ejecutar condicionalmente diferentes implementaciones de lógica basadas en el tipo de objeto. Lo que está haciendo en ambos ejemplos es atascar a mano lo que su idioma le brinda como característica. En pocas palabras, puede deshacerse de los subtipos y poner la capacidad de restablecer como una propiedad booleana del tipo de cuenta (ignorando otras características de los subtipos).
Sin una vista más amplia del diseño, es imposible saber si esta es una buena solución para su sistema en particular. Es simple y si funciona para lo que está haciendo, es probable que nunca tenga que volver a pensar mucho a menos que alguien no verifique CanResetPassword () antes de llamar a ResetPassword (). También puede devolver un valor booleano o fallar en silencio (no recomendado). Realmente depende de los detalles del diseño.
fuente
NotResetable
. "podría una cuenta tener más funcionalidades que ser reiniciable" Espero que sí, pero no parece ser importante para la pregunta en cuestión.Responder
Mi respuesta un poco obstinada a su pregunta es:
Apéndice:
Trepador
Veo que no agregaste la
c#
etiqueta, pero estás preguntando en generalobject-oriented
contexto .Lo que está describiendo es un tecnicismo de lenguajes tipados estáticos / fuertes, y no específico de "OO" en general. Su problema surge, entre otros, del hecho de que no puede llamar a métodos en esos idiomas sin tener un tipo suficientemente estrecho en el momento de la compilación (ya sea a través de la definición de variables, la definición de parámetros de métodos / valores de retorno o una conversión explícita). He hecho sus dos variantes en el pasado, en este tipo de idiomas, y ambas me parecen feas en estos días.
En los lenguajes OO de tipo dinámico, este problema no se produce debido a un concepto llamado "tipeo de pato". En resumen, esto significa que básicamente no declaras el tipo de un objeto en ningún lugar (excepto al crearlo). Envía mensajes a objetos, y los objetos manejan esos mensajes. No le importa si el objeto realmente tiene un método con ese nombre. Algunos idiomas incluso tienen un
method_missing
método genérico que lo hace aún más flexible.Entonces, en dicho lenguaje (Ruby, por ejemplo), su código se convierte en:
Período.
O
account
bien restablecerá la contraseña, lanzará una excepción "denegada" o lanzará una excepción "no sé cómo manejar 'reset_password'".Si necesita ramificarse explícitamente por cualquier razón, aún lo haría en la capacidad, no en la clase:
(Dónde
can?
es solo un método que verifica si el objeto puede ejecutar algún método, sin llamarlo).Tenga en cuenta que si bien mi preferencia personal está actualmente en idiomas escritos dinámicamente, esta es solo una preferencia personal. Los idiomas con escritura estática también tienen algo para ellos, así que por favor no tome esta divagación como una bofetada contra los idiomas con escritura estática ...
fuente
Parece que sus opciones se derivan de diferentes fuerzas de motivación. El primero parece ser un truco empleado debido al modelado deficiente de los objetos. Demostró que tus abstracciones están mal, ya que rompen el polimorfismo. Lo más probable es que se rompa El principio cerrado abierto , lo que resulta en un acoplamiento más estricto.
Su segunda opción podría estar bien desde la perspectiva OOP, pero la conversión de tipos me confunde. Si su cuenta le permite restablecer su contraseña, ¿por qué necesita una conversión de texto? Asumiendo que su
CanResetPassword
cláusula representa algún requisito comercial fundamental que forma parte de la abstracción de la cuenta, su segunda opción está bien.fuente