Ruby and duck typing: дизайн по контракту невозможен?

Подпись метода в Java:

public List<String> getFilesIn(List<File> directories)

аналогичный в рубине

def get_files_in(directories)

В случае Java система типов дает мне информацию о том, что метод ожидает и предоставляет. В случае с Руби я не знаю, что мне нужно передать или что я ожидаю получить.

В Java объект должен формально реализовывать интерфейс. В Ruby переданный объект должен отвечать на все методы, вызываемые в методе, определенном здесь.

Это кажется очень проблематичным:

  1. Даже со 100% точной и актуальной документацией код Ruby должен существенно раскрывать свою реализацию, нарушая инкапсуляцию. Если отбросить "чистоту OO", это могло бы показаться кошмаром обслуживания.
  2. Код Ruby не дает мне никакого понять, что возвращается; Мне пришлось бы по существу поэкспериментировать или прочитать код, чтобы узнать, на какие методы будет реагировать возвращаемый объект.

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

Обновлять

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


person davetron5000    schedule 07.10.2008    source источник


Ответы (8)


Все сводится к тому, что get_files_in - плохая репутация в Ruby - позвольте мне объяснить.

В java / C # / C ++ и особенно в цели C аргументы функции являются частью имени. В Ruby это не так.
Замечательный термин для этого - перегрузка метода, и он применяется компилятором.

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

Если вы хотите ограничить его каталогом, то, чтобы учесть эту информацию, вы должны вызвать метод get_files_in_directory. В качестве альтернативы вы можете сделать его методом класса Directory, который Ruby уже делает за вас.

Что касается типа возврата, из get_files подразумевается, что вы возвращаете массив файлов. Вам не нужно беспокоиться о том, что это List<File> или ArrayList<File> или так далее, потому что все просто используют массивы (и если они написали собственный, они напишут его для наследования от встроенного массива) .

Если вы хотите получить только один файл, вы бы назвали его get_file, get_first_file или так далее. Если вы делаете что-то более сложное, например, возвращаете FileWrapper объектов, а не просто строки, то есть действительно хорошее решение:

# returns a list of FileWrapper objects
def get_files_in_directory( dir )
end

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

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

person Orion Edwards    schedule 07.10.2008
comment
+1: После того, как я вырос на статически типизированных языках и перешел на более динамичные (Ruby, немного PHP), нужно изменить не просто синтаксис - это новый образ мышления. - person Matt Rogish; 07.10.2008
comment
Я полностью согласен с Мэттом здесь. Вам придется приспособиться к этому новому образу мышления. Однако этого не произойдет в первый день. Не существует From [вставьте сюда статически типизированный язык] в Ruby через 24 часа. Вы обнаружите, что сначала пишете java-идиоматический код, используя синтаксис ruby. Если вы постоянно пересматриваете и улучшаете свой код, вы, тем не менее, довольно быстро начнете писать рубиново-идиоматический код. - person kungfoo; 04.05.2009

Я бы сказал, что, хотя метод Java дает вам больше информации, он не дает вам достаточно информации для удобного программирования.
Например, это Список Строки просто имена файлов или полные пути?

Учитывая это, ваш аргумент о том, что Ruby не дает вам достаточно информации, применим и к Java.
Вы по-прежнему полагаетесь на чтение документации, просмотр исходного кода или вызов метода и просмотр его вывода (и достойного тестирования конечно).

person Darren Greaves    schedule 07.10.2008

Хотя я люблю статическую типизацию, когда пишу код Java, нет причин, по которым вы не можете настаивать на продуманных предварительных условиях в коде Ruby (или любом другом коде в этом отношении). Когда мне действительно нужно настаивать на предварительных условиях для параметров метода (в Ruby), я рад написать условие, которое могло бы вызвать исключение времени выполнения, чтобы предупредить об ошибках программиста. Я даже создаю себе подобие статической типизации, написав:

def get_files_in(directories)
   unless File.directory? directories
      raise ArgumentError, "directories should be a file directory, you bozo :)"
   end
   # rest of my block
end

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

(Кстати, «бозо» относится к вашему искреннему :)

person Alan    schedule 07.10.2008

Проверка метода с помощью утиного ввода:

i = {}
=> {}
i.methods.sort
=> ["==", "===", "=~", "[]", "[]=", "__id__", "__send__", "all?", "any?", "class", "clear", "clone", "collect", "default", "default=", "default_proc", "delete", "delete_if", "detect", "display", "dup", "each", "each_key", "each_pair", "each_value", "each_with_index", "empty?", "entries", "eql?", "equal?", "extend", "fetch", "find", "find_all", "freeze", "frozen?", "gem", "grep", "has_key?", "has_value?", "hash", "id", "include?", "index", "indexes", "indices", "inject", "inspect", "instance_eval", "instance_of?", "instance_variable_defined?", "instance_variable_get", "instance_variable_set", "instance_variables", "invert", "is_a?", "key?", "keys", "kind_of?", "length", "map", "max", "member?", "merge", "merge!", "method", "methods", "min", "nil?", "object_id", "partition", "private_methods", "protected_methods", "public_methods", "rehash", "reject", "reject!", "replace", "require", "respond_to?", "select", "send", "shift", "singleton_methods", "size", "sort", "sort_by", "store", "taint", "tainted?", "to_a", "to_hash", "to_s", "type", "untaint", "update", "value?", "values", "values_at", "zip"]
i.respond_to?('keys')
=> true
i.respond_to?('get_files_in')  
=> false

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

 def get_files_in(directories)
    fail "Not a List" unless directories.instance_of?('List')
 end

 def example2( *params ) 
    lists = params.map{|x| (x.instance_of?(List))?x:nil }.compact 
    fail "No list" unless lists.length > 0
    p lists[0] 
 end

x = List.new
get_files_in(x)
example2( 'this', 'should', 'still' , 1,2,3,4,5,'work' , x )

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

person Kent Fredric    schedule 07.10.2008
comment
Мне очень жаль, что я должен проголосовать против ответа участника с гораздо более высоким стажем SO, чем у меня. Но это не утиная печать. Или, скорее, это определенный тип набора текста с уткой, который часто скрывает от новичков гораздо более глубокую идею набора текста с уткой. По этой причине мы называем утиный набор #respond_to? и #instance_of? неправильным 'типом утиного набора или куриным набором, хотя технически это своего рода утиный набор. Лучший способ познакомить новичков с утиной печатью - сказать, что это кодовое слово для того, чтобы вообще не проверять тип и вместо этого ждать ошибки =) - person Boris Stitnicky; 10.08.2014
comment
Без обид. Прошло много времени с тех пор, как я сделал рубин, этот пост - битва за 6 лет. (и я чувствую себя старым). Лучшие практики меняются, и системы документации и поддержки должны это отражать. - person Kent Fredric; 14.08.2014
comment
Я не проверял дату. Действительно, 6 лет назад это был ультрасовременный ответ. Я тоже чувствую себя старым =) - person Boris Stitnicky; 14.08.2014

Краткий ответ: автоматические модульные тесты и передовые методы именования.

Правильное наименование методов очень важно. Давая имя get_files_in(directory) методу, вы также даете пользователям подсказку о том, что метод ожидает получить и что он вернет взамен. Например, я не ожидал, что Potato объект выйдет из get_files_in() - это просто не имеет смысла. Имеет смысл получить только список имён файлов или, что более уместно, список экземпляров File из этого метода. Что касается конкретного типа списка, в зависимости от того, что вы хотели сделать, фактический тип возвращаемого списка не имеет особого значения. Важно то, что вы можете каким-то образом перечислить элементы в этом списке.

Наконец, вы сделаете это явным, написав модульные тесты для этого метода - показывая примеры того, как он должен работать. Так что, если get_files_in внезапно вернет Potato, тест вызовет ошибку, и вы узнаете, что первоначальные предположения теперь неверны.

person jop    schedule 07.10.2008

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

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

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

Это связано с утиным набором текста. Контракт должен давать ключ к пониманию того, что происходит, независимо от типа входных данных метода. Таким образом, договор следует понимать более глубоко и обобщенно. Сам этот ответ может показаться немного неконкретным или даже надменным, за что я приношу свои извинения. Я просто пытаюсь сказать, что утка не ложь, утка означает, что человек думает о своей проблеме на более высоком уровне абстракции. Дизайнеры, программисты, математики - это разные имена для те же способности, и математики знают, что в математике есть много уровней способностей, когда математики следующего более высокого уровня легко решают задачи, которые тем, кто находится на более низком уровне, слишком сложно решить. Утка означает, что ваше программирование должно быть хорошим математическим, и оно ограничивает успешных разработчиков и пользователей только теми, , которые могут это сделать.

person Boris Stitnicky    schedule 10.08.2014

Это ни в коем случае не кошмар обслуживания, это просто еще один способ работы, требующий согласованности в API и хорошей документации.

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

Кстати, я не знаю о Ruby, но в PHP вы можете использовать теги @phpdoc, чтобы подсказывать IDE (Eclipse PDT) типы данных, возвращаемые определенным методом.

person Camilo Díaz Repka    schedule 07.10.2008

Несколько лет назад я предпринял наполовину неудачную попытку создать что-то вроде dbc для Ruby, что может дать людям некоторые идеи о том, как продвигаться вперед с более комплексным решением:

https://github.com/justinwiley/higher-expectations

person Allyl Isocyanate    schedule 22.03.2011