Substituir Condicional pelo Polimorfismo de maneira adequada?

10

Considere duas classes Doge Catambas em conformidade com o Animalprotocolo (em termos da linguagem de programação Swift. Essa seria a interface em Java / C #).

Temos uma tela exibindo uma lista mista de cães e gatos. Há Interactorclasse que lida com a lógica nos bastidores.

Agora, queremos apresentar um alerta de confirmação ao usuário quando ele desejar excluir um gato. No entanto, os cães precisam ser excluídos imediatamente, sem nenhum alerta. O método com condicionais ficaria assim:

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

Como esse código pode ser refatorado? Obviamente cheira

Andrey Gordeev
fonte

Respostas:

9

Você está deixando o próprio tipo de protocolo determinar o comportamento. Você deseja tratar todos os protocolos da mesma forma em todo o programa, exceto na própria classe de implementação. Fazer dessa maneira é respeitar o Princípio da Substituição de Liskov, que diz que você deve ser capaz de aprovar um Catou outro Dog(ou qualquer outro protocolo que você possa eventualmente ter sob Animalpotencial) e fazê-lo funcionar de maneira indiferente.

Então, presumivelmente, você adicionaria uma isCriticalfunção Animala ser implementada por ambos Doge Cat. Qualquer coisa implementada Dogretornaria falsa e qualquer coisa implementada Catretornaria verdadeira.

Nesse ponto, você só precisa fazer (minhas desculpas se a sintaxe não estiver correta. Não é usuário do Swift):

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

Existe apenas um pequeno problema com isso, e é isso Doge Catsão protocolos, o que significa que eles mesmos não determinam o que isCriticalretorna, deixando isso para todas as classes de implementação para decidirem por si mesmas. Se você tiver muitas implementações, provavelmente valeria a pena criar uma classe extensível Catou Dogque já implementa corretamente isCriticale efetivamente limpa todas as classes implementadoras da necessidade de substituição isCritical.

Se isso não responder à sua pergunta, escreva nos comentários e expandirei minha resposta de acordo!

Neil
fonte
É um pouco claro na declaração da pergunta, mas Doge Catsão descritas como classes, enquanto que Animalé um protocolo que é implementado por cada uma dessas classes. Portanto, há um pouco de incompatibilidade entre a pergunta e sua resposta.
Caleb
Então, você sugere que o modelo decida se deve apresentar um pop-up de confirmação ou não? Mas e se houver uma lógica pesada envolvida, como mostrar pop-up apenas se houver 10 gatos exibidos? A lógica depende do Interactorestado agora
Andrey Gordeev
Sim, desculpe pela pergunta incerta, eu fiz algumas edições. Deveria estar mais claro agora
Andrey Gordeev 04/10
1
Esse tipo de comportamento não deve estar vinculado ao modelo. Depende do contexto e não da própria entidade. Eu acho que gato e cachorro são mais propensos a ser POJO. Os comportamentos devem ser tratados em outros lugares e poder mudar de acordo com o contexto. Delegar comportamentos ou métodos nos quais os comportamentos dependerão no gato ou no cachorro levará a muitas responsabilidades nessas classes.
Grégory Elhaimer
@ GrégoryElhaimer Observe que não é um comportamento determinante. É apenas afirmar se é ou não uma classe crítica. Comportamentos ao longo do programa que precisam saber se é uma classe crítica podem avaliar e agir de acordo. Se esta é realmente uma propriedade que diferencia como as instâncias em ambos Cate Dogsão tratados, ele pode e deve ser uma propriedade comum em Animal. Fazer qualquer outra coisa está pedindo dor de cabeça para manutenção mais tarde.
Neil
4

Tell vs. Ask

A abordagem condicional que você está mostrando, chamaríamos de " perguntar ". É aqui que o cliente consumidor pergunta "de que tipo você é?" e personaliza seu comportamento e interação com os objetos de acordo.

Isso contrasta com a alternativa que chamamos de " contar ". Usando o tell , você envia mais do trabalho para as implementações polimórficas, para que o código do cliente consumidor seja mais simples, sem condicionais e comum, independentemente das implementações possíveis.

Como você deseja usar um alerta de confirmação, você pode tornar isso um recurso explícito da interface. Portanto, você pode ter um método booleano que, opcionalmente, verifica com o usuário e retorna a confirmação booleana. Nas classes que não desejam confirmar, elas simplesmente substituem return true;. Outras implementações podem determinar dinamicamente se desejam usar a confirmação.

O cliente consumidor sempre usaria o método de confirmação, independentemente da subclasse específica com a qual trabalha, o que torna a interação informada em vez de perguntar .

(Outra abordagem seria empurrar a confirmação para a exclusão, mas isso surpreenderia os clientes consumidores que esperam que uma operação de exclusão seja bem-sucedida.)

Erik Eidt
fonte
Então, você sugere que o modelo decida se deve apresentar um pop-up de confirmação ou não? Mas e se houver uma lógica pesada envolvida, como mostrar pop-up apenas se houver 10 gatos exibidos? A lógica depende do Interactorestado agora
Andrey Gordeev 04/10
2
Ok, sim, essa é uma pergunta diferente, exigindo uma resposta diferente.
Erik Eidt
2

Determinar se uma confirmação é necessária é de responsabilidade da Catclasse, portanto, habilite-a para executar essa ação. Eu não conheço Kotlin, então vou expressar as coisas em c #. Espero que as idéias também sejam transferíveis para 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;
}

Em seguida, ao criar uma Catinstância, você a fornecerá TellSceneToShowConfirmationAlert, que precisará retornar truese OK para excluir:

var model = new Cat(TellSceneToShowConfirmationAlert);

E então sua função se torna:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}
David Arno
fonte
1
Isso não move a lógica de exclusão para o modelo? Não seria muito melhor usar outro objeto para lidar com isso? Possivelmente uma estrutura de dados como um Dictionary <Cat> dentro de um ApplicationService; verifique se o gato existe e se existe, para disparar o alerta de confirmação?
precisa saber é o seguinte
@ keelerjr12, move a responsabilidade de determinar se é necessária uma confirmação para a exclusão na Catclasse. Eu diria que é aí que ele pertence. Ele não decide como essa confirmação é alcançada (que é injetada) e não se exclui. Portanto, não, ela não move a lógica de exclusão para o modelo.
David Arno
2
Eu sinto que essa abordagem levaria a toneladas e toneladas de código relacionado à interface do usuário anexadas à própria classe. Se a classe se destina a ser usada em várias camadas da interface do usuário, o problema aumenta. No entanto, se esta é uma classe do tipo ViewModel, em vez de uma entidade comercial, parece apropriado.
Graham
@ Graham, sim, esse é definitivamente um risco com essa abordagem: ela depende de ser fácil injetar TellSceneToShowConfirmationAlertem uma instância do Cat. Em situações em que isso não é algo fácil (como em um sistema de várias camadas em que essa funcionalidade se encontra em um nível profundo), essa abordagem não seria boa.
David Arno
1
Exatamente o que eu estava conseguindo. Uma entidade comercial versus uma classe ViewModel. No domínio comercial, um gato não deve saber sobre o código relacionado à interface do usuário. Meu gato da família não alerta ninguém. Obrigado!
precisa saber é o seguinte
1

Eu recomendaria ir para um padrão de visitante. Eu fiz uma pequena implementação em Java. Não conheço o Swift, mas você pode adaptá-lo facilmente.

O visitante

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

Seu modelo

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

Ligar para o visitante

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

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

Você pode ter quantas implementações do AnimalVisitor quiser.

Exemplo:

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
fonte