Clojure - В чем преимущество использования записей перед картами

Мне трудно решить, является ли использование defrecord правильным выбором и, в более широком смысле, если мое использование протоколов в моих записях является семантическим и функциональным.

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

Исходя из опыта ООП, я хочу сделать что-то вроде:

(defprotocol Enemy
  "Defines base function of an Enemy"
  (attack [this] "attack function"))

(extend-protocol Enemy
  Orc
  (attack [_] "Handles an orc attack")  
  Troll
  (attack [_] "Handles a Troll attack"))



(defrecord Orc [health attackPower defense])
(defrecord Troll [health attackPower defense])

(def enemy (Orc. 1 20 3))
(def enemy2 (Troll. 1 20 3))

(println (attack enemy))
; handles an orc attack

(println (attack enemy2))
;handles a troll attack

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

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

  1. Является ли моя описанная выше реализация записей и протоколов надежным вариантом использования?
  2. В более общем смысле, когда запись предпочтительнее карты? Я читал, что вам следует отдавать предпочтение записям, когда вы перестраиваете одну и ту же карту несколько раз (как и я в этом случае). Это логично?

person newBieDev    schedule 01.07.2020    source источник
comment
Что отличает атаку орков от атаки троллей?   -  person Jochen Bedersdorfer    schedule 01.07.2020
comment
Орк V. Тролль может быть не лучшим примером, но будут случаи, когда метод атаки для конкретной сущности будет достаточно другим, что потребует своего собственного уникального расчета. Атака также является лишь одним из примеров, здесь будет несколько общих методов, которые различаются по реализации.   -  person newBieDev    schedule 01.07.2020
comment
да, но будет ли это привязано к "типу" или наличию определенных ключей?   -  person Jochen Bedersdorfer    schedule 01.07.2020
comment
Я думаю, что по типу орк всегда будет выполнять атаку одинаково, и это будет справедливо для любых других распространенных методов врага.   -  person newBieDev    schedule 01.07.2020
comment
тогда запись вроде бы в порядке. Если у вас есть другие критерии для отправки, вы можете изучить мультиметоды.   -  person Jochen Bedersdorfer    schedule 01.07.2020
comment
Единственный недостаток, который я нахожу в записях, - это то, что вы фиксируете имя (то есть имя типа). Позже это может оказаться не лучшим решением, поскольку у вас могут быть другие идентифицирующие данные, но к тому времени у вас будет много кода, привязанного к этому имени.   -  person Jochen Bedersdorfer    schedule 01.07.2020


Ответы (2)


Эта блок-схема по-прежнему остается полезным советом девять лет спустя: https://cemerick.com/2011/07/05/flowchart-for-choosing-the-right-clojure-type-definition-form/ - похоже, переместился в https://cemerick.com/blog/2011/07/05/flowchart-for-choosing-the-right-clojure-type-definition-form.html

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

person Sean Corfield    schedule 01.07.2020
comment
К вашему сведению, ваша ссылка на блок-схему не работает. - person jmrah; 17.01.2021
comment
Похоже, Час Эмерик полностью переписал свой блог - и многие полезные ссылки на его контент исчезли (включая результаты опроса State of Clojure за несколько лет). Ссылка была действительна, когда я ее разместил ... - person Sean Corfield; 18.01.2021
comment
Я нашел обновленную ссылку и добавил ее к исходному ответу. - person Sean Corfield; 18.01.2021

К отличному ответу Шона я бы только добавил, что записи могут замедлить итеративную разработку, особенно с использованием такого инструмента, как lein-test-refresh или аналогичного.

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

Кроме того, перекомпиляция прерывает сравнение с все еще существующими объектами записи, поскольку перекомпилированный объект (даже если нет изменений!) Не будет = исходному, поскольку он имеет другой файл класса. В качестве примера предположим, что у вас есть Point запись:

(defrecord Point [x y])

(def p (->Point 1 2)) ; in file ppp.clj
(def q (->Point 1 2)) ; in file qqq.clj

(is (= p q)) ; in a unit test

Если файл ppp.clj перекомпилируется, он генерирует новый класс Point с другим значением идентификатора, чем раньше. Поскольку записи должны иметь одинаковые значения типа И, чтобы считаться равными, модульный тест завершится неудачно, даже если оба имеют тип Point и оба имеют значения [1 2]. Это непреднамеренная проблема при использовании записей.

person Alan Thompson    schedule 01.07.2020