Заменить Условное на Полиморфизм надлежащим образом?

10

Рассмотрим два класса Dogи Catкак в соответствии с Animalпротоколом (с точки зрения языка программирования Swift. Это было бы интерфейс в Java / C #).

У нас есть экран со смешанным списком собак и кошек. Есть Interactorкласс, который обрабатывает логику за кулисами.

Теперь мы хотим предоставить пользователю подтверждение, когда он хочет удалить кошку. Тем не менее, собаки должны быть удалены немедленно без каких-либо предупреждений. Метод с условиями будет выглядеть так:

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

Как этот код может быть реорганизован? Это явно пахнет

Андрей Гордеев
источник

Ответы:

9

Вы позволяете типу протокола самому определять поведение. Вы хотите обрабатывать все протоколы одинаково во всей вашей программе, кроме самого реализующего класса. Поступая таким образом, вы уважаете принцип подстановки Лискова, который гласит, что вы должны быть в состоянии передать один Catили Dog(или любые другие протоколы, которые вы в конечном итоге могли бы иметь Animal), и заставить его работать безразлично.

Так что, вероятно, вы бы добавили isCriticalфункцию, Animalкоторая будет реализована обоими Dogи Cat. Любая реализация Dogвернула бы false, а любая реализация Catвернула бы true.

На этом этапе вам нужно будет только сделать (Мои извинения, если синтаксис не правильный. Не пользователь Swift):

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

Есть только небольшая проблема с этим, и это Dogи Catесть протоколы, то есть они сами по себе не определяют, что isCriticalвозвращает, оставляя это на усмотрение каждого реализующего класса. Если у вас много реализаций, вероятно, вам стоит создать расширяемый класс Catили Dogкоторый уже правильно реализует isCriticalи эффективно очищает все реализующие классы от необходимости переопределения isCritical.

Если это не отвечает на ваш вопрос, пожалуйста, напишите в комментариях, и я расширю свой ответ соответственно!

Нил
источник
Это немного неясно в формулировке вопроса, но Dogи Catописываются как классы, в то время Animalкак это протокол, который реализуется каждым из этих классов. Так что между вопросом и вашим ответом есть небольшое несоответствие.
Калеб
Итак, вы предлагаете модели разрешить решать, представлять ли всплывающее окно подтверждения или нет? Но что, если задействована тяжелая логика, например, показ всплывающих окон, только если отображается 10 кошек? Логика Interactorтеперь зависит от состояния
Андрей Гордеев
Да, извините за неясный вопрос, я сделал несколько правок. Теперь должно быть яснее
Андрей Гордеев
1
Такое поведение не должно быть связано с моделью. Это зависит от контекста, а не от самой сущности. Я думаю, что Кот и Собака, скорее всего, будут POJO. Поведения должны обрабатываться в других местах и ​​иметь возможность меняться в зависимости от контекста. Делегирование поведения или методов, на которые будет опираться поведение в Cat или Dog, приведет к слишком большим обязанностям в таких классах.
Грегори Эльхаймер
@ GrégoryElhaimer Обратите внимание, что это не определяет поведение. Это просто указывает, является ли это критическим классом. Поведения во всей программе, которые должны знать, является ли это критическим классом, могут затем оценить и действовать соответственно. Если это действительно свойство, которое различает, как экземпляры в обоих Catи Dogобрабатываются, оно может и должно быть общим свойством Animal. Чтобы сделать что-то еще, нужно просить головную боль за обслуживание позже.
Нил
4

Скажи против спроси

Условный подход, который вы демонстрируете, мы бы назвали « спросить ». Вот где потребитель-клиент спрашивает: «Какой ты?» и настраивает их поведение и взаимодействие с объектами соответственно.

Это контрастирует с альтернативой, которую мы называем « сказать ». Используя Tell , вы вкладываете большую часть работы в полиморфные реализации, так что потребляющий клиентский код проще, без условных выражений и является общим независимо от возможных реализаций.

Поскольку вы хотите использовать подтверждающее предупреждение, вы можете сделать это явной возможностью интерфейса. Таким образом, у вас может быть логический метод, который опционально проверяет пользователя и возвращает логическое подтверждение. В тех классах, которые не хотят подтверждать, они просто переопределяются return true;. Другие реализации могут динамически определять, хотят ли они использовать подтверждение.

Потребляющий клиент всегда будет использовать метод подтверждения независимо от того, с каким конкретным подклассом он работает, что заставляет взаимодействие сообщать, а не спрашивать .

(Другой подход заключается в том, чтобы подтолкнуть подтверждение к удалению, но это удивит потребителей, которые ожидают, что операция удаления будет успешной.)

Эрик Эйдт
источник
Итак, вы предлагаете модели разрешить решать, представлять ли всплывающее окно подтверждения или нет? Но что, если задействована тяжелая логика, например, показ всплывающих окон, только если отображается 10 кошек? Логика Interactorтеперь зависит от состояния
Андрей Гордеев
2
Хорошо, да, это другой вопрос, требующий другого ответа.
Эрик Эйдт
2

Ответственность за определение необходимости подтверждения лежит на Catклассе, поэтому дайте ему возможность выполнить это действие. Я не знаю Kotlin, поэтому я буду выражать вещи в C #. Надеемся, что тогда идеи будут переданы и Котлину.

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

Затем при создании Catэкземпляра вы предоставляете его TellSceneToShowConfirmationAlert, который нужно будет вернуть, trueесли OK для удаления:

var model = new Cat(TellSceneToShowConfirmationAlert);

И тогда ваша функция становится:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}
Дэвид Арно
источник
1
Разве это не перемещает логику удаления в модель? Не лучше ли будет использовать другой объект для этого? Возможно, структура данных, такая как Dictionary <Cat> внутри ApplicationService; проверить, существует ли Кошка, и если да, то чтобы включить подтверждение?
keelerjr12
@ keelerjr12, он переносит ответственность за определение необходимости подтверждения для удаления в Catклассе. Я бы сказал, что это то, где оно принадлежит. Он не может решить, как это подтверждение получено (то есть введено), и он не удаляет себя. Так что нет, это не перемещает логику удаления в модель.
Дэвид Арно
2
Я чувствую, что такой подход приведет к тому, что к самому классу будет добавлен код, связанный с пользовательским интерфейсом. Если класс предназначен для использования в нескольких слоях пользовательского интерфейса, проблема возрастает. Однако если это класс типа ViewModel, а не бизнес-объект, то это представляется целесообразным.
Грэм
@ Грэм, да, это определенно риск с таким подходом: он полагается на то, что его легко внедрить TellSceneToShowConfirmationAlertв случай Cat. В ситуациях, когда это нелегко (например, в многослойной системе, где эта функциональность находится на глубоком уровне), такой подход не будет хорошим.
Дэвид Арно
1
Именно то, что я получал. Бизнес-объект против класса ViewModel. В бизнес-сфере Cat не должен знать о коде, связанном с пользовательским интерфейсом. Моя семейная кошка никого не предупреждает. Спасибо!
keelerjr12
1

Я бы посоветовал пойти на шаблон посетителя. Я сделал небольшую реализацию в Java. Я не знаком со Swift, но вы можете легко адаптировать его.

Посетитель

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

Ваша модель

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

Звонить посетителю

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

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

Вы можете иметь столько реализаций AnimalVisitor, сколько захотите.

Пример:

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

        public Boolean visitDog() {
            return true;
        }
    });
}
Грегори Эльхаймер
источник