with type tutorial strategy sourcemaking refactor method introduce guru code refactoring polymorphism conditional

refactoring - type - Reemplazar condicional con polimorfismo-agradable en teoría pero no práctico



refactoring tutorial (9)

"Reemplazar condicional con polimorfismo" es elegante solo cuando el tipo de objeto que está haciendo cambiar / si la declaración ya está seleccionada para usted. Como ejemplo, tengo una aplicación web que lee un parámetro de cadena de consulta llamado "acción". La acción puede tener valores de "ver", "editar", "ordenar", etc. Entonces, ¿cómo implemento esto con el polimorfismo? Bueno, puedo crear una clase abstracta llamada BaseAction y derivar ViewAction, EditAction y SortAction a partir de ella. ¿Pero no necesito un condicional para decidir qué tipo de tipo BaseAction para crear una instancia? No veo cómo se pueden reemplazar completamente los condicionales con polimorfismo. En todo caso, los condicionales están siendo empujados a la parte superior de la cadena.

EDITAR:

public abstract class BaseAction { public abstract void doSomething(); } public class ViewAction : BaseAction { public override void doSomething() { // perform a view action here... } } public class EditAction : BaseAction { public override void doSomething() { // perform an edit action here... } } public class SortAction : BaseAction { public override void doSomething() { // perform a sort action here... } } string action = "view"; // suppose user can pass either "view", "edit", or "sort" strings to you. BaseAction theAction = null; switch (action) { case "view": theAction = new ViewAction(); break; case "edit": theAction = new EditAction(); break; case "sort": theAction = new SortAction(); break; } theAction.doSomething(); // So I don''t need conditionals here, but I still need it to decide which BaseAction type to instantiate first. There''s no way to completely get rid of the conditionals.


A pesar de que la última respuesta fue hace un año, me gustaría hacer algunas revisiones / comentarios sobre este tema.

Respuestas Revisión

Estoy de acuerdo con @CarlManaster sobre la codificación de la instrucción de switch una vez para evitar todos los problemas conocidos de tratar con el código duplicado, en este caso que involucran condicionales (algunos de ellos mencionados por @thkala).

No creo que el enfoque propuesto por @ KonradSzałwiński o @AlexanderKogtenkov se ajuste a este escenario por dos razones:

En primer lugar, a partir del problema que ha descrito, no es necesario cambiar dinámicamente la asignación entre el nombre de una acción y la instancia de una acción que la maneja.

Tenga en cuenta que estas soluciones permiten hacerlo (simplemente asignando un nombre de acción a una nueva instancia de acción), mientras que la solución basada en conmutadores estáticos no (las asignaciones están codificadas). Además, todavía necesitará un condicional para verificar si una clave determinada está definida en la tabla de asignación, si no se debe realizar una acción (la parte default de una declaración de cambio).

Segundo, en este ejemplo particular, los dictionaries son implementaciones realmente ocultas de la instrucción switch. Aún más, podría ser más fácil leer / entender la instrucción de cambio con la cláusula predeterminada que tener que ejecutar mentalmente el código que devuelve el objeto de manejo de la tabla de mapeo, incluido el manejo de una clave no definida.

Hay una forma de deshacerse de todos los condicionales, incluida la instrucción switch:

Eliminación de la instrucción de cambio (no usar condicionales en absoluto)

¿Cómo crear el objeto de acción correcto a partir del nombre de la acción?

Seré agnóstico del lenguaje, por lo que esta respuesta no es tan larga, pero el truco es darse cuenta de que las clases también son objetos .

Si ya ha definido una jerarquía polimórfica, no tiene sentido hacer referencia a una subclase concreta de BaseAction : ¿por qué no pedirle que devuelva la instancia correcta que maneja una acción por su nombre?

Esto generalmente se implementa con la misma instrucción de cambio que había escrito (por ejemplo, un método de fábrica) ... pero, ¿qué pasa con esto?

public class BaseAction { //I''m using this notation to write a class method public static handlingByName(anActionName) { subclasses = this.concreteSubclasses() handlingClass = subclasses.detect(x => x.handlesByName(anActionName)); return new handlingClass(); } }

Entonces, ¿qué está haciendo ese método?

Primero, recupera todas las subclases concretas de esto (lo que apunta a BaseAction ). En su ejemplo, recuperaría una colección con ViewAction , EditAction y SortAction .

Note que dije subclases concretas, no todas las subclases. Si la jerarquía es más profunda, las subclases concretas siempre serán las que se encuentran en la parte inferior de la jerarquía (hoja). Eso es porque son los únicos que se supone que no son abstractos y proporcionan una implementación real.

Segundo, obtenga la primera subclase que responda si puede o no manejar una acción por su nombre (estoy usando una notación con sabor a lambda / cierre). Una implementación de handlesByName método de la clase ViewAction para ViewAction se vería así:

public static class ViewAction { public static bool handlesByName(anActionName) { return anActionName == ''view'' } }

Tercero, enviamos el mensaje nuevo a la clase que maneja la acción, creando efectivamente una instancia de ella.

Por supuesto, tiene que lidiar con el caso cuando ninguna de las subclases maneja la acción por su nombre. Muchos lenguajes de programación, incluidos Smalltalk y Ruby, permiten pasar al método de detección un segundo lambda / cierre que solo se evaluará si ninguna de las subclases coincide con el criterio. Además, tendrá que lidiar con el caso más de una subclase maneja la acción por su nombre (probablemente, uno de estos métodos fue codificado de manera incorrecta).

Conclusión

Una ventaja de este enfoque es que se pueden BaseAction nuevas acciones escribiendo (y no modificando) el código existente: simplemente cree una nueva subclase de BaseAction e implemente el método de la clase handlesByName correctamente. De manera efectiva, admite la adición de una nueva función al agregar un nuevo concepto, sin modificar la compresión existente. Es claro que, si la nueva característica requiere que se agregue un nuevo método polimórfico a la jerarquía, se necesitarán cambios.

Además, puede proporcionar a los desarrolladores que utilizan los comentarios de su sistema: "La acción proporcionada no se maneja en ninguna subclase de BaseAction, cree una nueva subclase e implemente los métodos abstractos". Para mí, el hecho de que el modelo en sí le diga lo que está mal (en lugar de tratar de ejecutar mentalmente una tabla de consulta) agrega valor e instrucciones claras sobre lo que se debe hacer.

Sí, esto podría sonar sobre diseño. Mantenga una mente abierta y comprenda que si una solución está sobre-diseñada o no tiene que ver, entre otras cosas, con la cultura de desarrollo del lenguaje de programación particular que está utilizando. Por ejemplo, los chicos de .NET probablemente no lo usarán porque .NET no le permite tratar las clases como objetos reales, mientras que, por otro lado, esa solución se usa en las culturas Smalltalk / Ruby.

Finalmente, use el sentido común y el gusto para determinar de antemano si una técnica en particular realmente resuelve su problema antes de usarlo. Es tentador, sí, pero se deben evaluar todas las compensaciones (cultura, antigüedad de los desarrolladores, resistencia al cambio, mentalidad abierta, etc.).


Algunas cosas a considerar:

  • Solo creas una instancia de cada objeto una vez . Una vez que lo haga, no se necesitarán más condicionales con respecto a su tipo.

  • Incluso en casos de una sola vez, ¿de cuántos condicionales se libraría si utilizara subclases? El código que usa condicionales como este es bastante propenso a estar lleno del mismo condicional una y otra vez y otra vez ...

  • ¿Qué sucede cuando necesitas un valor de foo Action en el futuro? ¿Cuántos lugares tendrás que modificar?

  • ¿Qué pasa si necesitas una bar que es ligeramente diferente a foo ? Con las clases, simplemente hereda BarAction de FooAction , anulando lo único que necesita cambiar.

A la larga, el código orientado a objetos es generalmente más fácil de mantener que el código de procedimiento: los gurús no tendrán ningún problema con ninguno de los dos, pero para el resto de nosotros hay una diferencia.


El polimorfismo es un método de unión. Es un caso especial de cosa conocida como "Modelo de Objeto". Los modelos de objetos se utilizan para manipular sistemas complejos, como circuitos o dibujos. Considere algo almacenado / ordenado formato de texto: artículo "A", conectado al artículo "B" y "C". Ahora necesitas saber qué está conectado a A. Un hombre puede decir que no voy a crear un Modelo de Objeto para esto porque puedo contarlo mientras analizo, de una sola pasada. En este caso, puede que tenga razón, puede escapar sin un modelo de objeto. ¿Pero qué sucede si necesita realizar muchas manipulaciones complejas con el diseño importado? ¿Lo manipulará en formato de texto o enviando mensajes invocando métodos java y haciendo referencia a objetos java es más conveniente? Es por eso que se mencionó que necesita hacer la traducción solo una vez.


Hay varias formas de traducir una cadena de entrada a un objeto de un tipo dado y una condicional es definitivamente una de ellas. Dependiendo del lenguaje de implementación, también podría ser posible utilizar una instrucción de cambio que permita especificar cadenas esperadas como índices y crear o recuperar un objeto del tipo correspondiente. Todavía hay una mejor manera de hacerlo.

Se puede usar una tabla de búsqueda para asignar cadenas de entrada a los objetos requeridos:

action = table.lookup (action_name); // Retrieve an action by its name if (action == null) ... // No matching action is found

El código de inicialización se encargaría de crear los objetos requeridos, por ejemplo

table ["edit"] = new EditAction (); table ["view"] = new ViewAction (); ...

Este es el esquema básico que se puede extender para cubrir más detalles, como argumentos adicionales de los objetos de acción, la normalización de los nombres de acción antes de usarlos para la búsqueda de tablas, reemplazando una tabla con una matriz simple mediante el uso de números enteros en lugar de cadenas para identificar acciones solicitadas, etc.


He estado pensando en este problema probablemente más que el resto de desarrolladores que conocí. La mayoría de ellos desconocen totalmente el costo de mantener largos anidados if-else o cambios de casos. Entiendo totalmente su problema al aplicar una solución llamada "Reemplazar condicional con polimorfismo" en su caso. Notaste con éxito que el polimorfismo funciona siempre que el objeto ya esté seleccionado. También se ha dicho en esta banda de rodadura que este problema se puede reducir a asociación [clave] -> [clase]. Aquí está, por ejemplo, la implementación AS3 de la solución.

private var _mapping:Dictionary; private function map():void { _mapping["view"] = new ViewAction(); _mapping["edit"] = new EditAction(); _mapping["sort"] = new SortAction(); } private function getAction(key:String):BaseAction { return _mapping[key] as BaseAction; }

Corriendo que te gustaría:

public function run(action:String):void { var selectedAction:BaseAction = _mapping[action]; selectedAction.apply(); }

En ActionScript3 hay una función global llamada getDefinitionByName (key: String): Class. La idea es utilizar sus valores clave para que coincidan con los nombres de las clases que representan la solución a su condición. En su caso, tendría que cambiar "ver" a "ViewAction", "editar" a "EditAction" y "ordenar" a "SortAtion". No es necesario memorizar nada utilizando tablas de búsqueda. La función ejecutar se verá así:

public function run(action:Script):void { var class:Class = getDefintionByName(action); var selectedAction:BaseAction = new class(); selectedAction.apply(); }

Desafortunadamente, pierde la comprobación de compilación con esta solución, pero obtiene flexibilidad para agregar nuevas acciones. Si creas una nueva clave, lo único que debes hacer es crear una clase apropiada que la maneje.

Por favor, deje un comentario incluso si no está de acuerdo conmigo.


Puede almacenar la cadena y el tipo de acción correspondiente en algún lugar del mapa hash.

public abstract class BaseAction { public abstract void doSomething(); } public class ViewAction : BaseAction { public override void doSomething() { // perform a view action here... } } public class EditAction : BaseAction { public override void doSomething() { // perform an edit action here... } } public class SortAction : BaseAction { public override void doSomething() { // perform a sort action here... } } string action = "view"; // suppose user can pass either // "view", "edit", or "sort" strings to you. BaseAction theAction = null; theAction = actionMap.get(action); // decide at runtime, no conditions theAction.doSomething();


Su ejemplo no requiere polimorfismo, y puede que no sea aconsejable. Sin embargo, la idea original de reemplazar la lógica condicional con el envío polimórfico es sólida.

Aquí está la diferencia: en su ejemplo, tiene un pequeño conjunto de acciones fijo (y predeterminado). Además, las acciones no están fuertemente relacionadas en el sentido de que las acciones de "ordenación" y "edición" tienen poco en común. El polimorfismo está sobre-diseñando su solución.

Por otro lado, si tiene muchos objetos con un comportamiento especializado para una idea común, el polimorfismo es exactamente lo que desea. Por ejemplo, en un juego puede haber muchos objetos que el jugador puede "activar", pero cada uno responde de manera diferente. Podría implementar esto con condiciones complejas (o más probablemente una declaración de cambio), pero el polimorfismo sería mejor. El polimorfismo le permite introducir nuevos objetos y comportamientos que no formaron parte de su diseño original (pero que encajan dentro de su espíritu).

En su ejemplo, aún sería una buena idea abstraer los objetos que admiten las acciones de visualización / edición / clasificación, pero tal vez no abstraer estas acciones. Aquí hay una prueba: ¿alguna vez querría poner esas acciones en una colección? Probablemente no, pero es posible que tenga una lista de los objetos que los admiten.


Tienes razón: "los condicionales están siendo empujados a la cima de la cadena", pero no hay "solo" al respecto. Es muy poderoso. Como dice @thkala, solo haces la elección una vez ; De ahí en adelante, el objeto sabe cómo hacer su negocio. El enfoque que describe, BaseAction, ViewAction y el resto, es una buena manera de hacerlo. Pruébelo y vea cuánto se limpia su código.

Cuando tienes un método de fábrica que toma una cadena como "Ver" y devuelve una Acción, y lo llamas, has aislado tu condicionalidad. Eso es genial. Y no puedes apreciar adecuadamente el poder hasta que lo hayas probado, ¡así que pruébalo!


public abstract class BaseAction { public abstract void doSomething(); } public class ViewAction : BaseAction { public override void doSomething() { // perform a view action here... } } public class EditAction : BaseAction { public override void doSomething() { // perform an edit action here... } } public class SortAction : BaseAction { public override void doSomething() { // perform a sort action here... } } string action = "view"; // suppose user can pass either // "view", "edit", or "sort" strings to you. BaseAction theAction = null; switch (action) { case "view": theAction = new ViewAction(); break; case "edit": theAction = new EditAction(); break; case "sort": theAction = new SortAction(); break; } theAction.doSomething();

Por lo tanto, no necesito condicionales aquí, pero aún lo necesito para decidir qué tipo de BaseAction se crea primero. No hay manera de deshacerse por completo de los condicionales.