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

#java #ruby #oop #interface #design-by-contract

Вопрос:

Сигнатура метода в Java:

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

похожий в ruby

 def get_files_in(directories)
 

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

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

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

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

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

Обновить

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

Ответ №1:

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

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

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

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

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

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

Комментарии:

1. 1: После того, как я вырос на статически типизированных языках и перешел на более динамичные (Ruby, некоторые языки PHP), нужно изменить не только синтаксис-это новый способ мышления

2. Здесь я полностью согласен с Мэттом. Вам придется приспособиться к этому новому образу мышления. Однако этого не произойдет в первый же день. Там нет «От [вставить статически типизированный язык здесь] до Ruby за 24 часа», вы обнаружите, что сначала пишете 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
 

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

(Кстати, «бозо» относится к вашему покорному слуге 🙂

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

Комментарии:

1. Мне жаль, что я должен отклонить ответ участника с гораздо более высоким стажем работы, чем у меня. Но это не утиный набор текста. Или, скорее, это определенный тип утиного набора текста, который часто скрывает от новичков гораздо более глубокую идею утиного набора текста. По этой причине мы называем утиный набор #respond_to? и #instance_of? «неправильный «тип утиного набора или» куриный набор», хотя технически это своего рода утиный набор. Лучший способ познакомить новичков с утиным набором текста-это сказать, что это кодовое слово для того, чтобы вообще не проверять тип и вместо этого ждать ошибки =)

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

3. Я не проверил дату. Действительно, 6 лет назад это был самый современный ответ. Я тоже чувствую себя старым =)

Ответ №5:

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

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

Наконец, вы сделаете это явным, написав модульные тесты против этого метода — показав примеры того, как он должен работать. Так что, если 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