Как я могу выступить против утиной печати на строго типизированном языке, таком как Java?

Я работаю в команде Java-программистов. Один из моих коллег время от времени предлагает мне сделать что-то вроде «просто добавьте поле типа» (обычно «строковый тип»). Или код будет загружен с "if (foo instanceof Foo){...} else if( foo instanceof Bar){...}".

Несмотря на предостережение Джоша Блоха о том, что «помеченные классы являются слабой имитацией надлежащей иерархии классов», каков мой однострочный ответ на подобные вещи? И тогда как мне более серьезно проработать концепцию?

Мне ясно, что - контекст является Java - рассматриваемый тип объекта находится прямо перед нашими коллективными лицами - IOW: слово сразу после «класса», «перечисления» или «интерфейса» и т. Д.

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


person jasonnerothin    schedule 31.03.2009    source источник
comment
Это не утиная печать. Утиная типизация - это просто полиморфизм в динамических языках (неявные интерфейсы). То, о чем вы говорите, - это просто проверка типов (что плохо даже для языков с утиным типом, таких как, скажем, Python).   -  person Devin Jeanpierre    schedule 31.03.2009
comment
@Devin Jeanpierre, @sharptooth, не следует ли кому-то исправить заголовок вопроса, раз уж мы знаем, что это не утиный ввод?   -  person Dan Rosenstark    schedule 11.03.2010


Ответы (11)


Когда вы говорите «утка печатать на строго типизированных языках», вы на самом деле имеете в виду «имитацию (подтипного) полиморфизма в статически типизированных языках».

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

person eljenso    schedule 31.03.2009
comment
бинго. вот к чему я клонил. какие-нибудь мысли о том, где могут сыграть инварианты? даже для объектов данных есть правила. и если бы они были по типам, казалось бы, правила другие. - person jasonnerothin; 08.04.2009

На самом деле, вы тут же сказали это достаточно хорошо.

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

Вы знаете, что это плохая идея: это делает код хрупким: если вы его используете и иерархия типов изменится, то это, вероятно, нарушит этот экземпляр расчесывайте везде, где это встречается. Более того, вы теряете преимущества строгой типизации; компилятор не может помочь вам, заблаговременно обнаруживая ошибки. (Это в некоторой степени аналогично проблемам, вызванным приведением типов в C.)

Обновлять

Позвольте мне немного расширить это, поскольку из комментария кажется, что я не совсем понял. Причина, по которой вы используете приведение типов в C, или instanceof, это то, что вы хотите сказать, как если бы: используйте это foo, как если бы это было bar. Теперь в C вообще нет информации о типе времени выполнения, поэтому вы просто работаете без сети: если вы укажете что-то, сгенерированный код будет обрабатывать этот адрес, как если бы он содержал определенный тип, независимо от того, что , и вам следует только надеяться, что это вызовет ошибку времени выполнения вместо того, чтобы что-то незаметно повредить.

Утиная печать просто поднимает это до нормы; в динамическом, слабо типизированном языке, таком как Ruby, Python или Smalltalk, все является нетипизированной ссылкой; вы стреляете в него сообщениями во время выполнения и смотрите, что происходит. Если он понимает конкретное сообщение, он ходит как утка - он его обрабатывает.

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

В строго типизированном языке, таком как Java, строго говоря, у вас вообще не может быть утиной печати: вы должны указать компилятору, к какому типу вы собираетесь относиться. Вы можете получить что-то вроде утиного набора, используя приведение типов, так что вы можете делать что-то вроде

Object x;   // A reference to an Object, analogous to a void * in C

// Some code that assigns something to x

((FoodDispenser)x).dropPellet();          // [1]

// Some more code

((MissleController)x).launchAt("Moon");   // [2]

Теперь во время выполнения все в порядке, пока x имеет вид FoodDispenser в [1] или MissleController в [2]; в противном случае бум. Или неожиданно нет бум.

В вашем описании вы защищаете себя гребешком из else if и instanceof.

 Object x ;

 // code code code 

 if(x instanceof FoodDispenser)
      ((FoodDispenser)x).dropPellet();
 else if (x instanceof MissleController )
      ((MissleController)x).launchAt("Moon");
 else if ( /* something else...*/ ) // ...
 else  // error

Теперь вы защищены от ошибки времени выполнения, но обязаны сделать что-нибудь разумное позже, на else.

Но теперь представьте, что вы вносите изменения в код, так что «x» может принимать типы «FloorWax» и «DessertTopping». Теперь вы должны пройти весь код, найти все экземпляры этого гребня и изменить их. Теперь код хрупкий - изменения требований означают много изменений кода. В объектно-ориентированном стиле вы стремитесь сделать код менее уязвимым.

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

  public interface VeebleFeetzer { /* ... */ };
  public class FoodDispenser implements VeebleFeetzer { /* ... */ }
  public class MissleController implements VeebleFeetzer { /* ... */ }
  public class FloorWax implements VeebleFeetzer { /* ... */ }
  public class DessertTopping implements VeebleFeetzer { /* ... */ }

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

VeebleFeetzer x;   // A reference to anything 
                   // that implements VeebleFeetzer

// Some code that assigns something to x

x.dropPellet();

// Some more code

x.launchAt("Moon");

 
    
person Charlie Martin    schedule 31.03.2009
comment
Что ж, instanceof делает приведение безопасным, в отличие от C, поэтому в этом случае вы действительно не потеряете помощь компилятора. Однако это не делает это менее плохим способом! - person TofuBeer; 31.03.2009
comment
Другая проблема заключается в том, что добавление класса FloorWax заставляет вас обратиться к целому ряду методов, не связанных с FloorWax, и добавить if (x instanceof FloorWax) в переключатель. - person DJClayworth; 31.03.2009
comment
Кажется, вы используете строгую типизацию как то же самое, что и статическую типизацию. В этом нет ничего необычного, но чаще всего используется проверка строго типизированных типов во время выполнения. s = Год: +2009 # TypeError Слабая типизация похожа на C, вы сами по себе. Это не имеет ничего общего со статической или динамической типизацией. * Java, статическая и строго типизированная. * Ruby & Pyton, динамичный и строго типизированный. * C, статическая и слабая типизация. en.wikipedia.org/wiki/ - person Jonas Elfström; 03.07.2009

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

Если вы постоянно проверяете тип объекта, значит, вы либо слишком умен (хотя я полагаю, что именно этот ум нравится любителям утки, кроме менее хрупкой формы), либо вы не принимаете основы объекта -ориентированное программирование.

person Jim Puls    schedule 31.03.2009
comment
Затем используйте утку, печатая правильно, передав Object *. Использование RTTI делает код менее объектно-ориентированным до такой степени, что это нарушает локальность и делает код хрупким. - person Charlie Martin; 31.03.2009
comment
Я сказал что-то, что противоречит этому? - person Jim Puls; 31.03.2009
comment
Люблю статью RTTI в Википедии: В исходной конструкции C ++ Страуструп не включал информацию о типах времени выполнения, потому что считал, что этот механизм часто используется не по назначению [1]. - person Jim Puls; 31.03.2009

Хм...

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

Когда у кого-то есть желание использовать теги в классе для определения типа, тогда следует, ИМХО, пересмотреть иерархию своего класса, поскольку это явный признак концептуального кровотечения, когда абстрактный класс должен знать детали реализации, которые пытается родительский класс класса. прятаться. Вы используете правильный шаблон? Другими словами, вы пытаетесь принудить к поведению по шаблону, который естественным образом его не поддерживает?

Утиная типизация - это возможность свободно определять тип, в котором метод может принимать любые типы, пока определены необходимые методы в экземпляре параметра. Затем метод будет использовать параметр и вызывать необходимые методы, не беспокоясь о родительстве экземпляра.

Итак, здесь ... вонючий намек, как указал Чарли, заключается в использовании instanceof. Подобно статическим или другим вонючим ключевым словам, всякий раз, когда они появляются, нужно спрашивать: «Правильно ли я здесь делаю?», Не потому, что они унаследованы, но они часто используются для взлома плохого или плохо подходящего объектно-ориентированного дизайна.

person Newtopian    schedule 31.03.2009

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

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

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

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

person TofuBeer    schedule 31.03.2009

Возможно, вы захотите указать своему коллеге на принцип замещения Лискова, один из пяти столпов SOLID.

Ссылки:

person Thomas Lundström    schedule 31.03.2009

Хотя я вообще поклонник языков с утиным типом, таких как python, я вижу вашу проблему с ним в java.

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

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

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

person Phil H    schedule 31.03.2009

Зачем «имитировать иерархию классов» вместо того, чтобы проектировать и использовать ее? Один из методов рефакторинга - замена «переключателей» (связанные if почти такие же) на полиморфизм. Зачем использовать переключатели, если полиморфизм приведет к более чистому коду?

person sharptooth    schedule 31.03.2009

Это не утиная типизация, это просто плохой способ имитировать полиморфизм на языке, который имеет (более или менее) реальный полиморфизм.

person Svante    schedule 31.03.2009

Два аргумента для ответа на заглавный вопрос:

1) Предполагается, что Java "напишет один раз, запустит где угодно", поэтому код, написанный для одной иерархии, не должен генерировать исключения RuntimeExceptions, когда мы где-то меняем среду. (Конечно, из этого правила есть исключения - каламбур.)

2) Java JIT выполняет очень агрессивные оптимизации, основанные на знании того, что данный символ должен быть одного типа и только одного типа. Единственный способ обойти это - применить.

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

person Overflown    schedule 31.03.2009

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

person Tobias    schedule 31.03.2009