Zastąpić warunkowo polimorfizmem w odpowiedni sposób?

10

Rozważ dwie klasy Dogi Catobie zgodne z Animalprotokołem (pod względem języka programowania Swift. Byłby to interfejs w Javie / C #).

Mamy ekran wyświetlający mieszaną listę psów i kotów. Istnieje Interactorklasa, która obsługuje logikę za kulisami.

Teraz chcemy przedstawić użytkownikowi powiadomienie o potwierdzeniu, gdy chce usunąć kota. Psy należy jednak natychmiast usunąć bez żadnych powiadomień. Metoda z warunkami warunkowymi wyglądałaby następująco:

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

Jak można refaktoryzować ten kod? To oczywiście pachnie

Andrey Gordeev
źródło

Odpowiedzi:

9

Pozwalasz samemu typowi protokołu określać zachowanie. Chcesz traktować wszystkie protokoły tak samo w całym programie, z wyjątkiem samej klasy implementującej. Robi to w ten sposób przestrzega Liskov za Zmiana zasadę, która mówi, że powinieneś być w stanie przejść albo Catalbo Dog(lub jakichkolwiek innych protokołów można w końcu mieć pod Animalpotencjalnie) i mają działać obojętnie.

Przypuszczalnie dodajesz isCriticalfunc, który Animalma być zaimplementowany zarówno przez, jak Dogi przez Cat. Cokolwiek implementujące Dogzwróci fałsz, a cokolwiek implementujące Catzwróci prawdę.

W tym momencie musisz tylko zrobić (przepraszam, jeśli składnia jest nieprawidłowa. Nie jest użytkownikiem Swift):

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

Jest z tym tylko niewielki problem, i to jest to, Dogi Catsą to protokoły, co oznacza, że ​​same w sobie nie określają, co isCriticalzwraca, pozostawiając to każdej klasie implementującej, która sama decyduje. Jeśli masz wiele implementacji, prawdopodobnie warto poświęcić trochę czasu na utworzenie rozszerzalnej klasy Catlub Dogktóra już poprawnie implementuje isCriticali skutecznie usuwa wszystkie klasy implementujące z konieczności zastąpienia isCritical.

Jeśli to nie odpowiada na twoje pytanie, napisz w komentarzach, a odpowiednio poszerzę swoją odpowiedź!

Neil
źródło
To jest trochę niejasne w sprawozdaniu z pytaniem, ale Dogi Catsą opisane jako klasy, podczas gdy Animaljest to protokół, który jest realizowany przez każdy z tych klas. Jest więc trochę niezgodności między pytaniem a odpowiedzią.
Caleb
Sugerujesz więc, aby pozwolić modelowi zdecydować, czy ma wyświetlać okienko z potwierdzeniem, czy nie? Ale co, jeśli w grę wchodzi ciężka logika, np. Pokaż wyskakujące okienko tylko wtedy, gdy wyświetlanych jest 10 kotów? Logika zależy Interactorteraz od stanu
Andrey Gordeev
Tak, przepraszam za niejasne pytanie, wprowadziłem kilka zmian. Teraz powinno być bardziej jasne
Andrey Gordeev
1
Tego rodzaju zachowanie nie powinno być powiązane z modelem. To zależy od kontekstu, a nie od samego bytu. Myślę, że Kot i Pies częściej są POJO. Zachowania powinny być obsługiwane w innych miejscach i mogą być zmieniane w zależności od kontekstu. Delegowanie zachowań lub metod, na których będą się opierać Cat lub Dog, doprowadzi do zbyt dużej odpowiedzialności w takich klasach.
Grégory Elhaimer,
@ GrégoryElhaimer Pamiętaj, że to nie determinuje zachowania. Stwierdza jedynie, czy jest to klasa krytyczna, czy nie. Zachowania w całym programie, które muszą wiedzieć, czy jest to klasa krytyczna, mogą następnie ocenić i odpowiednio postępować. Jeśli tak jest rzeczywiście właściwością, która odróżnia how instancji w obu Cati Dogsą obsługiwane, to może i powinien być wspólny majątek Animal. Robienie czegokolwiek innego wymaga późniejszego bólu głowy związanego z konserwacją.
Neil,
4

Tell vs. Ask

Podejście warunkowe, które pokazujemy, nazwalibyśmy „ zapytać ”. W tym miejscu klient konsumujący pyta „jaki jesteś?” i odpowiednio dostosowuje ich zachowanie i interakcję z obiektami.

Kontrastuje to z alternatywą, którą nazywamy „ powiedz ”. Korzystając z tell , wkładasz więcej pracy w polimorficzne implementacje, dzięki czemu kod klienta jest prostszy, bez warunków i wspólny, niezależnie od możliwych implementacji.

Ponieważ chcesz użyć alertu potwierdzającego, możesz uczynić to jawną funkcją interfejsu. Tak więc możesz mieć metodę logiczną, która opcjonalnie sprawdza u użytkownika i zwraca wartość logiczną potwierdzenia. W klasach, które nie chcą potwierdzić, po prostu zastępują return true;. Inne implementacje mogą dynamicznie określać, czy chcą użyć potwierdzenia.

Klient używający zawsze używałby metody potwierdzenia, niezależnie od konkretnej podklasy, z którą współpracuje, co powoduje, że interakcja mówi zamiast pytać .

(Innym podejściem byłoby przesunięcie potwierdzenia do usunięcia, ale zaskoczyłoby to konsumentów, którzy oczekują, że operacja usunięcia powiedzie się.)

Erik Eidt
źródło
Sugerujesz więc, aby pozwolić modelowi zdecydować, czy ma wyświetlać okienko z potwierdzeniem, czy nie? Ale co, jeśli w grę wchodzi ciężka logika, np. Pokaż wyskakujące okienko tylko wtedy, gdy wyświetlanych jest 10 kotów? Logika zależy Interactorteraz od stanu
Andrey Gordeev
2
Ok, tak, to inne pytanie, wymagające innej odpowiedzi.
Erik Eidt,
2

Określenie, czy konieczne jest potwierdzenie, jest obowiązkiem Catklasy, więc pozwól jej wykonać tę akcję. Nie znam Kotlina, więc wyrażę rzeczy w języku C #. Mamy nadzieję, że pomysły te zostaną następnie przekazane Kotlinowi.

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

Następnie, tworząc Catinstancję, dostarczasz ją TellSceneToShowConfirmationAlert, która będzie musiała wrócić, truejeśli OK, aby usunąć:

var model = new Cat(TellSceneToShowConfirmationAlert);

A potem twoja funkcja staje się:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}
David Arno
źródło
1
Czy to nie przenosi logiki usuwania do modelu? Czy nie byłoby o wiele lepiej użyć innego obiektu do obsługi tego? Prawdopodobnie struktura danych, taka jak Dictionary <Cat> wewnątrz ApplicationService; sprawdzić, czy kot istnieje, a jeśli tak, to wystrzelić alert potwierdzający?
keelerjr12
@ keelerjr12, przenosi odpowiedzialność za określenie, czy potwierdzenie jest potrzebne do usunięcia do Catklasy. Twierdziłbym, że tam właśnie należy. Nie decyduje, w jaki sposób osiąga się to potwierdzenie (które jest wstrzykiwane) i nie usuwa się. Więc nie, nie przenosi logiki usuwania do modelu.
David Arno
2
Wydaje mi się, że takie podejście doprowadziłoby do wielu ton kodu związanego z interfejsem użytkownika dołączonego do samej klasy. Jeśli klasa ma być używana na wielu warstwach interfejsu użytkownika, problem rośnie. Jeśli jednak jest to klasa typu ViewModel, a nie jednostka biznesowa, wydaje się to właściwe.
Graham
@Graham, tak, to zdecydowanie ryzyko przy takim podejściu: polega na tym, że łatwo jest go wprowadzić TellSceneToShowConfirmationAlertw instancję Cat. W sytuacjach, gdy nie jest to łatwe (na przykład w systemie wielowarstwowym, w którym ta funkcjonalność leży na głębokim poziomie), to podejście nie byłoby dobre.
David Arno
1
Dokładnie o co mi chodziło. Podmiot biznesowy a klasa ViewModel. W domenie biznesowej kot nie powinien wiedzieć o kodzie związanym z interfejsem użytkownika. Mój rodzinny kot nikogo nie ostrzega. Dzięki!
keelerjr12
1

Radziłbym wybrać wzór dla odwiedzających. Zrobiłem małą implementację w Javie. Nie znam Swift, ale możesz go łatwo dostosować.

Gość

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

Twój model

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

Dzwonię do gościa

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

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

Możesz mieć tyle implementacji AnimalVisitor, ile chcesz.

Przykład:

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
źródło