Предполагая язык с некоторой присущей безопасности типов (например, не 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.
источник
Ответы:
«Никогда» - это канонический ответ «когда все в порядке с тестированием типов?» Там нет никакого способа доказать или опровергнуть это; это часть системы убеждений о том, что делает «хороший дизайн» или «хороший объектно-ориентированный дизайн». Это тоже хокум.
Безусловно, если у вас есть интегрированный набор классов, а также более одной или двух функций, которые требуют такого рода прямого тестирования типов, вы, вероятно, ДЕЛАЕТЕ ЭТО НЕПРАВИЛЬНО. Что вам действительно нужно, так это метод, реализованный по-другому
SuperType
и его подтипы. Это неотъемлемая часть объектно-ориентированного программирования, и существуют целые классы разума и наследования.В этом случае явное тестирование типов является неправильным не потому, что тестирование типов по своей сути неверно, а потому, что в языке уже есть чистый, расширяемый идиоматический способ выполнения распознавания типов, а вы его не использовали. Вместо этого вы вернулись к примитивной, хрупкой, нерасширяемой идиоме.
Решение: используйте идиому. Как вы предложили, добавьте метод к каждому из классов, а затем позвольте стандартным алгоритмам наследования и выбора метода определить, какой случай применим. Или, если вы не можете изменить базовые типы, создайте подкласс и добавьте туда свой метод.
Так много для обычной мудрости, и на некоторые ответы. Некоторые случаи, когда имеет смысл явное типовое тестирование:
Это разовое. Если вам нужно было много различать типы, вы можете расширить типы или подкласс. Но ты не. У вас есть только одно или два места, где вам нужно явное тестирование, поэтому вам не стоит возвращаться и работать с иерархией классов, чтобы добавить функции в качестве методов. Или же это не стоит практических усилий, чтобы добавить такие общие, тестовые, проектные обзоры, документацию или другие атрибуты базовых классов для такого простого, ограниченного использования. В этом случае добавление функции, выполняющей прямое тестирование, является рациональным.
Вы не можете настроить классы. Вы думаете о подклассе - но вы не можете. Например, обозначены многие классы в Java
final
. Вы пытаетесь добавить a,public class ExtendedSubTypeA extends SubTypeA {...}
и компилятор недвусмысленно говорит вам, что то, что вы делаете, невозможно. Извините, изящество и изощренность объектно-ориентированной модели! Кто-то решил, что вы не можете расширить их типы! К сожалению, многие из стандартных библиотек естьfinal
, и создание классовfinal
является общим руководством по проектированию. Функция end-run - это то, что вам доступно.Кстати, это не ограничивается статически типизированными языками. Динамический язык В Python есть ряд базовых классов, которые под оболочками, реализованными в C, не могут быть изменены. Как и Java, он включает в себя большинство стандартных типов.
Ваш код является внешним. Вы разрабатываете с классами и объектами, которые поступают из ряда серверов баз данных, механизмов промежуточного программного обеспечения и других кодовых баз, которые вы не можете контролировать или настраивать. Ваш код - просто непритязательный потребитель объектов, сгенерированных в другом месте. Даже если бы вы могли
SuperType
создавать подклассы , вы не сможете получить те библиотеки, от которых зависит создание объектов в ваших подклассах. Они передадут вам экземпляры известных им типов, а не ваши варианты. Это не всегда так ... иногда они созданы для гибкости и динамически создают экземпляры классов, которые вы им кормите. Или они предоставляют механизм для регистрации подклассов, которые вы хотите построить на своих фабриках. Парсеры XML кажутся особенно хорошими в предоставлении таких точек входа; см. например или lxml в Python . Но большинство кодовых баз не предоставляют таких расширений. Они вернут вам классы, с которыми они были построены, и знают о них. Как правило, не имеет смысла передавать их результаты в ваши пользовательские результаты, чтобы вы могли использовать чисто объектно-ориентированный селектор типов. Если вы собираетесь проводить различение типов, вам придется делать это относительно грубо. Ваш код тестирования типов выглядит вполне уместно.Дженерики бедного человека / многократная отправка. Вы хотите принимать различные типы кода в свой код и чувствовать, что наличие массива методов, очень специфичных для типа, не изящно.
public void add(Object x)
кажется логичным, но не массивaddByte
,addShort
,addInt
,addLong
,addFloat
,addDouble
,addBoolean
,addChar
, иaddString
варианты (чтобы назвать несколько). Имея функции или методы, которые берут верхний супертип, а затем определяют, что делать в зависимости от типа, они не получат награду за чистоту на ежегодном Симпозиуме им. Буча-Лискова, а отбросят Венгерское наименование даст вам более простой API. В некотором смысле, вашis-a
илиis-instance-of
тестирование - это моделирование универсального или мультидиспетчерского в языковом контексте, который изначально не поддерживает его.Встроенная языковая поддержка как универсальных, так и утиных наборов сокращает необходимость проверки типов, повышая вероятность «сделать что-то изящное и подходящее». Множественная диспетчеризация Выбор / интерфейс видел в таких языках , как Юлю и Go так же заменить прямое тестирование типа со встроенными механизмами выбора типа на основе «что делать». Но не все языки поддерживают это. Например, Java, как правило, с одной диспетчеризацией, и его идиомы не являются супер-дружественными для утки.
Но даже со всеми этими функциями различения типов - наследование, обобщенные типы, типирование утки и многократная диспетчеризация - иногда просто удобно иметь единую консолидированную процедуру, которая делает тот факт, что вы делаете что-то, основываясь на типе объекта ясно и сразу. В метапрограммировании я нашел это по существу неизбежным. Является ли возврат к прямым запросам типа «прагматизмом в действии» или «грязным кодированием», будет зависеть от вашей философии дизайна и убеждений.
источник
(1.0).Equals(1.0f)
выдача true [аргумент способствуетdouble
], но(1.0f).Equals(1.0)
выдача false [аргумент способствуетobject
]; в JavaMath.round(123456789*1.0)
выдает 123456789, ноMath.round(123456789*1.0)
выдает 123456792 [аргументfloat
скорее повышает , чемdouble
].Math.round
выглядят одинаково для меня. Какая разница?Math.round(123456789)
[показывать, что может произойти, если кто-то переписывает,Math.round(thing.getPosition() * COUNTS_PER_MIL)
чтобы вернуть немасштабированное значение позиции, не понимая, чтоgetPosition
возвращаетint
илиlong
.]Основная ситуация, в которой я когда-либо нуждался, заключалась в сравнении двух объектов, например, в
equals(other)
методе, для которого могут потребоваться разные алгоритмы в зависимости от точного типаother
. Даже тогда это довольно редко.Другая ситуация, с которой мне приходилось сталкиваться, опять же очень редко, возникает после десериализации или анализа, когда вам иногда это нужно для безопасного приведения к более конкретному типу.
Кроме того, иногда вам просто нужен взлом, чтобы обойти сторонний код, который вы не контролируете. Это одна из тех вещей, которую вы не хотите использовать на регулярной основе, но рады, что она есть, когда она вам действительно нужна.
источник
BaseClass base = deserialize(input)
, потому что вы еще не знаете тип, а затемif (base instanceof Derived) derived = (Derived)base
сохраняете его как его точный производный тип.Стандартный (но, надеюсь, редкий) случай выглядит так: если в следующей ситуации
функции
DoSomethingA
илиDoSomethingB
не могут быть легко реализованы как функции-члены дерева наследованияSuperType
/SubTypeA
/SubTypeB
. Например, еслиDoSomethingXXX
этой библиотеки будет означать введение запрещенной зависимости.Обратите внимание часто бывают ситуации , когда вы можете обойти эту проблему (например, путем создания обертки или адаптер для
SubTypeA
иSubTypeB
или пытается повторно реализоватьDoSomething
укомплектовать с точки зрения основных операцийSuperType
), но иногда эти решения не стоит хлопот или сделать вещи более сложные и менее расширяемые, чем выполнение явного теста типов.Пример из моей вчерашней работы: у меня была ситуация, когда я собирался распараллелить обработку списка объектов (типа
SuperType
, точно с двумя разными подтипами, где крайне маловероятно, что их когда-либо будет больше). Непараллельная версия содержала два цикла: один цикл для объектов подтипа A, вызывающийDoSomethingA
, и второй цикл для объектов подтипа B, вызывающийDoSomethingB
.Методы «DoSomethingA» и «DoSomethingB» - это интенсивные вычисления, использующие контекстную информацию, которая недоступна в области действия подтипов A и B. (поэтому нет смысла реализовывать их как функции-члены подтипов). С точки зрения нового «параллельного цикла», это делает вещи намного проще, работая с ними единообразно, поэтому я реализовал функцию, аналогичную
DoSomethingTo
описанной выше. Однако изучение реализаций «DoSomethingA» и «DoSomethingB» показывает, что внутри они работают по-разному. Таким образом, попытка реализовать универсальное «DoSomething» путем расширенияSuperType
с помощью множества абстрактных методов на самом деле не сработает или будет означать полное перепроектирование вещей.источник
SuperType
и это подклассы?NSJSONSerialization
в Obj-C), но вы не хотите просто полагать, что ответ содержит ожидаемый вами тип, поэтому перед его использованием вы проверяете его (напримерif ([theResponse isKindOfClass:[NSArray class]])...
) ,Как называет это дядя Боб:
В одном из своих эпизодов чистого кодера он привел пример вызова функции, который используется для возврата
Employee
s.Manager
это подтипEmployee
. Давайте предположим, что у нас есть служба приложений, которая принимаетManager
идентификатор и вызывает его в офис :) ФункцияgetEmployeeById()
возвращает супертипEmployee
, но я хочу проверить, возвращается ли менеджер в этом сценарии использования.Например:
Здесь я проверяю, является ли сотрудник, возвращаемый запросом, действительно менеджером (т.е. я ожидаю, что он будет менеджером, и если в противном случае произойдет сбой быстро).
Не лучший пример, но в конце концов это дядя Боб.
Обновить
Я обновил пример настолько, насколько я помню по памяти.
источник
Manager
реализованаsummon()
просто реализация исключения?CEO
может вызватьManager
s.Employee
, он должен заботиться только о том, чтобы он получал что-то похожее наEmployee
. Если разные подклассыEmployee
имеют разные разрешения, обязанности и т. Д., Что делает тестирование типов лучшим вариантом, чем реальная система разрешений?Никогда.
is
предложение или (в некоторых языках или в зависимости от сценарий), потому что вы не можете расширить тип без изменения внутренних функций функции, выполняющейis
проверку.is
проверки являются сильным признаком того, что вы нарушаете принцип подстановки Лискова . Все, с чем работает,SuperType
должно полностью не знать, какие могут быть подтипы.Все это говорит, что
is
проверки могут быть менее плохими, чем другие альтернативы. Помещение всех базовых функций в базовый класс является сложным и часто приводит к худшим проблемам. Использование одного класса, имеющего флаг или перечисление для «типа» экземпляра ... хуже, чем ужасно, поскольку теперь вы распространяете обход системы типов для всех потребителей.Короче говоря, вы всегда должны рассматривать проверки типов как сильный запах кода. Но, как и во всех руководящих принципах, будут моменты, когда вы будете вынуждены выбирать, какое из них является наименее оскорбительным.
источник
instanceof
утечки деталей реализации и ломает абстракцию.IEnumerable<T>
не обещает, что существует "последний" элемент. Если вашему методу нужен такой элемент, он должен требовать тип, который гарантирует его существование. И тогда подтипы этого типа могут обеспечить эффективную реализацию метода «Last».Если вы получили большую кодовую базу (более 100 тыс. Строк кода) и близки к отгрузке или работаете в филиале, который впоследствии придется объединить, и, следовательно, существует много затрат / рисков, связанных с изменением большого количества кода.
Затем у вас есть возможность выбрать большой рефрактор системы или простое локализованное «тестирование типа». Это создает технический долг, который должен быть возвращен как можно скорее, но часто это не так.
(Невозможно придумать пример, так как любой код, который достаточно мал для использования в качестве примера, также достаточно мал, чтобы лучше был виден лучший дизайн.)
Или, другими словами, когда цель состоит в том, чтобы получать заработную плату, а не набирать голоса за чистоту вашего дизайна.
Другим распространенным случаем является код пользовательского интерфейса, когда, например, вы показываете другой пользовательский интерфейс для некоторых типов сотрудников, но явно не хотите, чтобы концепции пользовательского интерфейса проникали во все ваши «доменные» классы.
Вы можете использовать «тестирование типов», чтобы решить, какую версию пользовательского интерфейса показывать, или иметь какую-то необычную таблицу поиска, которая преобразует «доменные классы» в «классы пользовательского интерфейса». Таблица поиска - это просто способ скрыть «типовое тестирование» в одном месте.
(У кода обновления базы данных могут быть те же проблемы, что и у кода пользовательского интерфейса, однако вы, как правило, имеете только один набор кода обновления базы данных, но у вас может быть множество разных экранов, которые должны адаптироваться к типу отображаемого объекта.)
источник
Реализация LINQ использует множество проверок типов для возможной оптимизации производительности, а затем запасной вариант для IEnumerable.
Наиболее очевидный пример - это, вероятно, метод ElementAt (небольшая выдержка из источника .NET 4.5):
Но в классе Enumerable есть много мест, где используется подобный шаблон.
Поэтому, возможно, оптимизация производительности для часто используемого подтипа является допустимым использованием. Я не уверен, как это могло быть разработано лучше.
источник
IEnumerable<T>
включив в него множество методов, подобных темList<T>
, которые указаны в , вместе соFeatures
свойством, указывающим, какие методы могут работать хорошо, медленно или вообще не работать, а также различные предположения, которые потребитель может безопасно сделать относительно коллекции (например, гарантируется ли ее размер и / или существующее содержимое, которое никогда не изменится [тип может поддерживать,Add
при этом гарантируя, что существующее содержимое будет неизменным]).Есть пример, который часто встречается в игровых разработках, особенно в обнаружении столкновений, с которым трудно справиться без использования какой-либо формы тестирования типов.
Предположим, что все игровые объекты происходят из общего базового класса
GameObject
. Каждый объект имеет жесткую форму столкновения тела ,CollisionShape
которое может обеспечить общий интерфейс (сказать , позицию запроса, ориентацию и т.д.) , но фактические формы столкновения будут все конкретные подклассы , такие какSphere
,Box
,ConvexHull
и т.д. , хранящей информация , относящаяся к этому типу геометрического объекта (см. здесь для реального примера)Теперь, чтобы проверить наличие коллизий, мне нужно написать функцию для каждой пары типов коллизий:
которые содержат конкретную математику, необходимую для пересечения этих двух геометрических типов.
На каждом тике моего игрового цикла мне нужно проверять пары объектов на наличие столкновений. Но у меня есть доступ только к
GameObject
s и соответствующимCollisionShape
s. Ясно, что мне нужно знать конкретные типы, чтобы знать, какую функцию обнаружения столкновений вызывать. Даже двойная отправка (которая логически ничем не отличается от проверки типа) может здесь помочь *.На практике в этой ситуации физические движки, которые я видел (Bullet и Havok), основаны на тестировании типов той или иной формы.
Я не говорю, что это обязательно хорошее решение, просто оно может быть лучшим из небольшого числа возможных решений этой проблемы.
* Технически это можно использовать двойную диспетчеризацию в ужасающем и сложном пути , который требуется N (N + 1) / 2 комбинации (где N является количеством типов формы у вас есть) , и будет только запутать , что вы действительно делаете , который одновременно обнаруживает типы двух фигур, поэтому я не считаю это реалистичным решением.
источник
Иногда вы не хотите добавлять общий метод ко всем классам, потому что это не является их обязанностью выполнить эту конкретную задачу.
Например, вы хотите нарисовать некоторые объекты, но не хотите добавлять код рисования непосредственно к ним (что имеет смысл). На языках, не поддерживающих множественную диспетчеризацию, вы можете получить следующий код:
Это становится проблематичным, когда этот код появляется в нескольких местах, и вам необходимо изменить его везде при добавлении нового типа сущности. Если это так, то этого можно избежать, используя шаблон Visitor, но иногда лучше просто упростить задачу, а не переусердствовать. Это те ситуации, когда тестирование типов в порядке.
источник
Единственное время, которое я использую, это в сочетании с отражением. Но даже тогда динамическая проверка в основном не жестко запрограммирована для определенного класса (или жестко запрограммирована только для специальных классов, таких как
String
илиList
).Под динамической проверкой я имею в виду:
и не жестко
источник
Типовое тестирование и приведение типов - это две очень тесно связанные концепции. Так тесно связаны, что я уверен, говоря, что вы никогда не должны проводить тестирование типов, если только вы не собираетесь набирать объект на основе результата.
Когда вы думаете об идеальном объектно-ориентированном дизайне, тестирование типов (и приведение) никогда не должно происходить. Но, надеюсь, вы уже поняли, что объектно-ориентированное программирование не идеально. Иногда, особенно с кодом более низкого уровня, код не может оставаться верным идеалу. Так обстоит дело с ArrayLists в Java; поскольку во время выполнения они не знают, какой класс хранится в массиве, они создают
Object[]
массивы и статически приводят их к правильному типу.Было отмечено, что общая потребность в тестировании типов (и приведении типов) исходит от
Equals
метода, который в большинстве языков должен быть простымObject
. Реализация должна иметь несколько подробных проверок, если два объекта имеют одинаковый тип, что требует возможности проверить, какого они типа.Типовое тестирование также часто возникает в рефлексии. Часто у вас будут методы, которые возвращают
Object[]
или какой-то другой универсальный массив, и вы хотите извлечь всеFoo
объекты по любой причине. Это совершенно законное использование типового тестирования и приведения типов.В общем, типовое тестирование плохо, когда оно без необходимости связывает ваш код с тем, как была написана конкретная реализация. Это может легко привести к необходимости конкретного теста для каждого типа или комбинации типов, например, если вы хотите найти пересечение линий, прямоугольников и окружностей, а функция пересечения имеет свой алгоритм для каждой комбинации. Ваша цель - поместить любые детали, относящиеся к одному виду объекта, в то же место, что и этот объект, потому что это облегчит поддержку и расширение вашего кода.
источник
ArrayLists
не знаю, какой класс хранится во время выполнения, потому что у Java не было обобщений, и когда они наконец были представлены, Oracle выбрала обратную совместимость с кодом без обобщений.equals
имеет ту же проблему, и в любом случае это сомнительное дизайнерское решение; Сравнение равенства не имеет смысла для каждого типа.String x = (String) myListOfStrings.get(0)
Object
до тех пор, пока к нему не был получен доступ; дженерики в Java обеспечивают только неявное приведение, безопасное по правилам компилятора.Это приемлемо в случае, когда вам нужно принять решение, которое включает два типа, и это решение инкапсулировано в объекте вне иерархии этого типа. Например, предположим, что вы планируете, какой объект обрабатывается следующим в списке объектов, ожидающих обработки:
Теперь предположим, что наша бизнес-логика буквально «все машины имеют приоритет над лодками и грузовиками». Добавление
Priority
свойства в класс не позволяет вам четко выразить эту бизнес-логику, потому что в итоге вы получите следующее:Проблема в том, что теперь, чтобы понять порядок приоритетов, вы должны посмотреть на все подклассы, или другими словами, вы добавили связь с подклассами.
Разумеется, вы должны превратить приоритеты в константы и поместить их в класс самостоятельно, что помогает совместному планированию бизнес-логики:
Однако в действительности алгоритм планирования может измениться в будущем, и в конечном итоге он может зависеть не только от типа. Например, можно сказать, что «грузовые автомобили весом более 5000 кг имеют особый приоритет над всеми другими транспортными средствами». Вот почему алгоритм планирования принадлежит к своему классу, и это хорошая идея, чтобы проверить тип, чтобы определить, какой из них должен идти первым:
Это самый простой способ реализации бизнес-логики и все же самый гибкий для будущих изменений.
источник
null
, aString
или aString[]
. Если для 99% объектов потребуется ровно одна строка, инкапсуляция каждой строки в отдельно сконструированнойString[]
может привести к значительным дополнительным затратам памяти. Работа с однострочным регистром с использованием прямой ссылки на aString
потребует больше кода, но сэкономит память и может ускорить процесс.Типовое тестирование - это инструмент, используйте его с умом и он может стать мощным союзником. Используйте его плохо, и ваш код начнет пахнуть.
В нашем программном обеспечении мы получали сообщения по сети в ответ на запросы. Все десериализованные сообщения имеют общий базовый класс
Message
.Сами классы были очень простыми, просто полезная нагрузка, как и типизированные свойства C #, и процедуры для их маршалинга и демаршаллинга (фактически я сгенерировал большинство классов, используя шаблоны t4 из XML-описания формата сообщения).
Код будет что-то вроде:
Конечно, можно утверждать, что архитектура сообщений могла бы быть лучше спроектирована, но она была разработана давно, а не для C #, так что это именно так. Здесь тестирование типов решило для нас настоящую проблему не слишком потрепанным способом.
Стоит отметить, что C # 7.0 получает сопоставление с образцом (что во многих отношениях является тестированием типа на стероидах), это не может быть все плохо ...
источник
Возьмите общий JSON-анализатор. Результатом успешного анализа является массив, словарь, строка, число, логическое значение или нулевое значение. Это может быть любой из них. И элементы массива или значения в словаре могут снова быть любого из этих типов. Поскольку данные предоставляются извне вашей программы, вы должны принять любой результат (то есть вы должны принять его без сбоев; вы можете отклонить результат, который не соответствует ожидаемому вами).
источник