В прошлом посте мы узнали, как создавать самодокументирующиеся типы данных, мы использовали систему типов для создания типов данных, которые будут находиться в постоянном действительном состоянии (благодаря проверке при построении и при обновлении значения), теперь мы перейдем к типам, чтобы более просто выразить состояния, в которых могут находиться наши данные.
Рассмотрим следующие типы:
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 ®ular{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, даже если вы хотите писать в функциональном стиле.