Инверсия

Для сохранения прогресса вступите в курс. Войти или зарегистрироваться.

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

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

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

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

https://repl.it/@hexlet/js-ddp-invert-index

Для колоды из трёх или какого-нибудь другого количества карт надо будет модифицировать функцию. Можно сразу написать более универсальный вариант:

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

https://repl.it/@hexlet/js-ddp-invert-generator

Выводы

С помощью техники инвертирования мы добились следующих преимуществ:

  • Предсказуемого поведения — код стало возможным тестировать. Теперь можем управлять процессом выбора в зависимости от целей: для игр передавать обычный random, для тестов — кастомный.
  • В целом, добились расширения возможностей программы посредством делегирования части функциональности внешнему коду. Программа стала более гибкой в использовании.
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Javascript, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →