¿Deben identificarse las capacidades de un objeto exclusivamente por las interfaces que implementa?

36

El isoperador de C # y el instanceofoperador 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.

TheCatWhisperer
fuente
10
Mira tus dos ejemplos de código. ¿Cuál es más legible?
Robert Harvey
39
Solo es mi opinión, pero elegiría el primero solo porque se asegura de que puedas elegir el tipo adecuado. El segundo supone que CanResetPasswordsolo será cierto cuando algo se implemente IResetsPassword. 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).
Becuzz
10
@nocomprende: xkcd.com/927
Robert Harvey
14
El título de la pregunta y el cuerpo de la pregunta hacen preguntas diferentes. Para responder al título: Idealmente, sí. Para dirigirse al cuerpo: si se encuentra comprobando el tipo y emitiendo manualmente, probablemente no esté aprovechando su sistema de tipos correctamente. ¿Puede decirnos más específicamente cómo se encontró en esta situación?
MetaFight
99
Esta pregunta puede parecer simple, pero se vuelve bastante amplia cuando comienzas a pensar en ello. Preguntas que me vienen a la mente: ¿Espera agregar nuevos comportamientos a las cuentas existentes o crear tipos de cuentas con diferentes comportamientos? ¿Todos los tipos de cuenta tienen comportamientos similares o hay grandes diferencias entre ellos? ¿El código de ejemplo conoce todos los tipos de cuentas o solo la interfaz básica de IAccount?
Eufórico el

Respuestas:

7

Con la advertencia de que es mejor evitar el downcasting si es posible, entonces la primera forma es preferible por dos razones:

  1. estás dejando que el sistema de tipos funcione para ti. En el primer ejemplo, si se isdispara, el downcast definitivamente funcionará. En el segundo formulario, debe asegurarse manualmente de que solo los objetos que implementan IResetsPassworddevuelven verdadero para esa propiedad
  2. clase base frágil Debe agregar una propiedad a la clase / interfaz base para cada interfaz que desee agregar. Esto es engorroso y propenso a errores.

Eso 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á.

Alex
fuente
También prefiero la escritura compositiva. +1 por hacerme ver que este ejemplo en particular no lo está aprovechando correctamente. Sin embargo, yo diría que la tipificación computacional (como la herencia tradicional) es más limitada cuando las capacidades varían ampliamente (a diferencia de cómo se implementan). De ahí el enfoque de interfaz, que resuelve un problema diferente que el tipo de composición o la herencia estándar.
TheCatWhisperer
podría simplificar el cheque de esta manera: (account as IResettable)?.ResetPassword();ovar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Berin Loritsch
89

¿Deben identificarse las capacidades de un objeto exclusivamente por las interfaces que implementa?

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

if (account is IResetsPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

o

if (account.CanResetPassword)
    ((IResetsPassword)account).ResetPassword();
else
    Print("Not allowed to reset password with this account type!");

considerar

account.ResetPassword();

o

account.ResetPassword(authority);

porque accountya 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.

naranja confitada
fuente
54
El polimorfismo solo funciona cuando no sé o no me importa exactamente con qué estoy hablando. Entonces, la forma en que trato esa situación es evitarla cuidadosamente.
candied_orange
77
Si el cliente realmente necesita saber si el intento tuvo éxito, el método podría devolver un valor booleano. 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.
Radiodef
55
@DavidZ Ambos hablamos inglés. Así que puedo decirte que votes este comentario dos veces. ¿Eso significa que eres capaz de hacerlo? ¿Necesito saber si puedes antes de poder decirte que lo hagas?
candied_orange
55
@QPaysTaxes por todo lo que sabes, se pasó un controlador de errores accountcuando 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.
candied_orange
66
@QPaysTaxes Podría manejarse en otros lugares y de diferentes maneras, es superfluo para esta conversación y solo enturbiaría las aguas.
Tom.Bowen89
17

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 accounttenga 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?

account.getPasswordResetter().doAction();
account.getFormSubmitter().doAction(view.getContents());

AccountManager.resetPassword(account, account.getAccessToken());

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 ?

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

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
1
+1 para los olores de diseño, incluso si no usaste la frase.
Jared Smith
¿Qué pasa con los casos en que restablecer contraseña tiene argumentos completamente diferentes según el tipo? ResetPassword (pswd) vs ResetPasswordToRandom ()
TheCatWhisperer
1
@TheCatWhisperer El primer ejemplo es "cambiar contraseña", que es una acción diferente. El segundo ejemplo es lo que describo en esta respuesta.
¿Por qué no account.getPasswordResetter().resetPassword();.
AnoE
1
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).
Voo
1

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 que canResetPassword()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 passwordResettercampo 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.

Danikov
fuente
No todas las cuentas pueden tener una contraseña (de todos modos, desde la perspectiva de este sistema en particular). Por ejemplo, la cuenta puede ser de terceros ... google, facebook, ect. ¡No quiere decir que esta no sea una gran respuesta!
TheCatWhisperer
0

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:

class Foo : IResetsPassword {
    //...
}

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:

class Foo {

    PasswordResetter passwordResetter;

}

Ahora, en tiempo de ejecución, puede verificar si myFoo.passwordResetter != nullantes de realizar esta operación. Si desea desacoplar las cosas aún más (y planea agregar muchas más capacidades), podría:

class Foo {
    //... foo stuff
}

class PasswordResetOperation {
    bool Execute(Foo foo) { ... }
}

class SendMailOperation {
    bool Execute(Foo foo) { ... }
}

//...and you follow this pattern for each new capability...

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:

class BaseAccount {
    //...
}
class GuestAccount : BaseAccount {
    //...
}
class UserAccount : BaseAccount, IMyPasswordReset, IEditPosts {
    //...
}
class AdminAccount : BaseAccount, IPasswordReset, IEditPosts, ISendMail {
    //...
}

//Capabilities

interface IMyPasswordReset {
    bool ResetPassword();
}

interface IPasswordReset {
    bool ResetPassword(UserAccount userAcc);
}

interface IEditPosts {
    bool EditPost(long postId, ...);
}

interface ISendMail {
    bool SendMail(string from, string to, ...);
}

Ahora, intentaré analizar todas las opciones mencionadas:

OP segundo ejemplo:

if (account.CanResetPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

Digamos que este código está recibiendo una clase de cuenta básica (por ejemplo: BaseAccounten 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:

if (account is IResetsPassword)
       ((IResetsPassword)account).ResetPassword();
else
       Print("Not allowed to reset password with this account type!");

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:

account.ResetPassword(authority);

Si este ResetPasswordmétodo se inserta en la BaseAccountclase, también está contaminando la clase base con código inapropiado, como en el segundo ejemplo de OP

La respuesta de Snowman:

AccountManager.resetPassword(otherAccount, adminAccount.getAccessToken());

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 AccountManagerejemplo, 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.

//Template method
class BaseAccountOperation {

    BaseAccount account;

    void Execute() {

        //... some processing

        TryResetPassword();

        //... some processing

        TrySendMail();

        //... some processing
    }

    void TryResetPassword() {
        Print("Not allowed to reset password with this account type!");
    }

    void TrySendMail() {
        Print("Not allowed to reset password with this account type!");
    }
}

class UserAccountOperation : BaseAccountOperation {

    UserAccount userAccount;

    void TryResetPassword() {
        account.ResetPassword(...);
    }

}

class AdminAccountOperation : BaseAccountOperation {

    AdminAccount adminAccount;

    override void TryResetPassword() {
        account.ResetPassword(...);
    }

    void TrySendMail() {
        account.SendMail(...);
    }
}

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 dynamicpalabra 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).

Emerson Cardoso
fuente
1
¿Funcionaría implementar siempre el método "Execute ()" pero a veces no hacer nada? Devuelve un bool, así que supongo que eso significa que lo hizo o no. Eso lo devuelve al cliente: "Traté de restablecer la contraseña, pero no sucedió (por alguna razón), así que ahora debería ..."
44
Tener métodos "canX ()" y "doX ()" es un antipatrón: si una interfaz expone un método "doX ()", será mejor que pueda hacer X incluso si hacer X significa un no-op (por ejemplo, un patrón de objeto nulo ) Debería no corresponderá a los usuarios de una interfaz para participar en el acoplamiento temporal (método invoke A antes de procedimiento B), ya que es muy fácil de conseguir cosas mal y se rompen.
1
Tener interfaces no tiene relación con el uso de la composición sobre la herencia. Simplemente representan una funcionalidad que está disponible. Cómo entra en juego esa funcionalidad es independiente del uso de la interfaz. Lo que ha hecho aquí es usar una implementación ad-hoc de una interfaz sin ninguno de los beneficios.
Miyamoto Akira
Interesante: la respuesta más votada dice básicamente lo que dice mi comentario anterior. Algunos días solo tienes suerte.
@nocomprende: sí, actualicé mi respuesta de acuerdo con varios comentarios. Gracias por los comentarios :)
Emerson Cardoso
0

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):

interface IAccount {
  bool CanResetPassword();

  void ResetPassword();

  // Other Account operations as needed
}

public class Resetable : IAccount {
  public bool CanResetPassword() {
    return true;
  }

  public void ResetPassword() {
    /* RESET PASSWORD */
  }
}

public class NotResetable : IAccount {
  public bool CanResetPassword() {
    return false;
  }

  public void ResetPassword() {
    Print("Not allowed to reset password with this account type!");}
  }

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.

JimmyJames
fuente
No todas las cuentas se pueden restablecer. Además, ¿podría una cuenta tener más funcionalidad que ser reiniciable?
TheCatWhisperer
2
"No todas las cuentas se pueden restablecer". No estoy seguro si esto fue escrito antes de editar. La segunda clase es 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.
JimmyJames
1
Creo que esta solución va en la dirección correcta, ya que proporciona una buena solución dentro de OO. Probablemente cambiaría el nombre del nombre de la interfaz a IPasswordReseter (o algo por el estilo), para segregar las interfaces. IAccount podría declararse como compatible con varias otras interfaces. Eso le daría la opción de tener clases con la implementación que luego está compuesta y utilizada por delegación en los objetos de dominio. @JimmyJames, pequeño problema, en C #, ambas clases deberán especificar que implementan IAccount. De lo contrario, el código parece un poco extraño ;-)
Miyamoto Akira
1
Además, verifique un comentario de Snowman en una de las otras respuestas sobre CanX () y DoX (). Sin embargo, no siempre es un antipatrón, ya que tiene usos (por ejemplo, en el comportamiento de la interfaz de usuario)
Miyamoto Akira
1
Esta respuesta comienza bien: esto es malo POO. Desafortunadamente, su solución es igual de mala porque también subvierte el sistema de tipos pero arroja una excepción. Una buena solución se ve diferente: no tenga métodos no implementados en un tipo. El marco .NET en realidad usa su enfoque para algunos tipos, pero eso fue un error inequívoco.
Konrad Rudolph
0

Responder

Mi respuesta un poco obstinada a su pregunta es:

Las capacidades de un objeto deben identificarse a través de ellos mismos, no mediante la implementación de una interfaz. Sin embargo, un lenguaje estáticamente fuertemente tipado limitará esta posibilidad (por diseño).

Apéndice:

Ramificar en el tipo de objeto es malo.

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_missingmétodo genérico que lo hace aún más flexible.

Entonces, en dicho lenguaje (Ruby, por ejemplo), su código se convierte en:

account.reset_password

Período.

O accountbien 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:

account.reset_password  if account.can? :reset_password

(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 ...

AnoE
fuente
¡Genial tener tu perspectiva aquí! En el lado de Java / C # tendemos a olvidar que existe una rama diferente de OOP. Cuando les digo a mis colegas, JS (por todos sus problemas) es, en muchos sentidos, más OO que C #, ¡se ríen de mí!
TheCatWhisperer
Me encanta C #, y creo que es un buen lenguaje de propósito general , pero es mi opinión personal, que en términos de OO, es bastante pobre. Tanto en las características como en la práctica, tiende a fomentar la programación de procedimientos.
TheCatWhisperer
2
Podría decirse que probar la existencia de un método en un lenguaje de tipo pato es lo mismo que probar la implementación de una interfaz en un lenguaje de tipo estático: está realizando una verificación en tiempo de ejecución de si el objeto tiene una capacidad particular. Cada declaración de método es efectivamente su propia interfaz, identificada solo por el nombre del método, y su existencia no puede usarse para inferir ninguna otra información sobre el objeto.
IMSoP
0

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 CanResetPasswordcláusula representa algún requisito comercial fundamental que forma parte de la abstracción de la cuenta, su segunda opción está bien.

Zapadlo
fuente