Una consulta, cuando usamos interfaces puede pasar que una de las clases que va a usar esa interfaz necesita para realizar la operación que implica algún método común un parámetro adicional. En este caso incluimos aquel parámetro en la interfaz, a pesar que no sea necesario para el resto de las clases que la implementan? Esta es la única manera en que se me ocurre resolver esta situación, ya que si utilizara sobrecarga para definir distintos métodos con el mismo nombre pero distinta cantidad de parámetros, tendría que preguntar en algún punto para determinar cual de ambos métodos usar, y con ello violaría el principio "tell don't ask".
En alguna medida relacionado con esto, quisiera preguntar, que pasa cuando incluyo métodos en una interfaz que solo algunas clases emplearán realmente. Que pasa con las otras clases que SI implementan la interfaz, pero en ningún momento llegan a necesitar estos métodos, simplemente porque no es parte de su lógica? Las distintas clases deben implementar la misma interfaz necesariamente para cumplir con un diseño polimórfico y de POO, pero no todas las clases necesitaran usar la totalidad de los métodos definidos en la interfaz. Como estos métodos no pueden quedar vacíos en estas clases (¿o si pueden?), pienso que una opción es crear excepciones para las clases que deben llevar las firmas de estos métodos pero no necesitan implementarlos. Sería esto correcto?
Buenas Esmir!
Vayamos por partes:
"cuando usamos interfaces puede pasar que una de las clases que va a usar
esa interfaz necesita para realizar la operación que implica algún
método común un parámetro adicional. En este caso incluimos aquel
parámetro en la interfaz, a pesar que no sea necesario para el resto de
las clases que la implementan?"
- Esto es raro, si tenes algún ejemplo sos bienvenido a dejarlo y lo vemos particularmente. Uno cuando define una interfaz, justamente esta definiendo un abstracción que engloba a los demás clases particulares, el hecho de que necesites un parámetro adicional es un "smell" de que lo anterior no se estaría cumpliendo, ya que este parámetro adicional engloba una responsabilidad que diferencia a las demás entidades que necesitan este parámetro adicional de las que no, estarías violando el principio de substitución de Liskov en este caso. Quiza lo mas recomendable en este caso es reconocer la diferencia y plantear otro tipo de abstracción.
"Esta es la única manera en que se me ocurre resolver esta situación, ya
que si utilizara sobrecarga para definir distintos métodos con el mismo
nombre pero distinta cantidad de parámetros, tendría que preguntar en
algún punto para determinar cual de ambos métodos usar, y con ello
violaría el principio 'tell don't ask'."
- Estas en lo cierto, hacer eso efectivamente viola 'tell don't ask'
"En alguna medida relacionado con esto, quisiera preguntar, que pasa cuando incluyo métodos en una interfaz que solo algunas clases emplearán realmente. Que pasa con las otras clases que SI implementan la interfaz, pero en ningún momento llegan a necesitar estos métodos, simplemente porque no es parte de su lógica? Las distintas clases deben implementar la misma interfaz necesariamente para cumplir con un diseño polimórfico y de POO, pero no todas las clases necesitaran usar la totalidad de los métodos definidos en la interfaz. Como estos métodos no pueden quedar vacíos en estas clases (¿o si pueden?), pienso que una opción es crear excepciones para las clases que deben llevar las firmas de estos métodos pero no necesitan implementarlos. Sería esto correcto?"
- Similar respuesta que lo primero, hacer lo ultimo o no hacerlo viola el principio de substitución de Liskov un buen ejemplo es el del patito de goma, en donde no solo se pierde la generalidad que establece la abstracción de ave, sino que se penaliza al usuario por enviar un mensaje valido. Te dejo con la reflexión de la lectura que te linkeé. "Si se ve como un Pato, hace 'quack' como un Pato pero necesita Pilas entonces probablemente tengas la abstracción equivocada"
Yo tengo una duda similar, o un ejemplo se podría decir, (seria respecto de una interfaz que contiene un parámetro que no necesariamente todos los métodos de las clases hijas necesiten). El ejemplo seria el siguiente: Tenes un Personaje que puede hacer una acción (existen múltiples acciones que el personaje podría hacer) y el Personaje también puede tener un Buffer (hace que las acciones se hagan de una mejor forma) Pero solo ciertas acciones son afectadas por el Buffer algunas no. Entonces mi idea es que las acciones sean una interfaz, que entienda el método accionar y que reciba el buffer. La cosa estaría que no todas las acciones al llamar accionar le dan uso al buffer, esto me parece en parte mal y en parte bien, por que por un lado se me hace raro darle un parámetro el cual no usa, pero por otro lado el que no use el buffer a pesar de tenerlo es parte del comportamiento de la acción entonces no me parece tan mal. Ahí es donde estaría mi duda que es lo correcto/incorrecto de esto o si debería buscar otra abstracción para el caso. Desde ya gracias!
Buenas Lucas!
Lo que entendí de tu modelo es lo siguiente (pido disculpas si lo entendí mal):
Por supuesto que esto esta bien y como bien decís vos, el ignorar el buffer o no es parte del comportamiento de las acciones en particular, ademas de que en este caso las clases que implementan la interfaz cumplen con todo el contrato y no solo con una parte como en el problema de Esmir. Ademas si el día de mañana no van a existir distintos tipos de Buffer que afectan o no a distintos tipos de habilidades no habría problema en dejarlo así, los "code smell" suelen ser cosas como lo anterior quizá no traen un problema o quizá si.
Sin embargo si esto lo llevamos a un ejemplo donde ignorar parámetros nos traiga un problema, nos damos cuenta de que pasaría si uno tuviese mas acciones y estas pudiesen o no aplicar distintos modificadores, por ejemplo si tuviéramos un modificador por VelocidadDeAtaque o si tuviésemos otro modificador por DañoMagico, etc etc. es fácil ver que el personaje empezaría a tener mas estado que solo un simple "buffer" y tendría atributos como rapidezDeAtaque o aptitudMagica, etc. Ademas de que ahora las acciones no recibirían solo un buffer cuando se llame a "realizar" sino que seria algo del estilo: "+realizarCon(unBuffer, unaRapidezDeAtaque, unaAptitudMagica, etc)" donde dependiendo de las acciones utilizaran alguno o ninguno de estos parámetros.
Esto hace que se termine violando el open-close tanto para el Personaje como para las Acciones ya que si se desea agregar una nueva regla de negocio con respecto a un nuevo modificador, se deba modificar todo lo anterior. Es por ello que una mejor solución quizá termine siendo en reconocer esta diferencia entre acciones. Por ejemplo de las acciones que son "Buffereables" y de las que no, de las que son "IdoneasParaMagia" y de las que no, y las que son mejorables debido a la velocidad y las que no. Una solución posible es la siguiente:
En este caso el Personaje es el que lleva sus acciones y el que guarda o no su buffer es el PuñoDeFuego, de esta manera si queremos agregar un modificador que afecta a habilidades de magia de forma especifica, seria crear esta nueva propiedad y hacer que la habilidad "BolaDeFuego", por ejemplo, implementara la interfaz "AptaParaMagia" funcionando de forma similar que la interfaz "Buffereable".
En conclusión, siempre hay que saber reconocer cosas raras del codigo como esto de ignorar un parámetro, pero saber justificar si para el caso donde se esta trabajando resulta un problema grave o algo ignorable.
Saludos!
Hola Santiago, primero que todo muchísimas gracias por contestar de forma tan dedicada y completa. Antes había hecho una consulta y no me habían contestado, por lo que tenía pocas esperanzas con esta consulta en realidad. Muchas Gracias.
Respecto al primer punto, un ejemplo que podría darte sería a partir de un ejercicio de Dragon Ball, en el cual Goku podia usar distintos tipos de Kaioh Ken. En este caso yo defininí una interfaz "KaiohKen", que podían implementar las clases "SinKaiohKen", "KaiohKenNormal", KaiohKenX3" y "KaiohKenX4". Asi, Goku tendría un estado KaiohKen, y cuando se le enviase a Goku el mensaje "atacar", le delegaría la responsabilidad a su estado "KaiohKen" para que este supiera que hacer, segun del tipo de KaiohKen que se tratase. El problema acá era que solo uno de los KaiohKen, KaiohKenX4, consumia la vida del mismo Goku al momento de atacar a su oponente, por ende, lo que se me ocurrió fué agregar al metodo atacar de la interfaz "KaiohKen" el parametro "this", es decir, se pasaba el propio Goku (un objeto de la clase Goku) por parametro, para que unicamente "KaiohKenX4" lo usara. Las demás clases no hacian nada con el objeto que recibian por parametro, pero tenían que incluirlo en la firma, para cumplir con el formato de la interfaz.
El segundo caso, fue menos comun que el primero, pero voy a dar un ejemplo de este mismo ejercicio, para mantener el ambito de la imaginación en este caso. Había en él 3 tipos de ataques, "bolaDeEnergia" comunes a Vegeta y Goku, "kameHameHa" propio de Goku y "galickHo" propio de Vegeta. Para hacer el programa flexible a otros guerreros, hice que Goku y Vegeta implementaran la interfaz "Guerrero" y asi el metodo atacar de Guerrero, quedaría genérico, incluyendo por parametro un ataque, y un objeto Guerrero, el oponente. El tema aqui es que, el ataque kameHameHa, generaba distinto daño de acuerdo a si Goku tenia un estado KaiohKen de una u otra clase, por lo cual, inclui en la interfaz "Ataque" metodos como, atacarConKaiohKenX3, que unicamente utilizarian objetos de tipo "KameHameHa", pues para los otros, ese mensaje no tendría sentido, sin embargo, para poder emplear el polimorfismo en los metodos atacar(Guerrero oponente, Ataque ataque), se me hizo necesario hacer esto. En el caso de "GalickHo" por ejemplo, en la implementacion de estos metodos de ataque con KiohKen, lanzaba una excepcion, y no me parecio tan raro, porque de todas formas eran metodos que se empleaban de manera interna en el programa, no a nivel de usuario, por lo cual sería una buena forma, pensé, de indicarle a un programador que esos metodos no estaban pensados para otro tipo de ataques.
Espero haber sido claro, y nuevamente muchas gracias.
Saludos!