Коли проводиться тестування типу?


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дійсність SubTypeAчи є SubTypeB.

Чудовий.

Але ми все ще надаємо is aподібні операції у більшості, якщо не у всіх мовах, що захищають тип. І це говорить про потенційну потребу в явному тестуванні типу.

Отже, в яких ситуаціях, якщо такі є, ми повинні чи повинні проводити явне тестування типу?

Пробачте мою відсутність уваги чи відсутність творчості. Я знаю, що робив це раніше; але, чесно кажучи, так давно я не пам'ятаю, чи добре те, що я зробив! І в останній пам’яті, я не думаю, що я стикався з необхідністю тестувати типи поза моїм ковбойським JavaScript.


1
Читайте про системи висновків і типів
Basile Starynkevitch

4
Варто зазначити, що ні Java, ні C # не мали дженериків у своїй першій версії. Щоб користуватися контейнерами, вам потрібно було передати на об'єкт і з нього.
Doval

6
"Перевірка типу" майже завжди стосується перевірки того, чи дотримується код статичних правил введення мови. Що ви маєте в виду , що зазвичай називають тип тестування .


3
Ось чому мені подобається писати C ++ і залишати RTTI вимкненим. Коли буквально не можна перевіряти типи об’єктів під час виконання, це змушує розробника дотримуватися гарного дизайну OO щодо питання, яке тут задають.

Відповіді:


48

"Ніколи" - це канонічна відповідь на питання "коли тестування типу нормально?" Немає способу довести чи спростувати це; це частина системи переконань про те, що робить "гарний дизайн" або "хороший об'єктно-орієнтований дизайн". Це також хокум.

Щоб бути впевненим, якщо у вас є інтегрований набір класів, а також більше однієї або двох функцій, які потребують такого виду прямого тестування типу, ви, ймовірно, РОБИТИ НЕ ПРАВИЛЬНО. Вам дійсно потрібен метод, який реалізується по-різному в SuperTypeйого підтипах. Це невід'ємна частина об'єктно-орієнтованого програмування, і цілі причини класів та успадкування існують.

У цьому випадку явне тестування типу є помилковим не тому, що тестування типів по своїй суті є неправильним, а тому, що в мові вже є чистий, розширюваний, ідіоматичний спосіб здійснення дискримінації за типом, і ви її не використовували. Натомість ви повернулися до примітивної, тендітної, не розширюваної ідіоми.

Рішення: Використовуйте ідіому. Як ви запропонували, додайте метод до кожного з класів, а потім дозвольте стандартним алгоритмам успадкування та методів вибору методу визначати, який випадок застосовується. Або якщо ви не можете змінити базові типи, підклас та додайте туди свій метод.

Стільки для загальноприйнятої мудрості, і до деяких відповідей. Деякі випадки, коли явне тестування типу має сенс:

  1. Це одноразово. Якщо у вас було багато дискримінації типів, ви можете поширити типи або підклас. Але ти цього не робиш. У вас є лише одне або два місця, де вам потрібно чітке тестування, тому не варто витрачати час на роботу та працювати через ієрархію класів, щоб додати функції як методи. Або не варто докладати практичних зусиль, щоб додати виду загальності, тестування, огляди дизайну, документацію чи інші атрибути базових класів для такого простого, обмеженого використання. У цьому випадку додавання функції, яка робить пряме тестування, є раціональним.

  2. Ви не можете коригувати заняття. Ви думаєте про підкласифікацію - але не можете. Наприклад, призначено багато класів на Java final. Ви намагаєтеся вписати в себе, public class ExtendedSubTypeA extends SubTypeA {...} і компілятор скаже вам, не сумніваючись, що те, що ви робите, неможливо. Вибачте, витонченість та витонченість об'єктно-орієнтованої моделі! Хтось вирішив, що ви не можете поширювати їхні типи! На жаль, багато стандартних бібліотек є final, і створення класів finalє загальним дизайнерським керівництвом. Кінцеве виконання функції - це те, що вам доступно.

    До речі, це не обмежується статично типовими мовами. Динамічна мова Python має ряд базових класів, які під обкладинками, реалізованими на C, дійсно неможливо змінити. Як і Java, яка включає більшість стандартних типів.

  3. Ваш код зовнішній. Ви розвиваєтеся з класами та об'єктами, що надходять із ряду серверів баз даних, програмного забезпечення середнього програмного забезпечення та інших баз кодів, якими ви не можете керувати чи коригувати. Ваш код - це лише низький споживач об'єктів, що генеруються в інших місцях. Навіть якщо ви могли б мати підклас SuperType, ви не зможете отримати ті бібліотеки, від яких залежать, щоб генерувати об'єкти у ваших підкласах. Вони передадуть вам екземпляри знаних ними типів, а не ваші варіанти. Це не завжди так ... іноді вони побудовані для гнучкості, і вони динамічно створюють екземпляри класів, якими ви їх годуєте. Або вони надають механізм для реєстрації підкласів, на яких ви хочете побудувати їх фабрики. XML-аналізатори здаються особливо хорошими в наданні таких точок входу; див або lxml в Python . Але більшість кодових баз не мають таких розширень. Вони повернуть вам назад класи, з яких вони були побудовані та про яких знають. Як правило, не має сенсу проксифікувати їх результати у ваші власні результати, щоб ви могли використовувати суто об'єктно-орієнтований селектор типу. Якщо ви збираєтесь робити дискримінацію типу, вам доведеться робити це досить грубо. Ваш код тестування типу виглядає цілком доречним.

  4. Загальні дані / багаторазові відправлення. Ви хочете прийняти до свого коду різноманітні типи і вважаєте, що мати масив дуже специфічних методів не є вигідним. public void add(Object x)здається логічним, але не масив addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, і addStringваріантів (щоб назвати кілька). Функції чи методи, які приймають високий супер-тип, а потім визначають, що робити за типом, - вони не збираються виграти нагороду чистоти на щорічному симпозіумі Booch-Liskov Design, але відмовляться від Угорське іменування надасть вам простіший API. У певному сенсі, ваш is-aабоis-instance-of тестування моделює загальну чи мульти-диспетчеризацію в мовному контексті, який не підтримує її.

    Вбудована підтримка мови як для дженериків, так і для набору тексту качок зменшує необхідність перевірки типу, змушуючи "зробити щось витончене та відповідне" більш імовірним. Множинна диспетчеризація Вибір / інтерфейс бачив в таких мовах , як Джулії і Go так само замінити пряме тестування типу з вбудованими механізмами вибору типу на основі «що робити». Але не всі мови підтримують це. Наприклад, Java, як правило, одноразове, і її ідіоми не дуже зручні для набору качок.

    Але навіть з усіма цими особливостями дискримінації типу - успадкування, генерики, набору качок та багаторазового відправлення - іноді просто зручно мати єдиний, консолідований розпорядок, який робить факт, що ви робите щось залежно від типу об'єкта чіткий і негайний. У метапрограмуванні я вважав, що це суттєво неминуче. Від того, чи повернеться до запитів прямого типу, це "прагматизм у дії" або "брудне кодування", буде залежати від вашої філософії дизайну та переконань.


Якщо операція не може бути виконана на певному типі, але може бути виконана на двох різних типах, до яких це було б неявно конвертоване, перевантаження доцільне, лише якщо обидві конверсії дадуть однакові результати. Інакше в кінцевому підсумку виникає кримінальна поведінка, як у .NET: (1.0).Equals(1.0f)видається істина [аргумент сприяє double], але (1.0f).Equals(1.0)поступається помилковою [аргумент сприяє object]; у Java Math.round(123456789*1.0)дає 123456789, але Math.round(123456789*1.0)виходить 123456792 [аргумент сприяє, floatа не double].
supercat

Це класичний аргумент проти автоматичного лиття / ескалації типу. Погані та парадоксальні результати, принаймні у крайніх випадках. Я згоден, але не впевнений, як ви маєте намір це стосуватися моєї відповіді.
Джонатан Юніс

Я відповідав на ваш пункт №4, який, здавалося б, виступає за перевантаження, а не для використання методів різного типу з різними типами.
supercat

2
@supercat Винен у мене поганий зір, але два вирази з Math.roundвиглядом однакові для мене. Яка різниця?
Лілі Чунг

2
@IstvanChung: Ooops ... останнє мало бути Math.round(123456789)[показом того, що може статися, якщо хтось переписує, Math.round(thing.getPosition() * COUNTS_PER_MIL)щоб повернути незазначене значення позиції, не усвідомлюючи, що getPositionповертає intабо long.]
supercat

25

Основна ситуація, яка мені колись була потрібна, полягала в порівнянні двох об'єктів, наприклад, у equals(other)методі, який може вимагати різних алгоритмів залежно від точного типу other. Навіть тоді це досить рідко.

Інша ситуація, з якою я стикався, знову дуже рідко, - це після деріаріалізації чи розбору, де вам іноді потрібна безпечна передача на більш конкретний тип.

Крім того, іноді вам просто потрібен хак, щоб обійти сторонні код, який ви не контролюєте. Це одна з тих речей, якими ви насправді не хочете користуватися регулярно, але раді, що є там, коли вам це справді потрібно.


1
Я на мить був у захваті від випадку десеріалізації, помилково пам’ятаючи, що його там використовували; але тепер я не уявляю, як би у мене! Я знаю, що для цього я робив кілька дивних пошукових даних; але я не впевнений, чи є це тестуванням типу . Можливо, серіалізація є ближчою паралеллю: допит предметів за їх конкретним типом.
svidgen

1
Серіалізація, як правило, можлива за допомогою поліморфізму. Під час десеріалізації ви часто робите щось на кшталт BaseClass base = deserialize(input), оскільки ви ще не знаєте типу, то if (base instanceof Derived) derived = (Derived)baseзберігаєте його як його точний похідний тип.
Карл Білефельдт

1
Рівність має сенс. На мій досвід, такі методи часто мають форму на зразок "якщо ці два об'єкти мають один і той же конкретний тип, поверніть, чи всі їхні поля рівні; в іншому випадку поверніть помилкове (або непорівнянне) ".
Джон Перді,

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: це далеко не надумане, я фактично зіткнувся з цією ситуацією сьогодні (хоча я не можу розміщувати цей приклад тут, оскільки він містить внутрішній бізнес). І я погоджуюся з іншими відповідями, що використання оператора "є" має бути винятком і робити це лише у рідкісних випадках.
Doc Brown

Я думаю, що ваша редакція, яку я не помітив, дає хороший приклад. ... Є випадки , де це нормально , навіть якщо ви дійсно контролювати , 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ідентифікатор id і викликає його в офіс :) Функція getEmployeeById()повертає супер-тип Employee, але я хочу перевірити, чи повертається менеджер у цьому випадку використання.

Наприклад:

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

Тут я перевіряю, чи дійсно працівник, який повернувся за запитом, насправді є менеджером (тобто я очікую, що він буде менеджером, а якщо в іншому випадку - невдало).

Не найкращий приклад, але все-таки це дядько Боб.

Оновлення

Я оновив приклад настільки, наскільки пам'ятаю з пам'яті.


1
Чому Managerреалізація summon()просто не кидає виняток у цьому прикладі?
svidgen

@svidgen, можливо, CEOможе викликати Managers.
користувач253751

@svidgen, Тоді було б не так зрозуміло, що очікується повернення менеджера
Ian

@Ian Я не вважаю це проблемою. Якщо викликовий код запитує запит на запит Employee, він повинен дбати лише про те, щоб він отримав щось, що поводиться як an Employee. Якщо різні підкласи Employeeмають різні дозволи, обов'язки тощо, що робить тестування типу кращим варіантом, ніж реальна система дозволів?
svidgen

3

Коли перевірка типу в порядку?

Ніколи.

  1. Маючи поведінку, пов’язану з цим типом, в якійсь іншій функції, ви порушуєте принцип відкритого закритого типу , оскільки ви можете змінювати існуючу поведінку типу шляхом зміни isпункту, або (на деяких мовах або залежно від сценарій), оскільки ви не можете розширити тип, не змінюючи внутрішні функції функції, що робить isперевірку.
  2. Що ще важливіше, isчеки є сильним ознакою того, що ви порушуєте Принцип заміни Ліскова . Все, що працює, SuperTypeповинно бути повністю обізнане про те, які існують підтипи.
  3. Ви неявно пов’язуєте деяку поведінку з назвою типу. Це ускладнює підтримку вашого коду, оскільки ці неявні контракти поширюються по всьому коду і не гарантується, що вони будуть універсально та послідовно виконуватись, як фактичні члени класу.

Все, що сказано, isперевірки можуть бути менш поганими, ніж інші альтернативи. Внесення всіх загальних функціональних можливостей до базового класу є важкою рукою і часто призводить до гірших проблем. Використання одного класу, який містить прапор чи перерахунок для того, що "набрати" екземпляр, є гіршим, ніж жахливим, оскільки ви зараз поширюєте обхід системи типу для всіх споживачів.

Коротше кажучи, ви завжди повинні вважати перевірку типу сильним запахом коду. Але, як і в усіх рекомендаціях, буде час, коли ви змушені будете вибирати, яке порушення правил є найменш образливим.


3
Є одне незначне застереження: реалізація алгебраїчних типів даних мовами, які їх не підтримують. Однак використання успадкування та перевірка типу тексту - суто хакітна деталь реалізації; наміром є не введення підтипів, а категоризація значень. Я викладаю це лише тому, що ADT корисні і ніколи не є дуже сильним класифікатором, але в іншому випадку я повністю згоден; instanceofдеталі впровадження протікає та порушує абстракцію.
Doval

17
"Ніколи" - це слово, яке мені не дуже подобається в такому контексті, особливо коли воно суперечить тому, що ви пишете нижче.
Док Браун

10
Якщо під «ніколи» ви насправді маєте на увазі «іноді», то ви маєте рацію.
Калеб

2
ОК означає допустиме, прийнятне тощо, але не обов'язково ідеальне чи оптимальне. На противагу цьому не все гаразд : якщо щось не в порядку, то не слід цього робити взагалі. Як ви вказуєте, необхідність перевірити тип чогось може бути вказівкою на більш глибокі проблеми в коді, але бувають випадки, коли це найбільш доцільний, найменш поганий варіант, і в таких ситуаціях, очевидно, нормально використовувати інструменти у себе розпорядження. (Якби їх було легко уникнути у будь-яких ситуаціях, вони, мабуть, не були б там в першу чергу.) Питання зводиться до виявлення цих ситуацій і ніколи не допомагає.
Калеб

2
@supercat: Методи повинні вимагати найменш можливого типу, який відповідає її вимогам . IEnumerable<T>не обіцяє існування "останнього" елемента. Якщо ваш метод потребує такого елемента, він повинен вимагати типу, який гарантує існування одного. І тоді підтипи цього типу можуть забезпечити ефективну реалізацію методу "Останній".
cHao

2

Якщо у вас є велика база коду (понад 100 К рядків коду) і ви близькі до доставки або працюєте у відділенні, яке пізніше доведеться об'єднати, і тому існує велика вартість / ризик змінити багато справлятися.

Тоді іноді у вас є можливість великого рефрактора системи або просте локальне тестування типу. Це створює технічну заборгованість, яку слід погасити якомога швидше, але часто це не так.

(Прикладу неможливо придумати, оскільки будь-який код, який є досить малим, щоб використовуватись як приклад, також є досить малим, щоб краща конструкція була добре видно.)

Або іншими словами, коли метою є виплата вашої заробітної плати, а не отримання «голосів» за чистоту вашого дизайну.


Інший поширений випадок - це код інтерфейсу, коли ви, наприклад, показуєте інший інтерфейс користувача для деяких типів співробітників, але очевидно, що ви не хочете, щоб концепції інтерфейсу використовувались у всіх ваших класах «домену».

Ви можете використовувати "тестування типу", щоб визначити, яку версію інтерфейсу відображати, або мати якусь фантазійну таблицю пошуку, яка перетворює з "класів домену" в "класи інтерфейсу". Таблиця пошуку - це лише спосіб приховати "тестування типу" в одному місці.

(Код оновлення бази даних може мати ті ж проблеми, що і код UI, проте у вас є лише один набір коду оновлення бази даних, але у вас може бути безліч різних екранів, які повинні адаптуватися до типу об’єкта, що відображається.)


Шаблон відвідувачів часто є хорошим способом вирішити ваш інтерфейс користувача.
Ян Голдбі

@IanGoldby, погоджений часом, це може бути, проте ви все ще робите "тестування типу", просто трохи приховані.
Ян

Приховано в тому ж сенсі, що це приховано, коли ви викликаєте звичайний віртуальний метод? Або ти маєш на увазі щось інше? Використовувана вами схема відвідувачів не має умовних висловлювань, які залежать від типу. Це все робиться мовою.
Ян Голдбі

@IanGoldby, я мав на увазі прихований сенс, що це може зробити TPF або 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але все ще гарантує, що наявний вміст буде незмінним)).
supercat

За винятком сценаріїв, коли типу може знадобитися вміст однієї з двох абсолютно різних речей у різний час, а вимоги явно взаємно виключають (і, таким чином, використання окремих полів було б зайвим), я, як правило, вважаю, що потреба у спробі кастингу є знаком що члени, які повинні були бути частиною базового інтерфейсу, не були. Це не означає, що код, який використовує інтерфейс, який мав би включати деяких членів, але не повинен використовувати пробний кастинг для усунення пропусків базового інтерфейсу, але такі інтерфейси, що записують базовий, повинні мінімізувати потребу клієнтів у спробі набору.
supercat

1

Існує приклад, який часто зустрічається в розробці ігор, зокрема при виявленні зіткнень, з якими важко розібратися без використання якоїсь форми тестування типу.

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

Тепер, щоб перевірити на зіткнення, мені потрібно написати функцію для кожної пари типів форми зіткнення:

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

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

При кожному "галочці" мого ігрового циклу мені потрібно перевірити пари предметів на предмет зіткнень. Але я маю доступ лише до GameObjects та відповідних CollisionShapes. Ясно, що я повинен знати конкретні типи, щоб знати, яку функцію виявлення зіткнень викликати. Навіть подвійна відправка (що логічно нічим не відрізняється від перевірки на тип) не може допомогти тут *.

На практиці в цій ситуації фізичні двигуни, які я бачив (Куля та Хавок), покладаються на типове тестування тієї чи іншої форми.

Я не кажу, що це обов'язково хороше рішення, це просто найкраще з невеликої кількості можливих рішень цієї проблеми

* Технічно це можна використовувати подвійну диспетчеризацію в жахливому і складним чином , що потребують 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));
    } ...
}

Це стає проблематичним, коли цей код з’являється в декількох місцях, і вам потрібно його скрізь змінювати, додаючи новий тип Entity. Якщо це так, то цього можна уникнути за допомогою шаблону відвідувачів, але іноді краще просто просто робити речі, а не перенапружувати їх. Це такі ситуації, коли тестування типу нормально.


0

Єдиний раз, який я використовую, це поєднання з рефлексією. Але навіть тоді є динамічна перевірка здебільшого, а не жорсткого кодування до конкретного класу (або лише жорсткого коду до спеціальних класів, таких як Stringабо List).

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

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

    }
}

і не жорстко закодовані

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

    }
}

0

Тестування типу та лиття типів - дві дуже тісно пов’язані між собою концепції. Настільки тісно пов’язані між собою, що я впевнено кажу, що ніколи не слід робити тест на тип, якщо ваш намір не вводити об'єкт на основі результату.

Якщо ви думаєте про ідеальний об'єктно-орієнтований дизайн, тестування типу (і лиття) ніколи не повинно відбуватися. Але, сподіваємось, ви вже зрозуміли, що об'єктно-орієнтоване програмування не є ідеальним. Іноді, особливо з кодом нижчого рівня, код не може залишатися вірним ідеалу. Це справа з ArrayLists на Java; оскільки вони не знають під час виконання, який клас зберігається в масиві, вони створюють Object[]масиви та статично приводять їх до правильного типу.

Було зазначено, що загальна потреба вводити тестування (і кастинг типів) походить від Equalsметоду, який у більшості мов повинен сприймати просто Object. У впровадженні повинні бути проведені детальні перевірки, щоб два об'єкти були одного типу, що вимагає можливості перевірити, який вони тип.

Тестування типу також часто зустрічається у відображенні. Часто у вас будуть методи, які повертаються Object[]або якийсь інший загальний масив, і ви хочете витягнути всі Fooоб'єкти з будь-якої причини. Це цілком легальне використання типового тестування та кастингу.

Взагалі тестування типу погано, коли воно непотрібно з'єднує ваш код із тим, як була написана конкретна реалізація. Це може легко призвести до необхідності проведення певного тесту для кожного типу або комбінації типів, наприклад, якщо ви хочете знайти перетин ліній, прямокутників та кіл, а функція перетину має різний алгоритм для кожної комбінації. Ваша мета - розмістити будь-які деталі, характерні для одного виду об’єктів, в тому ж місці, що і цей об’єкт, оскільки це полегшить підтримку та розширення вашого коду.


1
ArrayListsне знаю, що клас зберігається під час виконання, тому що у Java не було дженериків, а коли їх нарешті було представлено, Oracle обрав зворотну сумісність із кодом без генерики. equalsмає те саме питання, і це все одно сумнівне дизайнерське рішення; Порівняння рівності не має сенсу для кожного типу.
Doval

1
Технічно колекції Java ні на що не кидають свій вміст. Компілятор вводить String x = (String) myListOfStrings.get(0)

Востаннє, коли я переглянув джерело Java (яке, можливо, було 1,6 або 1,5), у явному джерелі ArrayList було чітке відтворення. Він створює (придушене) попередження компілятора з поважних причин, але дозволено все одно. Я гадаю, ви можете сказати, що завдяки тому, як реалізована дженерика, вона завжди була Objectдо тих пір, поки до неї не було доступно; generics на 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[]може призвести до значних накладних витрат. Для обробки однорядного випадку за допомогою прямого посилання на файл Stringзнадобиться більше коду, але це дозволить заощадити сховище та може зробити це швидше.
supercat

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. Результатом успішного розбору є масив, словник, рядок, число, булеве або нульове значення. Це може бути будь-який із них. І елементи масиву або значення в словнику можуть знову бути будь-якими з цих типів. Оскільки дані надаються за межами вашої програми, ви повинні прийняти будь-який результат (тобто ви повинні прийняти його без збоїв; ви можете відхилити результат, який не є таким, який ви очікуєте).


Добре, деякі десеріалізатори JSON спробують інстанціювати об'єктне дерево, якщо у вашій структурі є інформація про тип для "полів виводу" в ньому. Але так. Я думаю, що саме в цьому напрямку була спрямована відповідь Карла Б.
svidgen
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.