Конспект урока
Проблема: код не поддаётся тестированию
Наша игра работает, но обладает ограничением — мы не можем её полноценно тестировать.
const cards = l(
cons('Алчный натиск скорости', () => 4),
cons('Демонов маршрут воздаяния', health => Math.round(health * 0.3)),
)
const game = make(cards)
const log = game('John', 'Ada')
Если колода содержит две и более карты, то ход игры и её результат заранее определить и протестировать НЕ получится.
assert.equal(length(log), ?); // неизвестно
Ведь в процессе игры карта выбирается случайным образом:
const card = random(cards) // эта строчка делает игру недетерминированной
const cardName = car(card)
const damage = cdr(card)(health2)
const newHealth = health2 - damage
Вызов random(cards)
возвращает случайную карту. Этот код располжен внутри функции с игрой, поэтому делает недетерминированной всю игру.
Решение: инвертирование
Сейчас выбор карты осуществляется внутри игры, и мы не можем на это никак повлиять. Но ситуация изменится, если сделать так, чтобы алгоритм выбора карты игра получала "снаружи". Это легко реализовать с помощью передачи параметра.
Рассмотрим простой пример: функция принимает на вход колоду карт cards
, внутри происходит случайный выбор карты и какие-то другие дальнейшие манипуляции. Это принципиальная схема, если отвлечься от несущественных деталей.
(cards) => {
const card = random(cards)
// to do something with card
}
Эта функция НЕ является чистой, она недетерминирована. И это нормально для игры, но не для тестов.
Применим к функции технику инвертирования, реализовав передачу процесса выбора карты снаружи, через параметры:
(cards, customRandom) => {
const card = customRandom(cards)
// to do something with card
}
Теперь мы можем управлять процессом выбора карты, передавая (в зависимости от ситуации и наших целей) ту или иную функцию в параметр customRandom
.
Тесты
Для тестирования нам не подойдёт обычный random. Поэтому определим и передадим в функцию игры специальную функцию выбора карты, обеспечивающую предсказуемое поведение:
const cards = l(
cons('Тусклый маниту диспута', () => 7),
cons('Мыслительный рубитель ограды', health => Math.round(health * 0.8)),
)
let cardIndex = 1
const game = make(cards, (pack) => {
cardIndex = cardIndex === 0 ? 1 : 0
return get(cardIndex, pack)
})
const log = game('John', 'Ada')
Мы передали в игру (вторым параметром) анонимную функцию:
(pack) => {
cardIndex = cardIndex === 0 ? 1 : 0
return get(cardIndex, pack)
}
Её ядро заключается в строчке кода, определяющей текущий индекс:
cardIndex = cardIndex === 0 ? 1 : 0
Значение переменной cardIndex
функция берёт из переменной, определённой во внешнем окружении:
let cardIndex = 1
Это важно, так мы можем смоделировать нужное нам предсказуемое поведение. При каждом новом вызове значение cardIndex
циклически меняется с нуля на единицу и наоборот (индексирование в списке карт начинается с нуля!). Это как раз то, что нужно для ситуации колоды, состоящей из двух карт.
Обязательно проанализируйте процесс выбора карт в модуле с тестами в практике к этому уроку!
На примере простой функции продемонстрируем принцип определения циклического (а значит предсказуемого!) изменения величины:
let cardIndex = 1
const getIndex = () => {
cardIndex = cardIndex === 0 ? 1 : 0
return cardIndex
}
for (let i = 0; i < 10; i += 1) {
console.log(getIndex())
}
// => 0
// => 1
// => 0
// => 1
// => 0
// => 1
// => 0
// => 1
// => 0
// => 1
Для колоды из трёх или какого-нибудь другого количества карт надо будет модифицировать функцию. Можно сразу написать более универсальный вариант:
const customRandom = (cardIndex, minIndex, maxIndex) => {
return () => {
if (cardIndex > maxIndex) {
cardIndex = minIndex
}
const currentIndex = cardIndex
cardIndex += 1
return currentIndex
}
}
console.log('Выводим индексы с 0 до 2. Начинаем с 0')
const getIndex = customRandom(0, 0, 2)
for (let i = 0; i < 6; i += 1) {
console.log(getIndex())
}
console.log('Выводим индексы с 1 до 5. Начинаем с 2')
const getIndex2 = customRandom(2, 1, 5)
for (let i = 0; i < 10; i += 1) {
console.log(getIndex2())
}
// => Выводим индексы с 0 до 2. Начинаем с 0
// => 0
// => 1
// => 2
// => 0
// => 1
// => 2
// => Выводим индексы с 1 до 5. Начинаем с 2
// => 2
// => 3
// => 4
// => 5
// => 1
// => 2
// => 3
// => 4
// => 5
// => 1
Выводы
С помощью техники инвертирования мы добились следующих преимуществ:
- Предсказуемого поведения — код стало возможным тестировать. Теперь можем управлять процессом выбора в зависимости от целей: для игр передавать обычный random, для тестов — кастомный.
- В целом, добились расширения возможностей программы посредством делегирования части функциональности внешнему коду. Программа стала более гибкой в использовании.
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.