Bedingt durch Polymorphismus ersetzen?

10

Betrachten wir zwei Klassen Dogund Catkonform sowohl zu AnimalProtokoll (in Bezug auf die Swift - Programmiersprache. Diese Schnittstelle in Java / C # würde).

Wir haben einen Bildschirm mit einer gemischten Liste von Hunden und Katzen. Es gibt eine InteractorKlasse, die hinter den Kulissen mit Logik umgeht.

Jetzt möchten wir dem Benutzer eine Bestätigungsbenachrichtigung anzeigen, wenn er eine Katze löschen möchte. Hunde müssen jedoch sofort ohne Benachrichtigung gelöscht werden. Die Methode mit Bedingungen würde folgendermaßen aussehen:

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

Wie kann dieser Code überarbeitet werden? Es riecht offensichtlich

Andrey Gordeev
quelle

Antworten:

9

Sie lassen den Protokolltyp selbst das Verhalten bestimmen. Sie möchten alle Protokolle in Ihrem Programm bis auf die implementierende Klasse selbst gleich behandeln. Wenn Sie dies auf diese Weise tun, respektieren Sie das Substitutionsprinzip von Liskov, das besagt, dass Sie in der Lage sein sollten, entweder Catoder Dog(oder andere Protokolle, unter denen Sie möglicherweise möglicherweise stehen Animal) zu bestehen, und dass es gleichgültig funktioniert.

So vermutlich würden Sie fügen isCriticalfunc Animalvon beiden implementiert werden Dogund Cat. Alles, was implementiert, Dogwürde false und alles, was implementiert wird, Catwürde true zurückgeben.

Zu diesem Zeitpunkt müssten Sie nur noch Folgendes tun (Ich entschuldige mich, wenn die Syntax nicht korrekt ist. Kein Benutzer von Swift):

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

Es gibt nur ein kleines Problem damit, und das ist , dass Dogund CatProtokolle sind, das heißt , sie in sich selbst bestimmt nicht , was isCriticalzurückkehrt, diese verlassen zu jeder implementierende Klasse für sich zu entscheiden. Wenn Sie viele Implementierungen haben, lohnt es sich wahrscheinlich, eine erweiterbare Klasse von Catoder zu erstellen, Dogdie isCriticalalle implementierenden Klassen bereits korrekt implementiert und effektiv von der Notwendigkeit des Überschreibens befreit isCritical.

Wenn dies Ihre Frage nicht beantwortet, schreiben Sie bitte in die Kommentare und ich werde meine Antwort entsprechend erweitern!

Neil
quelle
Es ist ein wenig unklar in der Aussage der Frage, aber Dogund Catwerden als Klassen beschrieben, während Animales sich um ein Protokoll handelt, das von jeder dieser Klassen implementiert wird. Es besteht also ein gewisses Missverhältnis zwischen der Frage und Ihrer Antwort.
Caleb
Sie schlagen also vor, das Modell entscheiden zu lassen, ob ein Bestätigungs-Popup angezeigt werden soll oder nicht? Aber was ist, wenn es sich um eine schwere Logik handelt, wie zum Beispiel ein Show-Popup, wenn nur 10 Katzen angezeigt werden? Die Logik hängt Interactorjetzt vom Zustand ab
Andrey Gordeev
Ja, Entschuldigung für die unklare Frage, ich habe nur wenige Änderungen vorgenommen. Sollte jetzt klarer sein
Andrey Gordeev
1
Diese Art von Verhalten sollte nicht mit dem Modell verknüpft werden. Dies hängt vom Kontext und nicht von der Entität selbst ab. Ich denke, dass Katze und Hund eher POJO sind. Verhaltensweisen sollten an anderen Stellen gehandhabt werden und sich je nach Kontext ändern können. Das Delegieren von Verhaltensweisen oder Methoden, auf die sich Verhaltensweisen bei Katzen oder Hunden stützen, führt zu zu viel Verantwortung in solchen Klassen.
Grégory Elhaimer
@ GrégoryElhaimer Bitte beachten Sie, dass es das Verhalten nicht bestimmt. Es wird lediglich angegeben, ob es sich um eine kritische Klasse handelt oder nicht. Verhaltensweisen im gesamten Programm, die wissen müssen, ob es sich um eine kritische Klasse handelt, können dann bewertet und entsprechend gehandelt werden. Wenn dies tatsächlich eine Eigenschaft , die wie Instanzen in beide differenziert Catund Doggehandhabt werden, kann und soll eine gemeinsame Eigenschaft in sein Animal. Wenn Sie etwas anderes tun, müssen Sie später nach Wartungsproblemen fragen.
Neil
4

Tell vs. Ask

Den bedingten Ansatz, den Sie zeigen, würden wir " fragen " nennen. Hier fragt der konsumierende Kunde : "Was für eine Art sind Sie?" und passt ihr Verhalten und ihre Interaktion mit Objekten entsprechend an.

Dies steht im Gegensatz zu der Alternative, die wir " Tell " nennen. Mit tell schieben Sie mehr Arbeit in die polymorphen Implementierungen, sodass der konsumierende Client-Code einfacher, ohne Bedingungen und unabhängig von den möglichen Implementierungen üblich ist.

Da Sie eine Bestätigungswarnung verwenden möchten, können Sie dies zu einer expliziten Funktion der Schnittstelle machen. Möglicherweise haben Sie eine boolesche Methode, die optional mit dem Benutzer prüft und den booleschen Bestätigungswert zurückgibt. In den Klassen, die nicht bestätigen möchten, überschreiben sie einfach mit return true;. Andere Implementierungen bestimmen möglicherweise dynamisch, ob sie eine Bestätigung verwenden möchten.

Der konsumierende Client verwendet immer die Bestätigungsmethode, unabhängig von der jeweiligen Unterklasse, mit der er arbeitet. Dadurch wird die Interaktion angezeigt, anstatt zu fragen .

(Ein anderer Ansatz wäre, die Bestätigung in den Löschvorgang zu verschieben. Dies würde jedoch die Kunden überraschen, die erwarten, dass ein Löschvorgang erfolgreich ist.)

Erik Eidt
quelle
Sie schlagen also vor, das Modell entscheiden zu lassen, ob ein Bestätigungs-Popup angezeigt werden soll oder nicht? Aber was ist, wenn es sich um eine schwere Logik handelt, wie zum Beispiel ein Show-Popup, wenn nur 10 Katzen angezeigt werden? Die Logik hängt Interactorjetzt vom Zustand ab
Andrey Gordeev
2
Ok, ja, das ist eine andere Frage, die eine andere Antwort erfordert.
Erik Eidt
2

Die Entscheidung, ob eine Bestätigung erforderlich ist, liegt in der Verantwortung der CatKlasse. Aktivieren Sie sie daher, um diese Aktion auszuführen. Ich kenne Kotlin nicht, also werde ich die Dinge in C # ausdrücken. Hoffentlich sind die Ideen dann auch auf Kotlin übertragbar.

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

Wenn Sie dann eine CatInstanz erstellen , geben Sie diese an TellSceneToShowConfirmationAlert, die zurückgegeben werden muss, truewenn OK zum Löschen:

var model = new Cat(TellSceneToShowConfirmationAlert);

Und dann wird Ihre Funktion:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}
David Arno
quelle
1
Verschiebt dies nicht die Löschlogik in das Modell? Wäre es nicht viel besser, ein anderes Objekt zu verwenden, um damit umzugehen? Möglicherweise eine Datenstruktur wie ein Dictionary <Cat> in einem ApplicationService; Überprüfen Sie, ob die Katze existiert, und wenn ja, um den Bestätigungsalarm auszulösen.
keelerjr12
@ keelerjr12 verschiebt es die Verantwortung für die Bestimmung, ob eine Bestätigung für das Löschen in die CatKlasse erforderlich ist . Ich würde argumentieren, dass es dort hingehört. Es kann nicht entscheiden, wie diese Bestätigung erreicht wird (die injiziert wird) und es löscht sich nicht von selbst. Nein, die Löschlogik wird nicht in das Modell verschoben.
David Arno
2
Ich bin der Meinung, dass dieser Ansatz zu Tonnen und Tonnen von UI-Code führen würde, der an die Klasse selbst angehängt wird. Wenn die Klasse für mehrere UI-Ebenen verwendet werden soll, wächst das Problem. Wenn es sich jedoch eher um eine ViewModel-Typklasse als um eine Geschäftsentität handelt, erscheint dies angemessen.
Graham
@ Abraham, ja, das ist definitiv ein Risiko bei diesem Ansatz: Es hängt davon ab, dass es einfach ist, TellSceneToShowConfirmationAlertin eine Instanz von zu injizieren Cat. In Situationen, in denen dies nicht einfach ist (z. B. in einem mehrschichtigen System, in dem diese Funktionalität auf einer tiefen Ebene liegt), wäre dieser Ansatz nicht gut.
David Arno
1
Genau das, worauf ich hinaus wollte. Eine Geschäftseinheit gegen eine ViewModel-Klasse. In der Geschäftsdomäne sollte eine Katze nichts über UI-bezogenen Code wissen. Meine Familienkatze alarmiert niemanden. Vielen Dank!
Keelerjr12
1

Ich würde raten, ein Besuchermuster zu wählen. Ich habe eine kleine Implementierung in Java gemacht. Ich bin nicht mit Swift vertraut, aber Sie können es leicht anpassen.

Der Besucher

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

Dein Modell

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

Den Besucher anrufen

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

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

Sie können so viele Implementierungen von AnimalVisitor haben, wie Sie möchten.

Beispiel:

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
quelle