Когда типовые испытания в порядке?

53

Предполагая язык с некоторой присущей безопасности типов (например, не JavaScript):

Учитывая метод, который принимает a SuperType, мы знаем, что в большинстве случаев, когда у нас может возникнуть искушение выполнить тестирование типа для выбора действия:

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    o.doSomethingA()
  } else {
    o.doSomethingB();
  }
}

Обычно мы должны, если не всегда, создавать один переопределяемый метод для SuperTypeи делать это:

public void DoSomethingTo(SuperType o) {
  o.doSomething();
}

... где каждый подтип имеет свою собственную doSomething()реализацию. Остальная часть нашего приложения может быть соответственно неосведомлена о том, SuperTypeявляется ли какое-либо данное на самом деле a SubTypeAили a SubTypeB.

Замечательный.

Но нам все еще дают is aподобные операции в большинстве, если не во всех, типобезопасных языках. И это говорит о потенциальной необходимости явного тестирования типов.

Итак, в каких ситуациях, если таковые имеются, должны мы или должны мы выполняем явное тестирование типа?

Прости меня за рассеянность или недостаток творчества. Я знаю, что делал это раньше; но, честно говоря, это было так давно, я не могу вспомнить, было ли то, что я сделал, хорошо! И в последнее время я не думаю, что столкнулся с необходимостью тестировать типы вне моего ковбойского JavaScript.

svidgen
источник
1
Читайте о типах логического вывода и системах типов
Василий Старынкевич,
4
Стоит отметить, что ни в Java, ни в C # не было обобщений в их первой версии. Вы должны были привести к объекту и из объекта, чтобы использовать контейнеры.
Довал
6
«Проверка типов» почти всегда относится к проверке того, соблюдает ли код правила статической типизации языка. То, что вы имеете в виду, обычно называется тестированием типов .
3
Вот почему я люблю писать на C ++ и оставлять RTTI выключенным. Когда человек буквально не может тестировать типы объектов во время выполнения, он заставляет разработчика придерживаться хорошего ОО-дизайна в отношении вопроса, который здесь задают.

Ответы:

48

«Никогда» - это канонический ответ «когда все в порядке с тестированием типов?» Там нет никакого способа доказать или опровергнуть это; это часть системы убеждений о том, что делает «хороший дизайн» или «хороший объектно-ориентированный дизайн». Это тоже хокум.

Безусловно, если у вас есть интегрированный набор классов, а также более одной или двух функций, которые требуют такого рода прямого тестирования типов, вы, вероятно, ДЕЛАЕТЕ ЭТО НЕПРАВИЛЬНО. Что вам действительно нужно, так это метод, реализованный по-другому SuperTypeи его подтипы. Это неотъемлемая часть объектно-ориентированного программирования, и существуют целые классы разума и наследования.

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

Решение: используйте идиому. Как вы предложили, добавьте метод к каждому из классов, а затем позвольте стандартным алгоритмам наследования и выбора метода определить, какой случай применим. Или, если вы не можете изменить базовые типы, создайте подкласс и добавьте туда свой метод.

Так много для обычной мудрости, и на некоторые ответы. Некоторые случаи, когда имеет смысл явное типовое тестирование:

  1. Это разовое. Если вам нужно было много различать типы, вы можете расширить типы или подкласс. Но ты не. У вас есть только одно или два места, где вам нужно явное тестирование, поэтому вам не стоит возвращаться и работать с иерархией классов, чтобы добавить функции в качестве методов. Или же это не стоит практических усилий, чтобы добавить такие общие, тестовые, проектные обзоры, документацию или другие атрибуты базовых классов для такого простого, ограниченного использования. В этом случае добавление функции, выполняющей прямое тестирование, является рациональным.

  2. Вы не можете настроить классы. Вы думаете о подклассе - но вы не можете. Например, обозначены многие классы в Java final. Вы пытаетесь добавить a, public class ExtendedSubTypeA extends SubTypeA {...} и компилятор недвусмысленно говорит вам, что то, что вы делаете, невозможно. Извините, изящество и изощренность объектно-ориентированной модели! Кто-то решил, что вы не можете расширить их типы! К сожалению, многие из стандартных библиотек есть final, и создание классов finalявляется общим руководством по проектированию. Функция end-run - это то, что вам доступно.

    Кстати, это не ограничивается статически типизированными языками. Динамический язык В Python есть ряд базовых классов, которые под оболочками, реализованными в C, не могут быть изменены. Как и Java, он включает в себя большинство стандартных типов.

  3. Ваш код является внешним. Вы разрабатываете с классами и объектами, которые поступают из ряда серверов баз данных, механизмов промежуточного программного обеспечения и других кодовых баз, которые вы не можете контролировать или настраивать. Ваш код - просто непритязательный потребитель объектов, сгенерированных в другом месте. Даже если бы вы могли SuperTypeсоздавать подклассы , вы не сможете получить те библиотеки, от которых зависит создание объектов в ваших подклассах. Они передадут вам экземпляры известных им типов, а не ваши варианты. Это не всегда так ... иногда они созданы для гибкости и динамически создают экземпляры классов, которые вы им кормите. Или они предоставляют механизм для регистрации подклассов, которые вы хотите построить на своих фабриках. Парсеры XML кажутся особенно хорошими в предоставлении таких точек входа; см. например или lxml в Python . Но большинство кодовых баз не предоставляют таких расширений. Они вернут вам классы, с которыми они были построены, и знают о них. Как правило, не имеет смысла передавать их результаты в ваши пользовательские результаты, чтобы вы могли использовать чисто объектно-ориентированный селектор типов. Если вы собираетесь проводить различение типов, вам придется делать это относительно грубо. Ваш код тестирования типов выглядит вполне уместно.

  4. Дженерики бедного человека / многократная отправка. Вы хотите принимать различные типы кода в свой код и чувствовать, что наличие массива методов, очень специфичных для типа, не изящно. public void add(Object x)кажется логичным, но не массив addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, и addStringварианты (чтобы назвать несколько). Имея функции или методы, которые берут верхний супертип, а затем определяют, что делать в зависимости от типа, они не получат награду за чистоту на ежегодном Симпозиуме им. Буча-Лискова, а отбросят Венгерское наименование даст вам более простой API. В некотором смысле, ваш is-aилиis-instance-of тестирование - это моделирование универсального или мультидиспетчерского в языковом контексте, который изначально не поддерживает его.

    Встроенная языковая поддержка как универсальных, так и утиных наборов сокращает необходимость проверки типов, повышая вероятность «сделать что-то изящное и подходящее». Множественная диспетчеризация Выбор / интерфейс видел в таких языках , как Юлю и Go так же заменить прямое тестирование типа со встроенными механизмами выбора типа на основе «что делать». Но не все языки поддерживают это. Например, Java, как правило, с одной диспетчеризацией, и его идиомы не являются супер-дружественными для утки.

    Но даже со всеми этими функциями различения типов - наследование, обобщенные типы, типирование утки и многократная диспетчеризация - иногда просто удобно иметь единую консолидированную процедуру, которая делает тот факт, что вы делаете что-то, основываясь на типе объекта ясно и сразу. В метапрограммировании я нашел это по существу неизбежным. Является ли возврат к прямым запросам типа «прагматизмом в действии» или «грязным кодированием», будет зависеть от вашей философии дизайна и убеждений.

Джонатан Юнис
источник
Если операция не может быть выполнена с конкретным типом, но может быть выполнена с двумя различными типами, к которым она может быть неявно преобразована, перегрузка подходит только в том случае, если оба преобразования дают одинаковые результаты. В противном случае можно столкнуться с враждебным поведением, как в .NET: (1.0).Equals(1.0f)выдача true [аргумент способствует double], но (1.0f).Equals(1.0)выдача false [аргумент способствует object]; в Java Math.round(123456789*1.0)выдает 123456789, но Math.round(123456789*1.0)выдает 123456792 [аргумент floatскорее повышает , чем double].
суперкат
Это классический аргумент против автоматического приведения типов / эскалации. Плохие и парадоксальные результаты, по крайней мере, в крайних случаях. Я согласен, но не уверен, как вы собираетесь это отнести к моему ответу.
Джонатан Юнис
Я отвечал на ваш пункт № 4, который, похоже, выступал за перегрузку, а не за использование методов с разными именами с разными типами.
суперкат
2
@supercat Виноват мое плохое зрение, но эти два выражения Math.roundвыглядят одинаково для меня. Какая разница?
Лили Чунг
2
@IstvanChung: Упс ... последний должен был Math.round(123456789)[показывать, что может произойти, если кто-то переписывает, Math.round(thing.getPosition() * COUNTS_PER_MIL)чтобы вернуть немасштабированное значение позиции, не понимая, что getPositionвозвращает intили long.]
суперкат
25

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

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

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

Карл Билефельдт
источник
1
Я был на мгновение в восторге от случая десериализации, ложно вспомнив об использовании его там; но сейчас я не представляю, как бы я это сделал! Я знаю, что сделал несколько странных поисков типов для этого; но я не уверен, что это тестирование типа . Возможно, сериализация - это более тесная параллель: нужно опрашивать объекты по их конкретному типу.
svidgen
1
Сериализация обычно выполнима с использованием полиморфизма. При десериализации вы часто делаете что-то подобное BaseClass base = deserialize(input), потому что вы еще не знаете тип, а затем if (base instanceof Derived) derived = (Derived)baseсохраняете его как его точный производный тип.
Карл Билефельдт
1
Равенство имеет смысл. По моему опыту, такие методы часто имеют такую ​​форму, как «если эти два объекта имеют один и тот же конкретный тип, возвращают, все ли их поля равны; в противном случае верните false (или несопоставимый) ».
Джон Перди,
2
В частности, тот факт, что вы тестируете тип, является хорошим индикатором того, что сравнения полиморфного равенства являются гнездом гадюк.
Стив Джессоп,
12

Стандартный (но, надеюсь, редкий) случай выглядит так: если в следующей ситуации

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

функции DoSomethingAили DoSomethingBне могут быть легко реализованы как функции-члены дерева наследования SuperType/ SubTypeA/ SubTypeB. Например, если

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

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

Пример из моей вчерашней работы: у меня была ситуация, когда я собирался распараллелить обработку списка объектов (типа SuperType, точно с двумя разными подтипами, где крайне маловероятно, что их когда-либо будет больше). Непараллельная версия содержала два цикла: один цикл для объектов подтипа A, вызывающий DoSomethingA, и второй цикл для объектов подтипа B, вызывающий DoSomethingB.

Методы «DoSomethingA» и «DoSomethingB» - это интенсивные вычисления, использующие контекстную информацию, которая недоступна в области действия подтипов A и B. (поэтому нет смысла реализовывать их как функции-члены подтипов). С точки зрения нового «параллельного цикла», это делает вещи намного проще, работая с ними единообразно, поэтому я реализовал функцию, аналогичную DoSomethingToописанной выше. Однако изучение реализаций «DoSomethingA» и «DoSomethingB» показывает, что внутри они работают по-разному. Таким образом, попытка реализовать универсальное «DoSomething» путем расширения SuperTypeс помощью множества абстрактных методов на самом деле не сработает или будет означать полное перепроектирование вещей.

Док Браун
источник
2
Есть ли шанс, что вы можете добавить небольшой конкретный пример, чтобы убедить мой туманный мозг, что этот сценарий не надуман?
svidgen
Чтобы было ясно, я не говорю, что это надумано. Я подозреваю, что сегодня я просто в смятении.
svidgen
@svidgen: это далеко не надумано, на самом деле я столкнулся с такой ситуацией сегодня (хотя я не могу опубликовать этот пример здесь, потому что он содержит внутреннюю информацию о бизнесе). И я согласен с другими ответами, что использование оператора «is» должно быть исключением и только в редких случаях.
Док Браун
Я думаю, что ваше редактирование, которое я упустил, дает хороший пример. ... Существуют ли случаи , где это нормально , даже если вы делаете контроль SuperTypeи это подклассы?
svidgen
6
Вот конкретный пример: контейнером верхнего уровня в ответе JSON от веб-службы может быть словарь или массив. Как правило, у вас есть какой-то инструмент, который превращает JSON в реальные объекты (например, NSJSONSerializationв Obj-C), но вы не хотите просто полагать, что ответ содержит ожидаемый вами тип, поэтому перед его использованием вы проверяете его (например if ([theResponse isKindOfClass:[NSArray class]])...) ,
Калеб
5

Как называет это дядя Боб:

When your compiler forgets about the type.

В одном из своих эпизодов чистого кодера он привел пример вызова функции, который используется для возврата Employees. Managerэто подтип Employee. Давайте предположим, что у нас есть служба приложений, которая принимает Managerидентификатор и вызывает его в офис :) Функция getEmployeeById()возвращает супертип Employee, но я хочу проверить, возвращается ли менеджер в этом сценарии использования.

Например:

var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();

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

Не лучший пример, но в конце концов это дядя Боб.

Обновить

Я обновил пример настолько, насколько я помню по памяти.

Songo
источник
1
Почему в этом примере не Managerреализована summon()просто реализация исключения?
Свидген
@svidgen может быть, CEOможет вызвать Managers.
user253751
@svidgen, тогда было бы не так ясно, что employeeRepository.getEmployeeById (empId) должен вернуть менеджера
Ян
@ Я не вижу в этом проблемы. Если вызывающий код запрашивает Employee, он должен заботиться только о том, чтобы он получал что-то похожее на Employee. Если разные подклассы Employeeимеют разные разрешения, обязанности и т. Д., Что делает тестирование типов лучшим вариантом, чем реальная система разрешений?
svidgen
3

Когда проверка типов в порядке?

Никогда.

  1. Имея поведение, связанное с этим типом в какой-либо другой функции, вы нарушаете открытый закрытый принцип , потому что вы можете изменять существующее поведение типа, изменяя isпредложение или (в некоторых языках или в зависимости от сценарий), потому что вы не можете расширить тип без изменения внутренних функций функции, выполняющей isпроверку.
  2. Что еще более важно, isпроверки являются сильным признаком того, что вы нарушаете принцип подстановки Лискова . Все, с чем работает, SuperTypeдолжно полностью не знать, какие могут быть подтипы.
  3. Вы неявно связываете некоторое поведение с именем типа. Это усложняет поддержку вашего кода, потому что эти неявные контракты распространяются по всему коду и не гарантируется универсальное и последовательное применение, как у реальных членов класса.

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

Короче говоря, вы всегда должны рассматривать проверки типов как сильный запах кода. Но, как и во всех руководящих принципах, будут моменты, когда вы будете вынуждены выбирать, какое из них является наименее оскорбительным.

Telastyn
источник
3
Есть одно небольшое предостережение: реализация алгебраических типов данных в языках, которые их не поддерживают. Тем не менее, использование наследования и проверки типов является чисто хакерской деталью реализации; цель не в том, чтобы ввести подтипы, а в том, чтобы классифицировать значения. Я поднимаю это только потому, что ADT полезны и никогда не являются очень сильным определителем, но в остальном я полностью согласен; instanceofутечки деталей реализации и ломает абстракцию.
Довал
17
«Никогда» - это слово, которое мне не очень нравится в таком контексте, особенно когда оно противоречит тому, что вы пишете ниже.
Док Браун
10
Если под «никогда» вы на самом деле имеете в виду «иногда», то вы правы.
Калеб
2
ОК означает допустимый, приемлемый и т. Д., Но не обязательно идеальный или оптимальный. Контраст с не хорошо : если что-то не в порядке, то вы не должны делать это вообще. Как указать, необходимо проверить тип что - то может быть признаком более глубоких проблем в коде, но бывают случаи , когда это наиболее целесообразно, наименее плохой вариант, и в таких ситуациях , это, очевидно , OK , чтобы использовать инструменты в вашем удаление. (Если бы их было легко избежать во всех ситуациях, их, вероятно, не было бы в первую очередь.) Вопрос сводится к идентификации этих ситуаций и никогда не помогает.
Калеб
2
@supercat: Методы должны требовать наименьшего возможного типа, который удовлетворяет их требованиям . IEnumerable<T>не обещает, что существует "последний" элемент. Если вашему методу нужен такой элемент, он должен требовать тип, который гарантирует его существование. И тогда подтипы этого типа могут обеспечить эффективную реализацию метода «Last».
Чао
2

Если вы получили большую кодовую базу (более 100 тыс. Строк кода) и близки к отгрузке или работаете в филиале, который впоследствии придется объединить, и, следовательно, существует много затрат / рисков, связанных с изменением большого количества кода.

Затем у вас есть возможность выбрать большой рефрактор системы или простое локализованное «тестирование типа». Это создает технический долг, который должен быть возвращен как можно скорее, но часто это не так.

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

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


Другим распространенным случаем является код пользовательского интерфейса, когда, например, вы показываете другой пользовательский интерфейс для некоторых типов сотрудников, но явно не хотите, чтобы концепции пользовательского интерфейса проникали во все ваши «доменные» классы.

Вы можете использовать «тестирование типов», чтобы решить, какую версию пользовательского интерфейса показывать, или иметь какую-то необычную таблицу поиска, которая преобразует «доменные классы» в «классы пользовательского интерфейса». Таблица поиска - это просто способ скрыть «типовое тестирование» в одном месте.

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

Ян
источник
Шаблон посетителя часто является хорошим способом решения вашей ситуации с пользовательским интерфейсом.
Ян Голдби
@IanGoldby, время от времени соглашался, что это возможно, однако вы все еще делаете «тестирование типов», просто немного скрытое.
Ян
Скрыто в том же смысле, что скрыто при вызове обычного виртуального метода? Или ты имеешь в виду что-то еще? Шаблон посетителя, который я использовал, не имеет условных операторов, которые зависят от типа. Это все сделано языком.
Ян Голдби
@IanGoldby, я имел в виду скрытый в том смысле, что он может усложнить понимание кода WPF или WinForms. Я ожидаю, что для некоторых веб-интерфейсов это будет работать очень хорошо.
Ян
2

Реализация LINQ использует множество проверок типов для возможной оптимизации производительности, а затем запасной вариант для IEnumerable.

Наиболее очевидный пример - это, вероятно, метод ElementAt (небольшая выдержка из источника .NET 4.5):

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

Но в классе Enumerable есть много мест, где используется подобный шаблон.

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

Менно ван ден Хеувел
источник
Его можно было бы спроектировать лучше, предоставив удобные средства, с помощью которых интерфейсы могли бы предоставлять реализации по умолчанию, и затем IEnumerable<T>включив в него множество методов, подобных тем List<T>, которые указаны в , вместе со Featuresсвойством, указывающим, какие методы могут работать хорошо, медленно или вообще не работать, а также различные предположения, которые потребитель может безопасно сделать относительно коллекции (например, гарантируется ли ее размер и / или существующее содержимое, которое никогда не изменится [тип может поддерживать, Addпри этом гарантируя, что существующее содержимое будет неизменным]).
суперкат
За исключением случаев, когда типу может понадобиться хранить одну из двух совершенно разных вещей в разное время и требования являются явно взаимоисключающими (и, следовательно, использование отдельных полей было бы излишним), я обычно считаю, что необходимость в приведении типа try является признаком те члены, которые должны были быть частью базового интерфейса, не были. Нельзя сказать, что код, который использует интерфейс, который должен был включать некоторые члены, но не должен использовать try-casting для обхода упущения базового интерфейса, но эти пишущие базовые интерфейсы должны минимизировать потребность клиентов в try-cast.
суперкат
1

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

Предположим, что все игровые объекты происходят из общего базового класса GameObject. Каждый объект имеет жесткую форму столкновения тела , CollisionShapeкоторое может обеспечить общий интерфейс (сказать , позицию запроса, ориентацию и т.д.) , но фактические формы столкновения будут все конкретные подклассы , такие как Sphere, Box, ConvexHullи т.д. , хранящей информация , относящаяся к этому типу геометрического объекта (см. здесь для реального примера)

Теперь, чтобы проверить наличие коллизий, мне нужно написать функцию для каждой пары типов коллизий:

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

которые содержат конкретную математику, необходимую для пересечения этих двух геометрических типов.

На каждом тике моего игрового цикла мне нужно проверять пары объектов на наличие столкновений. Но у меня есть доступ только к GameObjects и соответствующим CollisionShapes. Ясно, что мне нужно знать конкретные типы, чтобы знать, какую функцию обнаружения столкновений вызывать. Даже двойная отправка (которая логически ничем не отличается от проверки типа) может здесь помочь *.

На практике в этой ситуации физические движки, которые я видел (Bullet и Havok), основаны на тестировании типов той или иной формы.

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

* Технически это можно использовать двойную диспетчеризацию в ужасающем и сложном пути , который требуется N (N + 1) / 2 комбинации (где N является количеством типов формы у вас есть) , и будет только запутать , что вы действительно делаете , который одновременно обнаруживает типы двух фигур, поэтому я не считаю это реалистичным решением.

Мартин
источник
1

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

Например, вы хотите нарисовать некоторые объекты, но не хотите добавлять код рисования непосредственно к ним (что имеет смысл). На языках, не поддерживающих множественную диспетчеризацию, вы можете получить следующий код:

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

Это становится проблематичным, когда этот код появляется в нескольких местах, и вам необходимо изменить его везде при добавлении нового типа сущности. Если это так, то этого можно избежать, используя шаблон Visitor, но иногда лучше просто упростить задачу, а не переусердствовать. Это те ситуации, когда тестирование типов в порядке.

Хонза Брабек
источник
0

Единственное время, которое я использую, это в сочетании с отражением. Но даже тогда динамическая проверка в основном не жестко запрограммирована для определенного класса (или жестко запрограммирована только для специальных классов, таких как Stringили List).

Под динамической проверкой я имею в виду:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

и не жестко

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}
m3th0dman
источник
0

Типовое тестирование и приведение типов - это две очень тесно связанные концепции. Так тесно связаны, что я уверен, говоря, что вы никогда не должны проводить тестирование типов, если только вы не собираетесь набирать объект на основе результата.

Когда вы думаете об идеальном объектно-ориентированном дизайне, тестирование типов (и приведение) никогда не должно происходить. Но, надеюсь, вы уже поняли, что объектно-ориентированное программирование не идеально. Иногда, особенно с кодом более низкого уровня, код не может оставаться верным идеалу. Так обстоит дело с ArrayLists в Java; поскольку во время выполнения они не знают, какой класс хранится в массиве, они создают Object[]массивы и статически приводят их к правильному типу.

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

Типовое тестирование также часто возникает в рефлексии. Часто у вас будут методы, которые возвращают Object[]или какой-то другой универсальный массив, и вы хотите извлечь все Fooобъекты по любой причине. Это совершенно законное использование типового тестирования и приведения типов.

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

meustrus
источник
1
ArrayListsне знаю, какой класс хранится во время выполнения, потому что у Java не было обобщений, и когда они наконец были представлены, Oracle выбрала обратную совместимость с кодом без обобщений. equalsимеет ту же проблему, и в любом случае это сомнительное дизайнерское решение; Сравнение равенства не имеет смысла для каждого типа.
Довал
1
Технически коллекции Java не сводят свое содержимое ни к чему. Компилятор внедряет типы типов в месте каждого доступа: напримерString x = (String) myListOfStrings.get(0)
В последний раз, когда я смотрел на источник Java (который мог быть 1.6 или 1.5), в источнике ArrayList было явное приведение. Он генерирует (подавленное) предупреждение компилятора по уважительной причине, но в любом случае разрешено. Я полагаю, вы могли бы сказать, что из-за того, как реализованы дженерики, это всегда было Objectдо тех пор, пока к нему не был получен доступ; дженерики в Java обеспечивают только неявное приведение, безопасное по правилам компилятора.
meustrus
0

Это приемлемо в случае, когда вам нужно принять решение, которое включает два типа, и это решение инкапсулировано в объекте вне иерархии этого типа. Например, предположим, что вы планируете, какой объект обрабатывается следующим в списке объектов, ожидающих обработки:

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

Теперь предположим, что наша бизнес-логика буквально «все машины имеют приоритет над лодками и грузовиками». Добавление Priorityсвойства в класс не позволяет вам четко выразить эту бизнес-логику, потому что в итоге вы получите следующее:

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

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

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

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

Однако в действительности алгоритм планирования может измениться в будущем, и в конечном итоге он может зависеть не только от типа. Например, можно сказать, что «грузовые автомобили весом более 5000 кг имеют особый приоритет над всеми другими транспортными средствами». Вот почему алгоритм планирования принадлежит к своему классу, и это хорошая идея, чтобы проверить тип, чтобы определить, какой из них должен идти первым:

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

Это самый простой способ реализации бизнес-логики и все же самый гибкий для будущих изменений.

Скотт Уитлок
источник
Я не согласен с вашим конкретным примером, но согласен с принципом частной ссылки, которая может содержать разные типы с разными значениями. В качестве лучшего примера я бы предложил поле, которое может содержать null, a Stringили a String[]. Если для 99% объектов потребуется ровно одна строка, инкапсуляция каждой строки в отдельно сконструированной String[]может привести к значительным дополнительным затратам памяти. Работа с однострочным регистром с использованием прямой ссылки на a Stringпотребует больше кода, но сэкономит память и может ускорить процесс.
суперкат
0

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

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

Сами классы были очень простыми, просто полезная нагрузка, как и типизированные свойства C #, и процедуры для их маршалинга и демаршаллинга (фактически я сгенерировал большинство классов, используя шаблоны t4 из XML-описания формата сообщения).

Код будет что-то вроде:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

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

Стоит отметить, что C # 7.0 получает сопоставление с образцом (что во многих отношениях является тестированием типа на стероидах), это не может быть все плохо ...

Исак Саво
источник
0

Возьмите общий JSON-анализатор. Результатом успешного анализа является массив, словарь, строка, число, логическое значение или нулевое значение. Это может быть любой из них. И элементы массива или значения в словаре могут снова быть любого из этих типов. Поскольку данные предоставляются извне вашей программы, вы должны принять любой результат (то есть вы должны принять его без сбоев; вы можете отклонить результат, который не соответствует ожидаемому вами).

gnasher729
источник
Что ж, некоторые десериализаторы JSON попытаются создать экземпляр дерева объектов, если в вашей структуре есть информация о типе «полей вывода». Но да Я думаю, что это направление, в котором был направлен ответ Карла Б.
svidgen