Как я могу спорить с Duck-типизацией в строго типизированном языке, таком как Java? - PullRequest
6 голосов
/ 31 марта 2009

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

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

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

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

Ответы [ 11 ]

8 голосов
/ 31 марта 2009

На самом деле, вы сказали это довольно хорошо прямо там.

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

То, как вы знаете , плохая идея в том, что это делает код хрупким: если вы его используете, а иерархия типов меняет , то, вероятно, нарушает этот экземпляр. расчески везде, где это происходит. Более того, вы теряете преимущество сильной печати; компилятор не может помочь вам, отлавливая ошибки заранее. (Это несколько похоже на проблемы, вызванные типами в 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");
6 голосов
/ 31 марта 2009

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

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

3 голосов
/ 31 марта 2009

Ммм ...

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

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

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

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

2 голосов
/ 31 марта 2009

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

Ссылки:

2 голосов
/ 31 марта 2009

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

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

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

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

1 голос
/ 31 марта 2009

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

1 голос
/ 31 марта 2009

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

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

1 голос
/ 31 марта 2009

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

1 голос
/ 31 марта 2009

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

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

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

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

0 голосов
/ 31 марта 2009

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

...