Value Object
Цвет — классический пример Value Object. У него нет собственного идентификатора: его уникальность определяется только комбинацией полей Red, Green и Blue.
Ранее мы говорили, что богатая доменная модель не должна позволять произвольно изменять своё состояние и должна защищать себя от некорректных данных. То же правило применимо к Value Object, но в нем ограничения ещё строже — объект должен:
- быть создан в корректном состоянии
- быть неизменяемым (иммутабельным).
Давайте посмотрим, как это реализуется на практике.
Что такое RGB цвет
RGB — это модель цвета, где каждый цвет задаётся комбинацией трёх компонент: Red (красный), Green (зелёный), Blue (синий).
Каждая компонента обычно представлена числом от 0 до 255 (8 бит).
- 0 — компонент отсутствует
- 255 — компонент присутствует максимально
Комбинируя эти три компоненты, можно получить любой цвет в спектре RGB.
Пример:
- (255, 0, 0) — чистый красный
- (0, 255, 0) — чистый зелёный
- (0, 0, 255) — чистый синий
- (255, 255, 255) — белый
- (0, 0, 0) — черный
1. Структура и поля
Я реализовал структуру Color.
В ней три поля: red, green и blue. Каждое хранится как uint8 (0–255).
type Color struct {
red uint8
green uint8
blue uint8
}
3. Доступ к полям, геттеры
Color — это immutable Value Object, поэтому внешние клиенты не должны иметь возможность менять его поля. Но Value Object может предоставлять доступ к свои полям для чтения. Геттеры предоставляют безопасный доступ к значениям цвета без нарушения инвариантов объекта.
Поля red, green, blue приватные (не экспортируемые), поэтому напрямую их извне пакета нельзя использовать. Геттеры позволяют работать с ними по контракту, сохраняя инкапсуляцию.
Такой подход делает Color консистентным и предсказуемым: никто извне не может присвоить компоненте некорректное значение.
type Color struct {
red uint8
green uint8
blue uint8
valid bool
}
func (c Color) Red() uint8 {
return c.red
}
func (c Color) Green() uint8 {
return c.green
}
func (c Color) Blue() uint8 {
return c.blue
}
2. Конструктор и создание
NewColor — это фабричный метод, который создаёт Value Object Color.
Цвет в нашей модели — это самостоятельный объект с чёткими границами и правилами. Конструктор гарантирует, что любая созданная структура соответствует этим правилам.
Использование uint8 для компонентов естественно ограничивает диапазон значений (0–255), поэтому отдельная валидация не нужна. Но если бы естественных ограничений не было, то мы должны были бы проверить все входящие параметры метода на допустимость.
Метод инкапсулирует корректное создание структуры и позволяет валидировать параметры перед ее созданием.
type Color struct {
red uint8
green uint8
blue uint8
valid bool
}
func (c Color) Red() uint8 {
return c.red
}
func (c Color) Green() uint8 {
return c.green
}
func (c Color) Blue() uint8 {
return c.blue
}
func NewColor(red uint8, green uint8, blue uint8) Color {
// Валидация параметров происходит естественным образом через тип uint8 (диапазон 0–255)
// Поэтому тут ее нет
color := Color{
red: red,
green: green,
blue: blue,
valid: true,
}
return color
}
3. Предопределённые значения
Функции White и Black создают предопределённые Value Object для часто используемых цветов. Это упрощает код и делает его самодокументируемым: c := White() сразу понятно, какой цвет используется, без необходимости указывать цифры (255, 255, 255).
Такие функции помогают закрепить семантические значения в домене и поддерживают неизменяемость объектов, так как каждый вызов создаёт новый экземпляр.
Эти функции используют конструктор NewColor, что обеспечивает единообразие создания объектов и предотвращает дублирование кода. Такой подход делает код чистым и расширяемым — при необходимости легко добавить другие часто используемые цвета, например Red() или Green().
type Color struct {
red uint8
green uint8
blue uint8
valid bool
}
func (c Color) Red() uint8 {
return c.red
}
func (c Color) Green() uint8 {
return c.green
}
func (c Color) Blue() uint8 {
return c.blue
}
func NewColor(red uint8, green uint8, blue uint8) Color {
// Валидация параметров происходит естественным образом через тип uint8 (диапазон 0–255)
// Поэтому тут ее нет
color := Color{
red: red,
green: green,
blue: blue,
valid: true,
}
return color
}
func White() Color {
color := NewColor(255, 255, 255)
return color
}
func Black() Color {
color := NewColor(0, 0, 0)
return color
}
4. Сравнение Value Object
Color — это Value Object, а у Value Object важна семантика равенства по значению, а не по ссылке. Метод Equals позволяет сравнивать два цвета по всем компонентам одновременно, что соответствует идеологии DDD.
В Go структуры можно сравнивать напрямую с помощью ==, если все поля сравнимы (не яляются ссылкой или списком). Также вы можете сравнить каждое значение попарно, если это требуется.
type Color struct {
red uint8
green uint8
blue uint8
valid bool
}
func (c Color) Red() uint8 {
return c.red
}
func (c Color) Green() uint8 {
return c.green
}
func (c Color) Blue() uint8 {
return c.blue
}
func NewColor(red uint8, green uint8, blue uint8) Color {
// Валидация параметров происходит естественным образом через тип uint8 (диапазон 0–255)
// Поэтому тут ее нет
color := Color{
red: red,
green: green,
blue: blue,
valid: true,
}
return color
}
func White() Color {
color := NewColor(255, 255, 255)
return color
}
func Black() Color {
color := NewColor(0, 0, 0)
return color
}
func (c Color) Equals(other Color) bool {
return c == other
}
5. Логика в Value Object
Blend — это операция над Value Object, которая возвращает новый Color, являющийся смесью текущего и другого цвета. Это связано с тем, что Value Object не изменяемый (immutable). Любая операция создаёт новый объект, а не меняет исходный обьект. Это важное правило!
Метод инкапсулирует бизнес-логику “смешивания цветов”, соблюдая инварианты — нельзя смешивать с некорректным цветом (IsValid проверяет это). Ошибки обрабатываются явно, что соответствует принципу безопасного использования Value Object.
type Color struct {
red uint8
green uint8
blue uint8
valid bool
}
func (c Color) Red() uint8 {
return c.red
}
func (c Color) Green() uint8 {
return c.green
}
func (c Color) Blue() uint8 {
return c.blue
}
func NewColor(red uint8, green uint8, blue uint8) Color {
// Валидация параметров происходит естественным образом через тип uint8 (диапазон 0–255)
// Поэтому тут ее нет
color := Color{
red: red,
green: green,
blue: blue,
valid: true,
}
return color
}
func White() Color {
color := NewColor(255, 255, 255)
return color
}
func Black() Color {
color := NewColor(0, 0, 0)
return color
}
func (c Color) Equals(other Color) bool {
return c == other
}
func (c Color) IsValid() bool {
return c.valid
}
func (c Color) Blend(other Color) (Color, error) {
if !other.IsValid() {
return Color{}, errs.NewValueIsRequiredError("other")
}
return NewColor(
(c.red+other.red)/2,
(c.green+other.green)/2,
(c.blue+other.blue)/2,
), nil
}
6. Форматирование и вывод
Метод String делает структуру Color удобной для отображения и логирования. Он возвращает строковое представление цвета в привычном виде «R, G, B», что облегчает отладку и вывод значений в логах или на экран.
Этот метод не изменяет Value Object и не нарушает его неизменяемость, а добавляет сахар для удобства разработчика: вместо ручного форматирования компонентов каждый раз можно просто вызвать c.String().
type Color struct {
red uint8
green uint8
blue uint8
valid bool
}
func (c Color) Red() uint8 {
return c.red
}
func (c Color) Green() uint8 {
return c.green
}
func (c Color) Blue() uint8 {
return c.blue
}
func NewColor(red uint8, green uint8, blue uint8) Color {
// Валидация параметров происходит естественным образом через тип uint8 (диапазон 0–255)
// Поэтому тут ее нет
color := Color{
red: red,
green: green,
blue: blue,
valid: true,
}
return color
}
func White() Color {
color := NewColor(255, 255, 255)
return color
}
func Black() Color {
color := NewColor(0, 0, 0)
return color
}
func (c Color) Equals(other Color) bool {
return c == other
}
func (c Color) IsValid() bool {
return c.valid
}
func (c Color) Blend(other Color) (Color, error) {
if !other.IsValid() {
return Color{}, errs.NewValueIsRequiredError("other")
}
return NewColor(
(c.red+other.red)/2,
(c.green+other.green)/2,
(c.blue+other.blue)/2,
), nil
}
func (c Color) String() string {
return fmt.Sprintf("%v, %v, %v\n", c.red, c.green, c.blue)
}
7. Защита инвариантов
Давайте теперь рассмотрим использование структуры Color.
Так сработает, потому что Go разрешает создавать пустую структуру с помощью литерала даже с приватными полями. Но поля будут заполнены нулями, а valid — false. В DDD это некорректный объект, поэтому так делать не надо, используйте конструктор.
color1 := color.Color{}
println(color1.String())
Не скомпилируется, потому что поля приватные (red, green, blue). Go не позволяет обращаться к ним из другого пакета. Это показывает важность инкапсуляции: Value Object должен создаваться только через фабрику.
color2 := color.Color{red: 0, green: 0, blue: 0}
Ошибка при компиляции, так как uint8 не может быть отрицательным. Go типами обеспечивает естественную валидацию диапазона, поэтому конструктор защищает от некорректных значений на этапе компиляции.
color3 := color.NewColor(-1, -1, -1)
Цвет будет корректно создан, но вот сменить r (red) на 125 уже не выйдет, так как поля приватные. Это так же демонстрирует неизменяемость Value Object — его невозможно изменить после создания.
color4 := color.NewColor(255, 255, 255)
color4.r = 125
Ошибка времени выполнения, потому что Blend проверяет валидность другого цвета (IsValid). Если передать некорректный объект, метод вернёт ошибку. DDD принцип: операции с Value Object должны сохранять инварианты и не создавать некорректные объекты.
color5, err := color3.Blend(color.Color{})
errs.CheckIfError(err)
8. Использование Value Object
Корректное создание через конструктор. Цвет валиден, все инварианты соблюдены. Это правильный способ работы с Value Object.
color6 := color.NewColor(255, 255, 255)
Метод Equals показывает семантическое равенство Value Object. В данном случае false, так как компоненты различаются. DDD принцип: сравниваем по значению, а не по ссылке.
black := color.Black()
white := color.White()
isEqual := black.Equals(white)
println("Is black equal white: ", isEqual)
Метод создаёт новый Color, не изменяя исходные объекты, соблюдая immutability. Ошибки обрабатываются через error. Результат — новый цвет.
blendedColor, err := black.Blend(white)
if err != nil {
log.Fatal(err)
}
println("Blended color is: ", blendedColor.String())
По любым вопросам обращайтесь в комментариях к уроку или в Telegram
Если вы обнаружили ошибку или неточность в данном уроке, сообщите о ней в комментариях к данному уроку. Это поможет сделать курс лучше.