Подтвердить что ты не робот

Рубиновая и утиная печать: дизайн по контракту невозможно?

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

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

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

def get_files_in(directories)

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

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

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

  • Даже при 100% -ной точной, актуальной документации, Ruby-код должен существенно раскрывать свою реализацию, прерывая инкапсуляцию. "Чистота ОО" в стороне, это, казалось бы, кошмар для обслуживания.
  • Код Ruby дает мне no подсказку о том, что возвращается; Мне пришлось бы по существу экспериментировать или прочитать код, чтобы узнать, какие методы ответит возвращенный объект.

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

Update

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

4b9b3361

Ответ 1

Что происходит, так это то, что get_files_in является плохим именем в Ruby. Позвольте мне объяснить.

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

Размышляя об этом в этих терминах, вы просто определяете метод под названием 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

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

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

Ответ 2

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

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

Ответ 3

В то время как я люблю статическую типизацию, когда пишу 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

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

(BTW, "bozo" относится к вашим действительно:)

Ответ 4

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

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 для разработки, основанной на поведении.

Ответ 5

Краткий ответ: Автоматизированные модульные тесты и хорошие методы именования.

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

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

Ответ 6

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

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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