Sostituisci condizionale con polimorfismo in modo corretto?

10

Considera due classi Dog e Cat entrambe conformi al protocollo Animal (in termini di linguaggio di programmazione Swift. Sarebbe un'interfaccia in Java / C #).

Abbiamo uno schermo che mostra una lista mista di cani e gatti. Esiste la classe Interactor che gestisce la logica dietro le scene.

Ora vogliamo presentare un avviso di conferma all'utente quando vuole eliminare un gatto. Tuttavia, i cani devono essere eliminati immediatamente senza avvisi. Il metodo con i condizionali sarebbe simile a questo:

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

Come può essere refactato questo codice? Ovviamente ha un odore

    
posta Andrey Gordeev 03.10.2018 - 08:02
fonte

4 risposte

9

Stai permettendo al tipo di protocollo stesso di determinare il comportamento. Si desidera trattare tutti i protocolli allo stesso modo nel proprio programma tranne nella classe di implementazione stessa. Facendolo in questo modo rispetti il principio di sostituzione di Liskov, che dice che dovresti essere in grado di passare Cat o Dog (o qualsiasi altro protocollo che potresti eventualmente contenere in Animal potenzialmente), e farlo funzionare indifferentemente.

Quindi presumibilmente aggiungerebbe un isCritical func a Animal per essere implementato sia da Dog sia da Cat . Qualsiasi implementazione di Dog restituirebbe false e qualsiasi implementazione di Cat restituirebbe true.

A quel punto, avresti solo bisogno di fare (Le mie scuse se la sintassi non è corretta. Non un utente di Swift):

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

C'è solo un piccolo problema con questo, e cioè che Dog e Cat sono protocolli, il che significa che di per sé non determinano cosa restituisce isCritical , lasciando questo a ogni classe di implementazione per decidere autonomamente . Se disponi di molte implementazioni, probabilmente varrebbe la pena di creare una classe estendibile di Cat o Dog che già implementa correttamente isCritical e che cancella in modo efficace tutte le classi di implementazione dalla necessità di sovrascrivere isCritical .

Se questo non risponde alla tua domanda, ti preghiamo di scrivere nei commenti e amplierò la mia risposta di conseguenza!

    
risposta data 03.10.2018 - 08:22
fonte
4

Tell vs. Ask

L'approccio condizionale che stai visualizzando chiameremmo " chiedi ". Qui è dove il client che consuma chiede "che tipo sei?" e personalizza il loro comportamento e interazione con gli oggetti di conseguenza.

Questo contrasta con l'alternativa che chiamiamo " tell ". Usando tell , si spinge più del lavoro nelle implementazioni polimorfiche, in modo che il codice client che consuma sia più semplice, senza condizionali e comune indipendentemente dalle possibili implementazioni.

Poiché si desidera utilizzare un avviso di conferma, è possibile rendere esplicita tale funzionalità dell'interfaccia. Quindi, potresti avere un metodo booleano che opzionalmente controlla con l'utente e restituisce il booleano di conferma. Nelle classi che non si desidera confermare, eseguono semplicemente l'override con return true; . Altre implementazioni potrebbero determinare dinamicamente se vogliono usare la conferma.

Il client consumatore usa sempre il metodo di conferma indipendentemente dalla particolare sottoclasse con cui sta lavorando, il che rende l'interazione tell invece di ask .

(Un altro approccio sarebbe quello di spingere la conferma nella cancellazione, ma ciò potrebbe sorprendere i consumatori che si aspettano che un'operazione di cancellazione abbia successo.)

    
risposta data 03.10.2018 - 17:32
fonte
2

Determinare se è necessaria una conferma è responsabilità della classe Cat , quindi consentirgli di eseguire quell'azione. Non conosco Kotlin, quindi esprimerò le cose in C #. Speriamo che le idee siano trasferibili anche a Kotlin.

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

Quindi, quando crei un'istanza Cat , la fornisci con TellSceneToShowConfirmationAlert , che dovrà restituire true se OK da eliminare:

var model = new Cat(TellSceneToShowConfirmationAlert);

E poi la tua funzione diventa:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}
    
risposta data 03.10.2018 - 10:20
fonte
1

Consiglierei di andare su un pattern Visitor. Ho fatto una piccola implementazione in Java. Non ho familiarità con Swift, ma puoi adattarlo facilmente.

Il visitatore

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

Il tuo modello

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

Chiamare il visitatore

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

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

Puoi avere tutte le implementazioni di AnimalVisitor che desideri.

Esempio:

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

        public Boolean visitDog() {
            return true;
        }
    });
}
    
risposta data 04.10.2018 - 13:50
fonte