Зарегистрируйтесь, чтобы продолжить обучение

Инверсия JS: Программирование, управляемое данными

Конспект урока

Проблема: код не поддаётся тестированию

Наша игра работает, но обладает ограничением — мы не можем её полноценно тестировать.

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, для тестов — кастомный.
  • В целом, добились расширения возможностей программы посредством делегирования части функциональности внешнему коду. Программа стала более гибкой в использовании.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff