Go: Явное определение состояния с помощью системы типов

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

Рассмотрим следующие типы:

type chatroom struct {
    id      uuidStr
    name    nameStr
    members membersList
}

type membersList []member

type member struct {
    id   uuidStr
    role membership
}

type membership int

const (
    owner membership = iota
    removed
    banned
    regular
)
Войти в полноэкранный режим Выход из полноэкранного режима

Большинство из них объясняются сами собой, но нас интересует member, потому что в зависимости от значения поля role другие данные внутри member могут интерпретироваться по-разному и потребовать от нас сохранения различных инвариантов, это довольно распространенная ситуация, мы говорим, что member имеет состояние, потому что то, как мы обрабатываем member, отличается из-за значения одного или нескольких полей.

Этот простой пример не решает данной проблемы, представьте себе структуру данных гораздо большего размера, например, заказ на доставку, у вас, вероятно, будет множество полей с различными данными, заполняемыми в разное время в зависимости от состояния заказа, в лучшем случае у вас будет перечисление для указания состояния заказа, а в худшем — несколько булевых флагов для каждого состояния.

Вообще говоря, есть два способа решить эту проблему:

  • функциональный способ
  • объектно-ориентированный способ

Функциональный способ

В функциональном программировании данные — это просто данные, ничего больше и ничего меньше.

Все объекты в наших программах имеют тип, а тип — это имя для набора значений, тип может быть либо продуктовым (иногда известным как составной, или тип записи), либо суммовым (иногда известным как дискриминированное объединение, тип OR, тип выбора).

продуктовые и суммовые типы (RUST)

// Sum type
enum AwesomeFruit {
    Apple,
    Banana,
    Orange,
}

// Product type
struct Fruit {
    name: String,
    calories: i32,
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

сопоставление объекта типа choice с шаблонами (RUST)

match apple {
    AwesomeFruit::Apple => println!("You created an apple"),
    AwesomeFruit::Banana => println!("You created a banana"),
    AwesomeFruit::Orange => println!("You created an Orange"),
    };
Вход в полноэкранный режим Выход из полноэкранного режима

Компилятор откажется компилировать код, если мы не обработаем все возможные перестановки (или, по крайней мере, не предоставим обработчик catch all по умолчанию, вы можете сделать это в rust с помощью _ => println!("")).

Типы выбора довольно мощные, к сожалению, go не поддерживает типы сумм (хотя в настоящее время ведутся дебаты о добавлении их поддержки), большинство кода на go написано в стиле ООП/императивном стиле, но пусть это вас не обескураживает, есть много людей, которые пишут функциональные программы на go, функциональный код на go идиоматичен.

Несмотря на то, что в go нет типов выбора или согласования шаблонов, существует множество способов их имитации, и мы рассмотрим наиболее распространенный способ, который включает в себя моделирование типа выбора как типа интерфейса, а варианты/пермутации типа — это объекты, реализующие этот интерфейс:

type member interface{ member() }

type owner   struct{ id uuidStr }
type banned  struct{ id uuidStr }
type removed struct{ id uuidStr }
type regular struct{ id uuidStr }

func (*owner) member() {}
func (*banned) member() {}
func (*removed) member() {}
func (*regular) member() {}
Войти в полноэкранный режим Выход из полноэкранного режима

Затем мы можем создать конструкторы для каждого из вариантов

func makeOwner(id uuidStr) member {
    return &owner{id}
}
func makeRegular(id uuidStr) member {
    return &regular{id}
}
func makeBanned(id uuidStr) member {
    return &banned{id}
}
func makeRemoved(id uuidStr) member {
    return &removed{id}
}
Вход в полноэкранный режим Выход из полноэкранного режима

И всякий раз, когда у нас есть объект-член, мы можем «сопоставить» его с помощью переключателя типов:

switch v := m.(type) {
    case owner:
    // Do stuff for the owner
    case regular:
    // Do stuff for the regular
    case banned:
    // Do stuff for the banned
    case removed:
    // Do stuff for the removed

    default:
      panic("missing handling for " + fmt.Sprintf("%T"))
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Это имитирует поиск по образцу, но имеет несколько важных недостатков:

  • Это не поиск по образцу и не такой мощный, как поиск по образцу, также компилятор не будет проверять, что вы обработали все варианты в переключателе типов.
  • Приведенная выше защита по умолчанию будет срабатывать только во время выполнения, вызывая панику, а паника — это антипаттерн в go и даже в функциональном программировании.

Мы можем решить эти две проблемы, полагаясь на статические анализаторы, такие как go-sumtypes.

Объектно-ориентированный путь

В объектно-ориентированном программировании (в отличие от функционального программирования) данные защищены & инкапсулированы и сгруппированы с публичным поведением, которое мутирует данные.

Для работы с объектом, который имеет множество возможных состояний, мы используем паттерн стратегии. Этот паттерн позволяет объекту легко иметь различное поведение, извлекая API требуемого поведения в класс или интерфейс, а затем реализаторы наследуют этот класс или интерфейс и реализуют методы, после чего мы можем динамически загружать экземпляры подкласса во время выполнения и использовать их.

Вы, вероятно, уже использовали паттерн стратегии, он довольно часто встречается в go!

f, err := os.Open("foo")
if err != nil {
    return err
}
defer f.Close()

sink := make([]byte, 250)
defer.Read(sink)
Вход в полноэкранный режим Выход из полноэкранного режима

*os.File реализует интерфейс io.Reader, который имеет только один метод Read любой объект, реализующий Read может быть использован как io.Reader.

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

type interface member {
    canSendMsg() bool
}

type banned struct {}
func (*banned) canSendMsg() bool { return false }

type removed struct{}
func (*removed) canSendMsg() bool { return false }

type owner struct {}
func (*owner) canSendMsg() bool { return true }

type regular struct{}
func (*regular) canSendMsg() bool { return true }
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Оцените статью
Procodings.ru
Добавить комментарий