Remplacer le conditionnel par le polymorphisme de manière appropriée?

10

Considérez deux classes Doget les Catdeux conformes au Animalprotocole (en termes de langage de programmation Swift. Ce serait l'interface en Java / C #).

Nous avons un écran affichant une liste mixte de chiens et de chats. Il y a une Interactorclasse qui gère la logique dans les coulisses.

Maintenant, nous voulons présenter une alerte de confirmation à l'utilisateur lorsqu'il souhaite supprimer un chat. Cependant, les chiens doivent être supprimés immédiatement sans aucune alerte. La méthode avec des conditions ressemblerait à ceci:

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

Comment refactoriser ce code? Ça sent évidemment

Andrey Gordeev
la source

Réponses:

9

Vous laissez le type de protocole lui-même déterminer le comportement. Vous souhaitez traiter tous les protocoles de la même manière dans votre programme, sauf dans la classe d'implémentation elle-même. Le faire de cette façon, c'est respecter le principe de substitution de Liskov, qui dit que vous devriez pouvoir passer soit Catou Dog(ou tout autre protocole que vous pourriez éventuellement avoir éventuellement sous Animal), et le faire fonctionner indifféremment.

Il est donc probable que vous ajouteriez une isCriticalfonction à Animalimplémenter à la fois par Doget Cat. Tout ce qui implémenterait Dogretournerait faux et tout ce qui implémenterait Catretournerait vrai.

À ce stade, vous n'auriez qu'à le faire (Mes excuses si la syntaxe n'est pas correcte. Pas un utilisateur de Swift):

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

Il y a seulement un petit problème avec cela, et que Doget Catsont des protocoles, ce qui signifie qu'ils en eux - mêmes ne déterminent pas les isCriticalrendements, laissant cela à toutes les classes d' exécution de décider eux - mêmes. Si vous avez beaucoup d'implémentations, cela vaudrait probablement la peine de créer une classe extensible de Catou Dogqui implémente déjà correctement isCriticalet de supprimer efficacement toutes les classes d'implémentation de la nécessité de remplacer isCritical.

Si cela ne répond pas à votre question, veuillez écrire dans les commentaires et je développerai ma réponse en conséquence!

Neil
la source
Il est un peu clair dans l'énoncé de la question, mais Doget Catsont décrites comme des classes, tout Animalest un protocole qui est mis en œuvre par chacune de ces classes. Il y a donc un peu de décalage entre la question et votre réponse.
Caleb
Vous proposez donc de laisser le modèle décider de présenter ou non une fenêtre de confirmation? Mais que se passe-t-il si une logique lourde est impliquée, comme afficher la popup uniquement si 10 chats sont affichés? La logique dépend de l' Interactorétat maintenant
Andrey Gordeev
Ouais, désolé pour la question peu claire, j'ai fait quelques modifications. Ça devrait être plus clair maintenant
Andrey Gordeev
1
Ce type de comportement ne doit pas être lié au modèle. Cela dépend du contexte et non de l'entité elle-même. Je pense que le chat et le chien sont plus susceptibles d'être POJO. Les comportements doivent être manipulés à d'autres endroits et pouvoir changer en fonction du contexte. Déléguer des comportements ou des méthodes sur lesquels les comportements s'appuieront chez Chat ou Chien entraînera trop de responsabilités dans ces classes.
Grégory Elhaimer
@ GrégoryElhaimer Veuillez noter que ce n'est pas un comportement déterminant. Il s'agit simplement d'indiquer s'il s'agit ou non d'une classe critique. Les comportements tout au long du programme qui doivent savoir s'il s'agit d'une classe critique peuvent ensuite être évalués et agir en conséquence. S'il s'agit bien d'une propriété qui différencie la manière dont les instances sont gérées Catet Dogtraitées, elle peut et doit être une propriété commune dans Animal. Faire autre chose, c'est demander un mal de tête de maintenance plus tard.
Neil
4

Tell vs. Ask

L'approche conditionnelle que vous montrez que nous appellerions « demander ». C'est là que le client consommateur demande "quel genre êtes-vous?" et personnalise leur comportement et leur interaction avec les objets en conséquence.

Cela contraste avec l'alternative que nous appelons " dire ». En utilisant tell , vous poussez plus de travail dans les implémentations polymorphes, de sorte que le code client consommateur est plus simple, sans conditions et commun quelles que soient les implémentations possibles.

Puisque vous souhaitez utiliser une alerte de confirmation, vous pouvez en faire une capacité explicite de l'interface. Ainsi, vous pouvez avoir une méthode booléenne qui vérifie éventuellement avec l'utilisateur et renvoie la confirmation booléenne. Dans les classes qui ne veulent pas confirmer, elles remplacent simplement return true;. D'autres implémentations peuvent déterminer dynamiquement si elles souhaitent utiliser la confirmation.

Le client consommateur utiliserait toujours la méthode de confirmation quelle que soit la sous-classe particulière avec laquelle il travaille, ce qui fait que l'interaction indique au lieu de demander .

(Une autre approche consisterait à pousser la confirmation dans la suppression, mais cela surprendrait les clients consommateurs qui s'attendent à ce qu'une opération de suppression réussisse.)

Erik Eidt
la source
Vous proposez donc de laisser le modèle décider de présenter ou non une fenêtre de confirmation? Mais que se passe-t-il si une logique lourde est impliquée, comme afficher la popup uniquement si 10 chats sont affichés? La logique dépend de l' Interactorétat maintenant
Andrey Gordeev
2
Ok, oui, c'est une question différente, nécessitant une réponse différente.
Erik Eidt
2

Déterminer si une confirmation est nécessaire est la responsabilité de la Catclasse, alors permettez-lui d'exécuter cette action. Je ne connais pas Kotlin, donc j'exprimerai les choses en C #. Espérons que les idées soient ensuite transférables à Kotlin aussi.

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;
}

Ensuite, lors de la création d'une Catinstance, vous la fournissez TellSceneToShowConfirmationAlert, qui devra être renvoyée truesi OK pour la supprimer:

var model = new Cat(TellSceneToShowConfirmationAlert);

Et puis votre fonction devient:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}
David Arno
la source
1
Cela ne déplace-t-il pas la logique de suppression dans le modèle? Ne serait-il pas préférable d'utiliser un autre objet pour gérer cela? Peut-être une structure de données comme un Dictionary <Cat> à l'intérieur d'un ApplicationService; vérifier si le chat existe et s'il le fait, puis déclencher l'alerte de confirmation?
keelerjr12
@ keelerjr12, il déplace la responsabilité de déterminer si une confirmation est nécessaire pour la suppression dans la Catclasse. Je dirais que c'est à cela qu'il appartient. Il ne parvient pas à décider comment cette confirmation est obtenue (qui est injectée) et il ne se supprime pas lui-même. Donc non, cela ne déplace pas la logique de suppression dans le modèle.
David Arno
2
Je pense que cette approche entraînerait des tonnes et des tonnes de code lié à l'interface utilisateur attachés à la classe elle-même. Si la classe est destinée à être utilisée sur plusieurs couches d'interface utilisateur, le problème s'aggrave. Cependant, s'il s'agit d'une classe de type ViewModel plutôt que d'une entité commerciale, cela semble approprié.
Graham
@Graham, oui, c'est certainement un risque avec cette approche: elle repose sur le fait qu'elle est facile à injecter TellSceneToShowConfirmationAlertdans une instance de Cat. Dans des situations où ce n'est pas une chose facile (comme dans un système multicouche où cette fonctionnalité se situe à un niveau profond), alors cette approche ne serait pas bonne.
David Arno
1
Exactement ce que je voulais dire. Une entité commerciale par rapport à une classe ViewModel. Dans le domaine professionnel, un chat ne devrait pas connaître le code lié à l'interface utilisateur. Mon chat de famille n'alerte personne. Merci!
keelerjr12
1

Je conseillerais d'aller pour un modèle de visiteur. J'ai fait une petite implémentation en Java. Je ne connais pas Swift, mais vous pouvez l'adapter facilement.

Le visiteur

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

Votre modèle

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();
     }
}

Appeler le visiteur

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

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

Vous pouvez avoir autant d'implémentations d'AnimalVisitor que vous le souhaitez.

Exemple:

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
la source