// codeart.ru / Теория программирования / Про утиную типизацию Форум

Про утиную типизацию rss подписка

Автор: Evgeniy Sergeev

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

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

    Лучшие комментарии

  1. Основной смысл которой сводится к тому, что если два метода обладают одним и тем же именем, то они должны решать одну и ту же задачу.

    Совсем не обязательно. Метод выполняет нечто в контексте объекта, а не стереотипов восприятия названия программиста. С разными объектами разных классов методы с одинаковыми именами могут выполнять совсем разные действия. И это как раз связано с реализацией, которую скрыли. Например, метод delete может как удалять что-то, так и помечать как удалённое. С другой стороны методы remove и delete имеют разное название, но могут быть реализованы в одном классе (адаптере), например, для выполнения одного и того же действия при взаимодействии с API вызывающих их интегрируемых реализаций чего-то уже реализованного.

    Мир конкретен. Он не сферический конь в вакууме. Хотя для себя и для коллектива всё же стандарты нужно принимать, а различия документировать.

  2. > принадлежноть объекта к тому или иному классу (интерфейсу) определяется путем проверки на наличие всех свойств искомого класса в созданном объекте.

    Нет, это вы перепутали с языком Go.

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

    Как заставить разработчика делать xxx (подставить что угодно) — это скорее вопрос управления. А чтобы утиная или куриная или какая-то другая типизация не приносила проблем, нужно просто попросить разработчика воспользоваться его собственным кодом в качестве потребителя. Причём, через месяц-два после написания этого кода. Эффект удивительный, проверьте.

  3. Сергей Шепелев,

    >Утиная типизация сводится к тому, что вы просто вызываете нужный метод объекта. И если не произошло исключения, значит этот объект реализует нужный вам интерфейс.

    Нифига подобного, если мы используем только часть методов — значит мы реализуем какой-то другой интерфейс, в котором есть только эти методы.
    Другими словами, набор используемых методов — это описательно заданный интерфейс.
    На примере множеств:
    Есть множество А(а,б,в) и множество Б(б,в) — так вот набор (б, в) равен множеству Б. Набор (в) не равен ни А и ни Б, зато равен некому третьему множеству С(в)… Смысл в том, что интерфейсы и есть эти множества Интерфейс1(а,б.в), Интерфейс2(б,в), Интерфейс3(в), Интерфейс4(а, в) и т.д.
    Отсюда получается, что скажем объект с двумя методами уже может принадлежать к 3-м интерфейсам, с тремя методами к 7-ми интерфейсам и т.д.

    Короче говоря, интерфейсов при утинной типизации может быть очень много, и разработчик сам должен понять, что он реализует и нужно ли ему заморачиваться на интерфейсы :-)

  4. Заморочка на интерфейсы в питоне и руби выглядит примерно так: «а ещё добавлю метод __iter__, чтоб по нему можно было ходить for-ом». И это полностью реализованный интерфейс «чего-то по чему можно ходить for-ом».

    На работе речь не идёт о каких-то конкретных интерфейсах, как в языках со стат. типизацией — IEnumerable, IDispatch. Вась, мне нужно чтоб его можно было в пажинатор пихнуть — значит надо добавить метод .count(), всё остальное уже есть.

    > Короче говоря, интерфейсов при утинной типизации может быть очень много, и разработчик сам должен понять, что он реализует и нужно ли ему заморачиваться на интерфейсы
    Ах вот оно. Так вот нет, заморачиваться на реализацию интерфейсов из прошлых своих языков не надо. Это не окупится.

  1. Основной смысл которой сводится к тому, что если два метода обладают одним и тем же именем, то они должны решать одну и ту же задачу.

    Совсем не обязательно. Метод выполняет нечто в контексте объекта, а не стереотипов восприятия названия программиста. С разными объектами разных классов методы с одинаковыми именами могут выполнять совсем разные действия. И это как раз связано с реализацией, которую скрыли. Например, метод delete может как удалять что-то, так и помечать как удалённое. С другой стороны методы remove и delete имеют разное название, но могут быть реализованы в одном классе (адаптере), например, для выполнения одного и того же действия при взаимодействии с API вызывающих их интегрируемых реализаций чего-то уже реализованного.

    Мир конкретен. Он не сферический конь в вакууме. Хотя для себя и для коллектива всё же стандарты нужно принимать, а различия документировать.

  2. А можно кратко рассказать про историю такого странного термина? Почему утиная? :-)

  3. А можно кратко рассказать про историю такого странного термина? Почему утиная?

    Если что-то выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка и есть

  4. Прикольно. Кстати, охотники пользуются этим утиным предположением, заманивая доверчивых уточек.

  5. Это из фильма «Монти Пайтон и Священный Грааль». Название языка Python, кстати, тоже в его честь, а не в честь змия.

  6. Не знаю как в руби, но в питоне используется принцип открытого кимоно: как следствие отсутствие(почти) protected/private и наличие method mixing.
    В php скоро появится traits, но это не совсем method mixing.

  7. Евгений,

    >Совсем не обязательно. Метод выполняет нечто в контексте объекта, а не стереотипов восприятия названия программиста. С разными объектами разных классов методы с одинаковыми именами могут выполнять совсем разные действия…

    В приведенном вами примере метод «delete» может быть реализован по-разному (удаление или пометка об удалении), но все же имеет одну и ту же функцию (задачу).

    Относительно именования, могу привести такой пример — никто не может запретить родителям близнецов дать им одинаковые имена. Но приведите хоть один пример, когда на практике двум братьям-близница (или сестрам-близняшкам) давали одинаковые имена? Разные имена, в данном случае, нужны для того, чтобы показать, что хоть люди и похожи, но это разные люди и один не может быть заменен другим.
    Но! Тут нужно понимать, что знание о невозможности заменить одного другим возникло из того, что мы знаем, что это люди (т.е. как бы из знания о том к какому классу они принадлежат). А разные имена даются лишь для того, чтобы еще раз это подчеркнуть и не внести путаницу.

    В ООП есть замечательный принцип, который называется «принцип подстановки Барбары Лисков». Суть сводится к тому, что «Функции, которые используют ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом» (определение из Википедии).
    Утиная типизация позволяет отказаться от наследования принятого в строготипизированных языках, тогда понятие «производного класса» можно изложить в такой форме — производным будем считать такой класс у которого все свойства производного класса совпадают с методами базового класса. Отсюда, имя метода (или совокупность имен) должно быть исчерпывающей характеристикой, позволяющей однозначно определить принадлежность базовому классу. А это возможно только в случае уникальности имен и их функционального назначения. Обращаю внимание, что функциональное назначение — это не реализация.

  8. Evgeny Sergeev, ваш пример не корректен. Если уж вести речь о близнецах, то это объекты а не методы. Что-то не верно у вас с архитектурой :) Михаила Пупкина вы не замените Михаилом Ивановым за токарным станком.

    Насчёт принципа подстановки согласен, но в контексте, когда для манипуляции имеют место объекты задуманные для манипуляции, а не все.

    Кроме того в тот же питон, да и практически все сколько то ООЯ встроены средства для ручной работы… От кого произошёл, имеет тот или иной метод или свойство. То есть Михаила Пупкина вы можете заменить Михаилом Ивановым, если знаете, что он тоже станочник с нужной вам квалификацией. Например, если они оба произошли от людей станочников с определённым разрядом.

  9. Михаила Пупкина вы не замените Михаилом Ивановым за токарным станком.
    Здесь я не дописал, что даже близнецов не заменишь. Заменить можно опираясь на определённый критерий. Близнецов можно заменить например в ванной, что бы их помыть по очереди. И не важно близнецы они или нет. И тем более не важно как их зовут. Мыть нужно обоих.

  10. Евгений, согласен, про близнецов пример неудачный.

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

    >Совсем не обязательно…

    Обязательно!

  11. > принадлежноть объекта к тому или иному классу (интерфейсу) определяется путем проверки на наличие всех свойств искомого класса в созданном объекте.

    Нет, это вы перепутали с языком Go.

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

    Как заставить разработчика делать xxx (подставить что угодно) — это скорее вопрос управления. А чтобы утиная или куриная или какая-то другая типизация не приносила проблем, нужно просто попросить разработчика воспользоваться его собственным кодом в качестве потребителя. Причём, через месяц-два после написания этого кода. Эффект удивительный, проверьте.

  12. Сергей Шепелев,

    >Утиная типизация сводится к тому, что вы просто вызываете нужный метод объекта. И если не произошло исключения, значит этот объект реализует нужный вам интерфейс.

    Нифига подобного, если мы используем только часть методов — значит мы реализуем какой-то другой интерфейс, в котором есть только эти методы.
    Другими словами, набор используемых методов — это описательно заданный интерфейс.
    На примере множеств:
    Есть множество А(а,б,в) и множество Б(б,в) — так вот набор (б, в) равен множеству Б. Набор (в) не равен ни А и ни Б, зато равен некому третьему множеству С(в)… Смысл в том, что интерфейсы и есть эти множества Интерфейс1(а,б.в), Интерфейс2(б,в), Интерфейс3(в), Интерфейс4(а, в) и т.д.
    Отсюда получается, что скажем объект с двумя методами уже может принадлежать к 3-м интерфейсам, с тремя методами к 7-ми интерфейсам и т.д.

    Короче говоря, интерфейсов при утинной типизации может быть очень много, и разработчик сам должен понять, что он реализует и нужно ли ему заморачиваться на интерфейсы :-)

  13. Заморочка на интерфейсы в питоне и руби выглядит примерно так: «а ещё добавлю метод __iter__, чтоб по нему можно было ходить for-ом». И это полностью реализованный интерфейс «чего-то по чему можно ходить for-ом».

    На работе речь не идёт о каких-то конкретных интерфейсах, как в языках со стат. типизацией — IEnumerable, IDispatch. Вась, мне нужно чтоб его можно было в пажинатор пихнуть — значит надо добавить метод .count(), всё остальное уже есть.

    > Короче говоря, интерфейсов при утинной типизации может быть очень много, и разработчик сам должен понять, что он реализует и нужно ли ему заморачиваться на интерфейсы
    Ах вот оно. Так вот нет, заморачиваться на реализацию интерфейсов из прошлых своих языков не надо. Это не окупится.

  14. Сергей, это твой двойной тёзка?
    http://shepelev.adomains.ru/

  15. Сергей Шепелев,

    >Заморочка на интерфейсы в питоне и руби выглядит примерно так: “а ещё добавлю метод __iter__, чтоб по нему можно было ходить for-ом”. И это полностью реализованный интерфейс “чего-то по чему можно ходить for-ом”.

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

    Вот недавний пример, я хочу чтобы в мою модель поступали только валидированные данные, на PHP выразить эту мысль легко:

    class Model {
    public function __constructor(ValidData $data){

    }
    }

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

    При этом я не обязан знать детали реализации конструктора модели. Я должен знать только, что из себя представляет интерфейс ValidData.

    А как то же самое будет выглядеть на Пайтоне? Что-то типа:
    class Model:
    def __init__(self, validData):
    data.is_valid()
    self.data = data

    Как, например, IDE мне подскажет, что в объект нужно передавать методы именно с методом is_valid? Как мне понять, вообще, что из себя представляет validData?

  16. Можно сделать assert isinstance(data, ValidData) — вручную проверить тип.

    Только это противоречит идее утиной типизации. Тут нужно повернуть мозг в определённую сторону и не ожидать конкретный тип или интерфейс. Нужно доверять пользователям своего кода. Да, они могут передать что угодно. Это нормально.

    По поводу IDE подскажет… а что, в контексте откуда эта модель создаётся много из чего выбирать? Зачем эта подсказка нужна вообще? Вот я понимаю, забыл как метод называется — os.path.exist или os.path.exists, некоторые IDE умеют подсказывать.

  17. Сергей Шепелев,

    >Тут нужно повернуть мозг в определённую сторону и не ожидать конкретный тип или интерфейс.

    А что тогда ожидать? Все равно при написании метода я должен продумать какие у него входные и выходные данные. В любом случае обработать «что угодно» я не смогу.

    >Нужно доверять пользователям своего кода.

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

    >По поводу IDE подскажет… а что, в контексте откуда эта модель создаётся много из чего выбирать?

    Я имел в виду, что нормальные IDE при автодополнении показывают сигнатуру метода, и по ней можно судить какому интерфейсу должны соответствовать передаваемые данные.

  18. Проблемы с доверием нет. Проблема в том, что другие программисты должен знать как реализован мой метод, чтобы понять какие данные он может обработать.
    Я так понимаю, что не реализовать, а использовать? Только документирование API.

  19. Евгений,

    >Только документирование API.

    Я пришел к тому же. Единственный нюанс — документирование делаю с помощью юнит тестов.

  20. >А что тогда ожидать? Все равно при написании метода я должен продумать какие у него входные и выходные данные. В любом случае обработать “что угодно” я не смогу.

    Ожидать объект с нужными методами, то есть ровно с теми, которые были использованы в вашем коде.

    Например, def sort_head(lst): return sorted(lst)[:10] — ожидается объект с методом __iter__. Не именно список, не именно какой-то другой тип, нужен только один метод. В окамле то же самое, только статически проверяется — совсем конфетка.

    И если я вызову sort_head(5) — я сам виноват, я получу неожиданное исключение и sort_head об этом заботиться не должен.

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

    Эм.. ну нет. Вот есть например, os.path.exists. Я не хочу знать как он реализован, я хочу знать только контракт: строка на входе, буль на выходе. Это отражено в документации.

    > Я имел в виду, что нормальные IDE при автодополнении показывают сигнатуру метода, и по ней можно судить какому интерфейсу должны соответствовать передаваемые данные.

    Да, есть такое. Опять же, из жизни — чаще всего хватает понятных имён. В редких случаях да, не очень понятно, приходится лезть в документацию. Опять же, «нормальные IDE» (что уже само по себе провокация :) ) показывают не только сигнатуру, но и докстринг к методу, что иногда помогает.

  21. Сергей Шепелев,

    > И если я вызову sort_head(5) — я сам виноват, я получу неожиданное исключение и sort_head об этом заботиться не должен.

    А если не получите исключение? Ведь методы далеко не всегда линейны, при одних условиях все пройдет успешно, при других вывалится с исключением. Кто тогда виноват?

  22. Если сводить вопрос к «кто виноват», то виноват в первую очередь тот, кто документацию не писал, а во вторую — тот, кто её не читал. Потому что документация в динамических языках это часть проверки типов в статических. То есть, в каком-то странном смысле, без документации программа не скомпилирована. :) хотя и работает

    Но лучше не скатываться на «кто виноват» — это не конструктивно, а просто написать докстринг и/или починить вызывающий код.

Leave a Reply

« »