Sostituire il condizionale con il polimorfismo in modo corretto?


10

Considera due classi Doged Catentrambe conformi al Animalprotocollo (in termini di linguaggio di programmazione Swift. Sarebbe un'interfaccia in Java / C #).

Abbiamo uno schermo che mostra un elenco misto di cani e gatti. C'è una Interactorclasse che gestisce la logica dietro le quinte.

Ora vogliamo presentare un avviso di conferma all'utente quando desidera eliminare un gatto. Tuttavia, i cani devono essere eliminati immediatamente senza alcun avviso. Il metodo con i condizionali sarebbe simile al seguente:

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

Come può essere refactored questo codice? Ovviamente odora

Risposte:


9

Stai lasciando che il tipo di protocollo stesso determini il comportamento. Volete trattare tutti i protocolli allo stesso modo in tutto il programma, tranne nella classe di implementazione stessa. Farlo in questo modo è rispettare il Principio di sostituzione di Liskov, che dice che dovresti essere in grado di passare uno Cato Dog(o qualsiasi altro protocollo che potresti eventualmente avere sotto Animalpotenzialmente), e farlo funzionare indifferentemente.

Quindi presumibilmente aggiungeresti una isCriticalfunzione Animalper essere implementata da entrambi Doge Cat. Qualsiasi implementazione Dogsarebbe restituita falsa e qualsiasi implementazione Catsarebbe vera.

A quel punto, dovrai solo 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è quello Doge Catsono i protocolli, nel senso che essi stessi non determinano ciò che isCriticalritorna, lasciando questo a ogni classe di implementazione di decidere autonomamente. Se hai molte implementazioni, probabilmente varrebbe la pena creare una classe estensibile Cato Dogche già implementa correttamente isCriticaled eliminare efficacemente tutte le classi di implementazione dalla necessità di scavalcare isCritical.

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


È un po 'poco chiaro nell'affermazione della domanda, ma Doge Catsono descritti come classi, mentre Animalè un protocollo che è implementato da ciascuna di quelle classi. Quindi c'è un po 'di discrepanza tra la domanda e la tua risposta.
Caleb,

Quindi suggerisci di lasciare che la modella decida se presentare o meno un popup di conferma? E se fosse coinvolta una logica pesante, come mostrare popup solo se sono visualizzati 10 gatti? La logica Interactorora dipende dallo stato
Andrey Gordeev,

Sì, scusa per la domanda poco chiara, ho apportato poche modifiche. Dovrebbe essere più chiaro ora
Andrey Gordeev,

1
Questo tipo di comportamento non deve essere collegato al modello. Dipende dal contesto e non dall'entità stessa. Penso che Cat e Dog abbiano maggiori probabilità di essere POJO. I comportamenti devono essere gestiti in altri luoghi ed essere in grado di cambiare in base al contesto. Delegare comportamenti o metodi su cui si baseranno i comportamenti in Cat o Dog porterà a troppe responsabilità in tali classi.
Grégory Elhaimer,

@ GrégoryElhaimer Si noti che non determina il comportamento. Sta semplicemente affermando se si tratta o meno di una classe critica. I comportamenti durante il programma che devono sapere se si tratta di una classe critica possono quindi valutare e agire di conseguenza. Se questa è davvero una proprietà che differenzia how casi in entrambi Cate Dogvengono gestiti, può e deve essere una proprietà comune a Animal. Fare qualsiasi altra cosa è chiedere un mal di testa di manutenzione in seguito.
Neil,

4

Tell vs. Ask

L'approccio condizionale che stai mostrando vorremmo chiamare " chiedi ". Qui è dove il cliente consumatore chiede "che tipo sei?" e personalizza di conseguenza il loro comportamento e l'interazione con gli oggetti.

Ciò contrasta con l'alternativa che chiamiamo " raccontare ". Usando tell , spingi più lavoro nelle implementazioni polimorfiche, in modo che il consumo del codice client sia più semplice, senza condizionali e comune indipendentemente dalle possibili implementazioni.

Dal momento che si desidera utilizzare un avviso di conferma, è possibile rendere esplicita questa funzionalità dell'interfaccia. Quindi, potresti avere un metodo booleano che controlla facoltativamente con l'utente e restituisce il valore booleano di conferma. Nelle classi che non vogliono confermare, hanno semplicemente la precedenza return true;. Altre implementazioni potrebbero determinare dinamicamente se vogliono usare la conferma.

Il cliente utilizzatore userebbe sempre il metodo di conferma indipendentemente dalla particolare sottoclasse con cui sta lavorando, il che fa sì che l'interazione dica invece di chiedere .

(Un altro approccio sarebbe quello di spingere la conferma nell'eliminazione, ma ciò sorprenderebbe i clienti consumatori che si aspettano che un'operazione di eliminazione abbia successo.)


Quindi suggerisci di lasciare che la modella decida se presentare o meno un popup di conferma? E se fosse coinvolta una logica pesante, come mostrare popup solo se sono visualizzati 10 gatti? La logica Interactorora dipende dallo stato
Andrey Gordeev,

2
Ok, sì, questa è una domanda diversa, che richiede una risposta diversa.
Erik Eidt,

2

Determinare se è necessaria una conferma è la responsabilità della Catclasse, quindi abilitala a compiere quell'azione. Non conosco Kotlin, quindi esprimerò le cose in C #. Spero che le idee siano poi 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 Catun'istanza, la fornisci TellSceneToShowConfirmationAlert, che dovrà essere restituita truese OK per eliminare:

var model = new Cat(TellSceneToShowConfirmationAlert);

E poi la tua funzione diventa:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}

1
Questo non sposta la logica di eliminazione nel modello? Non sarebbe molto meglio usare un altro oggetto per gestirlo? Forse una struttura di dati come un dizionario <Cat> all'interno di un ApplicationService; controllare per vedere se il gatto esiste e se lo fa quindi attivare l'allarme di conferma?
keelerjr12,

@ keelerjr12, sposta la responsabilità nel determinare se è necessaria una conferma per l'eliminazione nella Catclasse. Direi che è lì che appartiene. Non riesce a decidere come raggiungere quella conferma (che viene iniettata) e non si cancella da solo. Quindi no, non sposta la logica di eliminazione nel modello.
David Arno,

2
Sento che questo approccio porterebbe a tonnellate e tonnellate di codice relativo all'interfaccia utente allegato alla classe stessa. Se si intende utilizzare la classe su più livelli UI, il problema aumenta. Tuttavia, se si tratta di una classe di tipo ViewModel, piuttosto che di un'entità aziendale, allora sembra appropriato.
Graham,

@Graham, sì, questo è sicuramente un rischio con questo approccio: si basa sul fatto che è facile iniettare TellSceneToShowConfirmationAlertin un'istanza di Cat. In situazioni in cui non è una cosa facile (come in un sistema a più livelli in cui questa funzionalità si trova a un livello profondo), questo approccio non sarebbe un buon approccio.
David Arno,

1
Esatto quello che stavo arrivando. Un'entità aziendale rispetto a una classe ViewModel. Nel dominio aziendale, un gatto non dovrebbe conoscere il codice relativo all'interfaccia utente. Il mio gatto di famiglia non avvisa nessuno. Grazie!
keelerjr12,

1

Consiglierei di scegliere un modello Visitatore. 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;
        }
    });
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.