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

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

Помеченные данные – это одна из ключевых тем нашего курса, поэтому крайне важно разобраться с этим уроком и понять всё, что здесь объясняется.

Недостатки имплементации

Давайте посмотрим на реализацию (имплементацию), которую мы сделали до этого и разберём те недостатки, которые она в себе несёт.

import { cons, car, cdr } from 'hexlet-pairs'

const cards = l(
  cons('Костяная кочерга гробницы', () => 6),
)
const game = make(cards)

// inside ...
const card = random(cards)
const cardName = car(card)
const damage = cdr(card)()
const newHealth = health2 - damage

Достаём имя карты, damage и применяем его к здоровью игрока и после этого получаем новое здоровье.

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

Прячем реализацию

// percentCard.js

import { cons, car, cdr } from 'hexlet-pairs'

export const make = (name, percent) =>
  cons(name, percent)

export const getName = card => car(card)

export const damage = (card, health) =>
  Math.round(health * (cdr(card) / 100))

Создаём отдельный файл под реализацию карты конкретного типа. Например, у нас есть простая карта и процентная карта, которая снимает урон в зависимости от того какой процент ей сказали снимать. make – это конструктор , который возвращает пару, getName извлекает имя (это car в паре) и damage извлекает cdr из карты и применяет его к переданному здоровью (второй параметр). Это классическая абстракция.

Используем абстракцию

simpleCard.make('Ошарашивающие шорты равновесия', 7)
const cardName = simpleCard.getName(card)
const damage = simpleCard.damage(card)

// or

percentCard.make('Фаланговая знатность утешения', 80)
const cardName = percentCard.getName(card)
const damage = percentCard.damage(card, health2)

Используем make, чтобы создать карту, а getName и damage для извлечения того, что нам нужно.

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

Проблема: не знаем тип карты

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

const iter = (...) => {

  // some code ...

  const card = random(cards);
  const cardName = ?.getName(card);
  const damage = ?.damage(card, health2);

Функция getName у нас одинаковая (это могло быть не так), но функция damage разная и она зависит от того, с каким типом мы сейчас работаем. Из этого примера видно, что когда у нас появляется многообразие типов, с которыми мы хотим работать одинаковым способом, то обычный подход перестаёт работать целиком и полностью, потому что мы теперь не знаем, что здесь находится. И для этого нам нужно их как-то различать. Мы должны точно знать, что сейчас нам пришло, чтобы вызвать соответствующие функции.

Решение: помеченные данные

Есть методика, которая называется помеченные данные. Всё сводится к тому, что мы берём какие-то данные и помечаем их какой-то специальной меткой, которая называется метка типа. Именно она будет определять с чем мы работаем.

Как мы будем это использовать? У нас появляется модуль type, который в себе содержит несколько функций: attach и contents. Работают они крайне просто.

import { cons, car, cdr } from 'hexlet-pairs'
import { attach, contents } from './type'

export const make = (name, percent) =>
  attach('PercentCard', cons(name, percent))

export const getName = self => car(contents(self))

Теперь в нашей функции make мы используем attach, который первым параметром принимает метку – это имя типа, а вторым параметром те данные, которые мы там конструируем. У функции getName теперь тоже есть отличие. Мы используем функцию contents, которая принимает на вход нашу карту и именуем её self. Мы обозначаем её так, потому что она ссылается сама на себя. self – это помеченная карта, поэтому сначала нужно из неё извлечь контент и делается это с помощью contents. Отсюда видно, что наш модуль type построен очень грамотно с точки зрения модульности. Мы можем его применять абсолютно к любым данным и нам вообще не важна их структура.

Устройство type внутри

import { cons, car, cdr } from 'hexlet-pairs'

export const attach = (tag, data) =>
  cons(tag, data)

export const typeTag = taggedData =>
  car(taggedData)

export const contents = taggedData =>
  cdr(taggedData)

attach представляет собой еще одну пару поверх данных, где первый параметр – это метка типа, а второй – данные, с которыми мы работаем. Функция contents возвращает сами данные, а функция typeTag возвращает имя тега (это нужно для того, чтобы узнать тип сущности, с которой мы работаем).

Тесты

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

import * as simpleCard from './simpleCard.js'
import * as percentCard from './percentCard.js'

let cardIndex = 2
const cards = l(
  simpleCard.make('Ошарашивающие шорты', 7),
  percentCard.make('Фаланговая знатность', 80),
)
const game = make(cards, (c) => {
  cardIndex = cardIndex === 1 ? 2 : 1
  return get(cardIndex, c)
})
const log = game('John', 'Ada')

assert.equal(length(log), 5)

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

Теперь создание карт выглядит следующим образом: вызываем make с приставкой конкретного модуля.

simpleCard.make('Ошарашивающие шорты', 7),
percentCard.make('Фаланговая знатность', 80)

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

Итоги

  • Реализацию необходимо прятать
  • Иногда вызываемый код зависит от типа
  • Типы можно ввести через специальные метки
  • Типы помогают определить сущность

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

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

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

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

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

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

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

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

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