Reemplazar condicional con polimorfismo de una manera adecuada?

10

Considere dos clases Dogy Catambas conforme al Animalprotocolo (en términos del lenguaje de programación Swift. Esa sería la interfaz en Java / C #).

Tenemos una pantalla que muestra una lista mixta de perros y gatos. Hay una Interactorclase que maneja la lógica detrás de escena.

Ahora queremos presentar una alerta de confirmación al usuario cuando quiera eliminar un gato. Sin embargo, los perros deben eliminarse inmediatamente sin ninguna alerta. El método con condicionales se vería así:

func tryToDeleteModel(model: Animal) {
    if let model = model as? Cat {
        tellSceneToShowConfirmationAlert()
    } else if let model = model as? Dog {
        deleteModel(model: model)
    }
}

¿Cómo se puede refactorizar este código? Obviamente huele

Andrey Gordeev
fuente

Respuestas:

9

Estás dejando que el tipo de protocolo mismo determine el comportamiento. Desea tratar todos los protocolos de la misma manera en todo su programa, excepto en la propia clase de implementación. Hacerlo de esta manera respeta el Principio de sustitución de Liskov, que dice que debería ser capaz de aprobar uno Catu otro Dog(o cualquier otro protocolo que eventualmente pueda tener Animal) y que funcione de manera indiferente.

Por lo tanto, presumiblemente agregaría un isCriticalfunc Animalpara ser implementado por ambos Dogy Cat. Todo lo que se implemente Dogdevolvería falso y todo lo que se implementaría Catdevolvería verdadero.

En ese momento, solo tendría que hacer (Mis disculpas si la sintaxis no es correcta. No es un usuario de Swift):

func tryToDeleteModel(model: Animal) {
    if model.isCritical() {
        tellSceneToShowConfirmationAlert()
    } else {
        deleteModel(model: model)
    }
}

Solo hay un pequeño problema con eso, y eso es eso Dogy Catson protocolos, lo que significa que en sí mismos no determinan qué isCriticalretornos, dejando que cada clase de implementación decida por sí misma. Si tiene muchas implementaciones, probablemente valga la pena crear una clase extensible Cato Dogque ya se implemente correctamente isCriticaly elimine de manera efectiva todas las clases de implementación de la necesidad de anular isCritical.

Si esto no responde a su pregunta, escriba los comentarios y expandiré mi respuesta en consecuencia.

Neil
fuente
Es un poco confuso en el estado de la cuestión, sino Dog, y Catse describen como clases, mientras que Animales un protocolo que se implementa por cada una de esas clases. Así que hay un poco de desajuste entre la pregunta y su respuesta.
Caleb
¿Entonces sugiere que el modelo decida si presentará una ventana emergente de confirmación o no? Pero, ¿qué pasa si hay una lógica pesada involucrada, como mostrar ventana emergente solo si se muestran 10 gatos? La lógica depende del Interactorestado ahora
Andrey Gordeev
Sí, perdón por la pregunta poco clara, he hecho algunas ediciones. Debería ser más claro ahora
Andrey Gordeev
1
Este tipo de comportamiento no debe estar vinculado al modelo. Depende del contexto y no de la entidad misma. Creo que Cat y Dog tienen más probabilidades de ser POJO. Los comportamientos deben manejarse en otros lugares y ser capaces de cambiar según el contexto. Delegar comportamientos o métodos en los que se basarán los comportamientos en Cat o Dog dará lugar a demasiadas responsabilidades en tales clases.
Grégory Elhaimer
@ GrégoryElhaimer Tenga en cuenta que no es un comportamiento determinante. Simplemente indica si es o no una clase crítica. Los comportamientos a lo largo del programa que necesitan saber si es una clase crítica pueden evaluar y actuar en consecuencia. Si esto es de hecho una característica que diferencia a la forma en casos en tanto Caty Dogse manejan, que puede y debe ser una característica común en Animal. Hacer cualquier otra cosa es pedir un dolor de cabeza de mantenimiento más tarde.
Neil
4

Tell vs. Ask

El enfoque condicional que está mostrando lo llamaremos " preguntar ". Aquí es donde el cliente consumidor pregunta "¿de qué tipo eres?" y personaliza su comportamiento e interacción con los objetos en consecuencia.

Esto contrasta con la alternativa que llamamos " tell ". Usando tell , llevas más trabajo a las implementaciones polimórficas, de modo que el código del cliente consumidor sea más simple, sin condicionales y común, independientemente de las posibles implementaciones.

Como desea utilizar una alerta de confirmación, puede hacer que sea una capacidad explícita de la interfaz. Por lo tanto, puede tener un método booleano que opcionalmente verifica con el usuario y devuelve la confirmación booleana. En las clases que no quieren confirmar, simplemente anulan con return true;. Otras implementaciones pueden determinar dinámicamente si desean usar la confirmación.

El cliente consumidor siempre usaría el método de confirmación independientemente de la subclase particular con la que esté trabajando, lo que hace que la interacción diga en lugar de preguntar .

(Otro enfoque sería enviar la confirmación a la eliminación, pero eso sorprendería a los clientes consumidores que esperan que una operación de eliminación tenga éxito).

Erik Eidt
fuente
¿Entonces sugiere que el modelo decida si presentará una ventana emergente de confirmación o no? Pero, ¿qué pasa si hay una lógica pesada involucrada, como mostrar ventana emergente solo si se muestran 10 gatos? La lógica depende del Interactorestado ahora
Andrey Gordeev
2
Ok, sí, esa es una pregunta diferente, que requiere una respuesta diferente.
Erik Eidt
2

Determinar si se necesita una confirmación es responsabilidad de la Catclase, así que habilítelo para realizar esa acción. No conozco a Kotlin, así que expresaré las cosas en C #. Esperemos que las ideas sean transferibles a Kotlin también.

interface Animal
{
    bool IsOkToDelete();
}

class Cat : Animal
{
    private readonly Func<bool> _confirmation;

    public Cat (Func<bool> confirmation) => _confirmation = confirmation;

    public bool IsOkToDelete() => _confirmation();
}

class Dog : Animal
{
    public bool IsOkToDelete() => true;
}

Luego, al crear una Catinstancia, debe proporcionarla TellSceneToShowConfirmationAlert, que deberá devolver truesi está bien para eliminar:

var model = new Cat(TellSceneToShowConfirmationAlert);

Y luego tu función se convierte en:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}
David Arno
fuente
1
¿No mueve esto la lógica de eliminación al modelo? ¿No sería mucho mejor usar otro objeto para manejar esto? Posiblemente una estructura de datos como un Dictionary <Cat> dentro de un ApplicationService; verifica si Cat existe y si es así, ¿dispara la alerta de confirmación?
keelerjr12
@ keelerjr12, mueve la responsabilidad de determinar si se necesita una confirmación para la eliminación en la Catclase. Yo diría que ahí es donde pertenece. No puede decidir cómo se logra esa confirmación (que se inyecta) y no se elimina a sí misma. Entonces no, no mueve la lógica de eliminación al modelo.
David Arno
2
Siento que este enfoque llevaría a toneladas y toneladas de código relacionado con la interfaz de usuario adjunto a la clase misma. Si la clase está destinada a ser utilizada en múltiples capas de IU, el problema aumenta. Sin embargo, si se trata de una clase de tipo ViewModel, en lugar de una entidad comercial, entonces parece apropiado.
Graham el
@Graham, sí, definitivamente es un riesgo con este enfoque: se basa en que es fácil de inyectar TellSceneToShowConfirmationAlerten una instancia de Cat. En situaciones donde eso no es fácil (como en un sistema de varias capas donde esta funcionalidad se encuentra en un nivel profundo), entonces este enfoque no sería bueno.
David Arno
1
Exactamente a lo que me refería. Una entidad comercial frente a una clase ViewModel. En el dominio empresarial, un Cat no debe saber sobre el código relacionado con la interfaz de usuario. El gato de mi familia no alerta a nadie. ¡Gracias!
keelerjr12
1

Yo recomendaría ir por un patrón de visitante. Hice una pequeña implementación en Java. No estoy familiarizado con Swift, pero puedes adaptarlo fácilmente.

El visitante

public interface AnimalVisitor<R>{
    R visitCat();
    R visitDog();
}

Su modelo

abstract class Animal { // can also be an interface like VisitableAnimal
    abstract <R> R accept(AnimalVisitor<R> visitor);
}

class Cat extends Animal {
    public <R> R accept(AnimalVisitor<R> visitor) {
         return visitor.visitCat();
     }
}

class Dog extends Animal {
    public <R> R accept(AnimalVisitor<R> visitor) {
         return visitor.visitDog();
     }
}

Llamando al visitante

public void tryToDelete(Animal animal) {
    animal.accept( new AnimalVisitor<Void>() {
        public Void visitCat() {
            tellSceneToShowConfirmation();
            return null;
        }

        public Void visitDog() {
            deleteModel(animal);
            return null;
        }
    });
}

Puede tener tantas implementaciones de AnimalVisitor como desee.

Ejemplo:

public void isColorValid(Color color) {
    animal.accept( new AnimalVisitor<Boolean>() {
        public Boolean visitCat() {
            return Color.BLUE.equals(color);
        }

        public Boolean visitDog() {
            return true;
        }
    });
}
Grégory Elhaimer
fuente