Рубин и утка: дизайн по контракту невозможен? - PullRequest
19 голосов
/ 07 октября 2008

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

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

похож в рубине

def get_files_in(directories)

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

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

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

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

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

Обновление

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

Ответы [ 8 ]

30 голосов
/ 07 октября 2008

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

В java / C # / C ++, и особенно в цели 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

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

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

8 голосов
/ 07 октября 2008

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

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

6 голосов
/ 07 октября 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

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

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

5 голосов
/ 07 октября 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 для развития, управляемого поведением.

3 голосов
/ 10 августа 2014

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

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

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

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

3 голосов
/ 07 октября 2008

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

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

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

1 голос
/ 22 марта 2011

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

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

1 голос
/ 07 октября 2008

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

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

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

...