На написание этого поста меня вдохновила статья "Утиная типизация в Java" (автор Yaroslav Pogrebnyak).
Но с первым абзацем мне стала ясна суть идеи и я решил не дочитывать до конца, а попробовать самому решить эту задачу. После последнего коммита, я прочитал всю статью и отметил те моменты, которые были для меня новыми.
Внимание! Дальше будет много кода, если интересно - кликни меня...
Исходный код можно скачать тут, а все ревизии в SVN репозитории - тут.
Итак, лог разработки про то, как создавался ProxyFactory на TDD:
Ну что, если бы я делал проксик, то наверное хотел бы видеть его API как-то так
А вот и интерфейсы
Как помощники при тестировании, кроме jUnit на благо нового метода трудятся FestAssertions и Maven
Если мы запустим этот тест, то увидим что он не работает
"
По всем правилам TDD простой фикс, который можно сделать чтобы код компилился и работал – это
Тест пройдет
Но давайте протестируем все же поведение – я хочу знать что мой прокси вызовет другой объект а не то, что он вернет что-то...
Мне понадобится Mock-фреймворк Mockito
А вот и сам тест
Естественно он перестанет работать (я исправил старый)
Но небольшой фикс поможет
Идем дальше – меня интересует, что для других типов уток все будет работать!
Вот и тест
Он идентичен прошлому, с той лишь разницей, что в метод object передается объект другого класса...
Тест естественно не пройдет
Для реализации этой штуки мы подготовимся и выделим метод, который будем менять
А потом сделаем фикс
Тесты? Зеленые! Коммитимся
А теперь проверим, что наши чудо утки приводятся к другим интерфейсам тоже
А вот новый тест
Его основное отличие в типе интерфейса к которому приводится объект
И он не работает :)
Но фикс поможет!
Не пугайся, тут я воспользовался одной библиотечкой (1) fest reflect – чтобы не играться с ужасным api java.reflect.*
Так же тут используется прокси-класс (2), который обернет любой интерфейс и перенаправит все вызовы на метод invoke класса ObjectProxy (3)
Тесты проходят, а пока не сильно испортил код – лучше закоммичусь. Сложновасто уже :)
Дальше я хочу проверить как на совместимом интерфейсе вылетит Exception. Вот он интерфейс
А вот и тест.
Странно, но он проходит сразу – не верю!
Немного подумав, я решил, что стоит перенести валидацию не на момент запуска метода, а на момент создания враппера – это все равно runtime, но все же ближе к компиляции, чем изначальный вариант с валидацией при вызове конкретного метода.
Вот, это уже другой разговор!
Пошли его фиксить
Все, коммитимся
Дальше я хочу проверить, кейс, когда два метода в интерфейсе, а потому задам некого монстра, который может и квакать и мукать... И сходный с ним интерфейс. Внимание тут нет связи между классом и интерфейсом – по правилам java их с первого взгляда идентичные методы – разные!
А вот и тест, ничего нового, квакнули на прокси, проверили что монстр квакнул, мукнули на прокси, проверили что монстр мукнул...
Странно, но и это работает. Не верю!
А потому поломаю, добавив временный код. Увижу, что слетело и успокоюсь...
Ну ладно, проверю на всякий еще что-то...
Теперь можно сделать как было и проверить, что все работает
Коммитимся!
Но теперь давай сделаем другого монстра, который умеет еще и гавкать
Проверим что к его типу нельзя привести простого монстра...
Блин, опять все работает!
Не верю! А потому заремарю код проверяющий это и успокоюсь, когда увижу, что код слетел как я ожидал
Потом все верну как было, еще раз запущу и закоммичу, если зеленое
А теперь давай проверим кейсы с методами с параметрами
Вот и тест
Ну хоть тут тест слетел! :)
А фикс достаточно простой
И все! :) Зеленое – коммитимся!
Монстр методы которого возвращают другой тип чем в исходном, оборачиваемом классе
Вот и тест
Ура! Это еще не реализовано!
А потому реализуем
И все работает! А потому коммитимся!
Дальше поиграемся с примитивами
Все должно быть в шоколаде
Так и есть. Но надо перепроверить!
Добавим немного тестового кода и проверим что тест поломался
А после Ctrl-Z все работает
Коммит!
Дальше потестим, что примитивы и их обертки не распознаются как одни и те же типы (как было со String и Object)
Вот тест
Все прошло. Коммитимся (на самом деле я перепроверял, ломая код, просто тут не привел, ибо демонстрировал это уже) .
И еще один кейс – то же, но задом наперед
Вот и тест (так же работает - перепроверял)
Ну и все. Больше пока идей в голову не приходит.... В реальной жизни, когда заюзаю, быть может появятся кейсы...
Хотя нет! Появилась идея. А что, если интерфейсы наследуются друг от друга. Смогу ли я представить объект в виде суперинтерфейса и интерфейса наследника?
А вот и тест.
И он проходит! (я перепроверял, через поломку)
В вот второй тест, который делает то же но для суперинтерфейса
У него суперинтерфейса нет метода bowWof а потому блок заремарен :) (я копипастил)
Все проходит, а потому commit
Ура!
Но с первым абзацем мне стала ясна суть идеи и я решил не дочитывать до конца, а попробовать самому решить эту задачу. После последнего коммита, я прочитал всю статью и отметил те моменты, которые были для меня новыми.
Внимание! Дальше будет много кода, если интересно - кликни меня...
Исходный код можно скачать тут, а все ревизии в SVN репозитории - тут.
Итак, лог разработки про то, как создавался ProxyFactory на TDD:
Ну что, если бы я делал проксик, то наверное хотел бы видеть его API как-то так
А вот и интерфейсы
Как помощники при тестировании, кроме jUnit на благо нового метода трудятся FestAssertions и Maven
Если мы запустим этот тест, то увидим что он не работает
"
По всем правилам TDD простой фикс, который можно сделать чтобы код компилился и работал – это
Тест пройдет
Но давайте протестируем все же поведение – я хочу знать что мой прокси вызовет другой объект а не то, что он вернет что-то...
Мне понадобится Mock-фреймворк Mockito
А вот и сам тест
Естественно он перестанет работать (я исправил старый)
Но небольшой фикс поможет
Идем дальше – меня интересует, что для других типов уток все будет работать!
Вот и тест
Он идентичен прошлому, с той лишь разницей, что в метод object передается объект другого класса...
Тест естественно не пройдет
Для реализации этой штуки мы подготовимся и выделим метод, который будем менять
А потом сделаем фикс
Тесты? Зеленые! Коммитимся
А теперь проверим, что наши чудо утки приводятся к другим интерфейсам тоже
А вот новый тест
Его основное отличие в типе интерфейса к которому приводится объект
И он не работает :)
Но фикс поможет!
Не пугайся, тут я воспользовался одной библиотечкой (1) fest reflect – чтобы не играться с ужасным api java.reflect.*
Так же тут используется прокси-класс (2), который обернет любой интерфейс и перенаправит все вызовы на метод invoke класса ObjectProxy (3)
Тесты проходят, а пока не сильно испортил код – лучше закоммичусь. Сложновасто уже :)
Дальше я хочу проверить как на совместимом интерфейсе вылетит Exception. Вот он интерфейс
А вот и тест.
Странно, но он проходит сразу – не верю!
Немного подумав, я решил, что стоит перенести валидацию не на момент запуска метода, а на момент создания враппера – это все равно runtime, но все же ближе к компиляции, чем изначальный вариант с валидацией при вызове конкретного метода.
Вот, это уже другой разговор!
Пошли его фиксить
Все, коммитимся
Дальше я хочу проверить, кейс, когда два метода в интерфейсе, а потому задам некого монстра, который может и квакать и мукать... И сходный с ним интерфейс. Внимание тут нет связи между классом и интерфейсом – по правилам java их с первого взгляда идентичные методы – разные!
А вот и тест, ничего нового, квакнули на прокси, проверили что монстр квакнул, мукнули на прокси, проверили что монстр мукнул...
Странно, но и это работает. Не верю!
А потому поломаю, добавив временный код. Увижу, что слетело и успокоюсь...
Ну ладно, проверю на всякий еще что-то...
Теперь можно сделать как было и проверить, что все работает
Коммитимся!
Но теперь давай сделаем другого монстра, который умеет еще и гавкать
Проверим что к его типу нельзя привести простого монстра...
Блин, опять все работает!
Не верю! А потому заремарю код проверяющий это и успокоюсь, когда увижу, что код слетел как я ожидал
Потом все верну как было, еще раз запущу и закоммичу, если зеленое
А теперь давай проверим кейсы с методами с параметрами
Вот и тест
Ну хоть тут тест слетел! :)
А фикс достаточно простой
И все! :) Зеленое – коммитимся!
Монстр методы которого возвращают другой тип чем в исходном, оборачиваемом классе
Вот и тест
Ура! Это еще не реализовано!
А потому реализуем
И все работает! А потому коммитимся!
Дальше поиграемся с примитивами
Все должно быть в шоколаде
Так и есть. Но надо перепроверить!
Добавим немного тестового кода и проверим что тест поломался
А после Ctrl-Z все работает
Коммит!
Дальше потестим, что примитивы и их обертки не распознаются как одни и те же типы (как было со String и Object)
Вот тест
Все прошло. Коммитимся (на самом деле я перепроверял, ломая код, просто тут не привел, ибо демонстрировал это уже) .
И еще один кейс – то же, но задом наперед
Вот и тест (так же работает - перепроверял)
Ну и все. Больше пока идей в голову не приходит.... В реальной жизни, когда заюзаю, быть может появятся кейсы...
Хотя нет! Появилась идея. А что, если интерфейсы наследуются друг от друга. Смогу ли я представить объект в виде суперинтерфейса и интерфейса наследника?
А вот и тест.
И он проходит! (я перепроверял, через поломку)
В вот второй тест, который делает то же но для суперинтерфейса
У него суперинтерфейса нет метода bowWof а потому блок заремарен :) (я копипастил)
Все проходит, а потому commit
Ура!
Привет.
ОтветитьУдалитьПолный пост не виден из Google Reader, что не привычно для твоих постов.
camparability нет такого слова)
ОтветитьУдалитьвидимо ты имел ввиду compatibility (совместимость, сочетаемость)
Очень много грамматических ошибок. Текст из ворда выглядит ужасно и плохо читается. Больше не торопись при написании постов.
ОтветитьУдалитьПо поводу твоего TDD идея с синтетической (когда ты добавлял код, который точно не работает) поломкой тестов мне вообще не понравилась.
По поводу самой утиной типизации, хочется еще видеть типизацию к классу, что в Java, насколько я ее знаю, тоже реализуемо, т.к. все методы виртуальные.
Когда возвращаемые типы методов разные, нужно, по-возможности, проверять совместимость возвращаемых типов, а не просто кидать исключение, если тип разный. Если типы одинаковые, то возвращать объект (спорно, возможно тоже прокси), если разный, но совместимый - то прокси.
Также и со входящими типами методов.
И, напоследок, переименуй ProxyFactory во что-нибудь типа DuckTypingFactory.
PS: с новым годом.
C новым годом!
ОтветитьУдалитьВ выборе слова полагался на
http://dl.dropbox.com/u/11842832/Blog/DuckTyping/1.png
Он говорит, что такое слово есть. Ему верю.
Я исправил 13 очепяток и одну ошибку. Чуть позже будет как обычно - без ворда и ифреймов.
А зачем приводить к классу? Как-то это пахнет. Хотя и сама типизация пахнет :).
В туду.
Когда возвращаемые типы методов разные - согласен, в туду. Ибо такая штука разрешена в Java
interface Bar {
Object foo(); // внимание Object
}
class BarImpl implements Bar {
public String foo() { // внимание String
return null;
}
}
А такая нет
interface Bar {
String foo(); // внимание, тут наоборот String
}
class BarImpl implements Bar {
public Object foo() { // а тут Object
return null;
}
}
Если типы одинаковые, то возвращать объект - интересно :) Надо будет попробовать... Туду!
Переименуй ProxyFactory во что-нибудь типа DuckTypingFactory. Это так же можно решить выбором уникального пакета. Хотя название веселое :) Туду!
Спасибо за ценные замечания.
Прием.
В слове camparability очепятка, надо было comparability... На рисунках исправлять долго. А потому так и останется.
ОтветитьУдалитьСкажи, вот ты потратил время на изучение моей работы, проделал code review - что-то новое удалось узнать?
ОтветитьУдалить"По поводу твоего TDD идея с синтетической (когда ты добавлял код, который точно не работает) поломкой тестов мне вообще не понравилась."
А отсюда по-подробнее.
Скажу, зачем это делал я. После расскажи, пожалуйста, почему оно не понравилось тебе.
Я так разошелся, что решил экстракнуть большой коммент в отдельный пост
>comparability
ОтветитьУдалитьМне мой вариант больше нравиться, ну да ладно.
>что-то новое удалось узнать
Если честно, то нет. Хотел узнать как делается утиная типизация в Java, оказалось, что так же, как в C#.
>почему оно не понравилось тебе
Потому, что если ты пишешь тест, который изначально зеленый, то на это есть только 2 причины: либо тест не нужен, либо ты не понимаешь свой код. Обе причины, скорее всего, при условии, что ты единственный творец кода, следствие того, что ты движешься слишком большими шагами (или сделал несколько больших шагов).
Для себя я избрал другую тактику: двигаться снаружи во внутрь. Сначала пишу большой тест на весь модуль. Добиваюсь, что бы проект компилировался, но тест не проходил. Скипаю тест, пишу тест на подмодуль и так, пока не дойду до самых мелких шагов. Конечно в этом есть один недостаток - с каждым новым красным тестом увеличивается стресс. Но жить можно.
"Если ты пишешь тест, который изначально зеленый, то на это есть только 2 причины: либо тест не нужен, либо ты не понимаешь свой код. Обе причины, скорее всего, при условии, что ты единственный творец кода, следствие того, что ты движешься слишком большими шагами (или сделал несколько больших шагов)."
ОтветитьУдалитьСогласен на 100%.
Теперь немного переиграем. Представим пространство-время, в котором я делал бы шажки более мелкими и каждый мой тест из списка предложенных был бы изначально красный. В таком случае, вероятно, это не вызвало бы эксепшена у тебя.
Что получается - делаешь мелкие шажки и все 20 тестов валидные; делаешь шаги побольше и часть из тех же 20 тестов уже лишние (потому что изначально зеленые).
Есть еще такая штука как ожидание автора. Тут можно поделить на две части:
Бывает так, что ожидаешь, что тест пройдет и все равно его пишешь - просто осознаешь, что где-то сделал большой шаг и в будущем без этого теста рефакторить будет сложно. Пофиг, что он зеленый - мне без него будет не комфортно потом.
Бывает так же так, что ты вовсе не ждешь зеленой полосы, а тут она на тебя бумц - и тут ты снова прав - это фидбек о том, что ты недостаточно знаешь систему. Но что от этого? Тест не перестает быть тестом. Знал бы систему, сделал бы мельче шаг - был бы вынужден написать этот тест.
При все при этом ценность - что ты при разработке чувствуешь и какой код получится в результате.
Для себя я избрал другую тактику: двигаться снаружи во внутрь. Сначала пишу большой тест на весь модуль. Добиваюсь, что бы проект компилировался, но тест не проходил. Скипаю тест, пишу тест на подмодуль и так, пока не дойду до самых мелких шагов. Конечно в этом есть один недостаток - с каждым новым красным тестом увеличивается стресс. Но жить можно.
ОтветитьУдалитьКажется понимаю, о чем ты. Но все же, было бы любопытно это увидеть на практике. Лучше конечно попарнокодить :) Но за неимением этого, хотя бы подглядеть в щелочку за твоим IDE в момент разработки. Как считаешь, реально?
И спасибо за твое время...
ОтветитьУдалитьСсылка на первоначальную статью мертвая. Правильно так: http://bitsofmind.wordpress.com/2008/07/07/duck_typing_in_java/
ОтветитьУдалитьСпасибо, линк обновил..
ОтветитьУдалить