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

Неестественность ООП-моделирования
Одно из определений ООП — воссоздание или моделирование реального мира в коде, со всем свойственным поведением моделируемых объектов. Попробуем смоделировать собаку c ошейником из реального мира, и попробуем поменять ошейник не меняя состояния.
type DogCollar struct { color string } func NewDogCollar(color string) DogCollar { return DogCollar{color: color} } type Dog struct { nickName string dogCollar DogCollar } func NewDog(nickName string, dogCollar DogCollar) Dog { return Dog{nickName: nickName, dogCollar: dogCollar} } func (d Dog) WithCollar(dogCollar DogCollar) Dog { return Dog{dogCollar: dogCollar, nickName: d.nickName} } func main() { dog := NewDog("Вася", NewDogCollar("red")) dog = dog.WithCollar(NewDogCollar("blue")) }
Так, подождите, я только что создал новую собаку? Что я должен думать глядя на метод WithCollar? Почему я создаю новую собаку вместо того, чтобы просто поменять ей ошейник? Это контринтуитивно!
ООП призван делать код естественным и интуитивно понятным, но иммутабельное поведение вносит неестественность для реального мира. Пример гипертрофирован, но представьте что будет, если мы оперируем сложными и опасными объектами на своей работе, например банковским счётом?
Производительность
А что на счёт нее? Давайте попробуем создать карту, на которой будут размещены точки гео-объектов. Естественно мы будем делать это иммутабельно:
package main import "slices" type GeoPoint struct { lat float64 long float64 } func NewGeoPoint(lat, long float64) GeoPoint { return GeoPoint{lat: lat, long: long} } type Map struct { geoPoints []GeoPoint } func NewMap() Map { return Map{} } func (m Map) WithNewPoint(point GeoPoint) Map { return Map{geoPoints: append(slices.Clone(m.geoPoints), point)} } func main() { geoMap := NewMap() geoMap = geoMap.WithNewPoint(NewGeoPoint(1, 1)). WithNewPoint(NewGeoPoint(2, 2)). WithNewPoint(NewGeoPoint(3, 3)). WithNewPoint(NewGeoPoint(4, 4)) }
Так, так…Подождите. Вы хотите сказать, что при добавлении новой точки на карту, я создаю новую карту и каждый раз аллоцирую и копирую память в куче, 4 раза!? Интересно, что скажет garbage collector и оперативная память на этот счёт, например если у нас миллион точек?
Идентичность
А как дела обстоят тут? Давайте представим что у нас есть коллекция пользователей, которых мы планируем менять. Знаю, вы скажете что этот код можно написать без указателей, но это не всегда возможно! Это просто демонстрация.
package main import ( "fmt" ) // Иммутабельная структура пользователя type User struct { ID int Name string Age int } // "Изменяем" пользователя (создаём новый объект) func (u User) WithAge(newAge int) User { return User{ ID: u.ID, Name: u.Name, Age: newAge, } } func main() { // Создаём пользователя original := User{ID: 1, Name: "Ivan", Age: 25} // "Изменяем" возраст — появится новый User updated := original.WithAge(26) // Сравниваем ссылки (Go всегда копирует структуры, но для примера) fmt.Printf("original == updated: %v\n", original == updated) // false по Age, true по ID+Name // Проблема: если мы используем карты/сеты по ссылке — это уже новый объект users := map[*User]string{ &original: "active", } // Спрашиваем по другому объекту — получаем false _, ok := users[&updated] // false, потому что это СОВЕРШЕННО другой объект в памяти fmt.Printf("users[&updated] exists: %v\n", ok) // Даже если User.ID тот же — Go различает эти объекты как разные ссылки }
Атомарность изменений
Один из аргументов за написание иммутабельного когда является атомарность изменений. Продемонстрирую пример:
type Developer struct { name string email string grade string } // Иммутабельный способ func (d Developer) UpGradeImmutable() (Developer, error) { if //some logic// { return Developer{}, fmt.Errorf("upgrade error") } //Мы создали обьект атомарно, без промежуточных состояний return Developer{ name: d.name, email: d.email, grade: "senior", }, nil } // Мы вернули ошибку, но изменили состояние перед возвратом. func (d Developer) UpGrade() error { d.grade = "senior" if //some logic// { return fmt.Errorf("upgrade error") } }
Иммутабельный метод преподносится, как более безопасный и менее подверженный логическим ошибкам способ программирования. На мой взгляд плата за такой код очень велика, ввиду проблем описанных выше.
thread-safety
Потокобезопасность — так же позиционируется как плюс иммутабельного подхода.
Вернемся к карте с гео-обьектами, опишем структуру с изменяемым состоянием да ещё и потокобезопасно:
package main import ( "sync" ) type GeoPoint struct { lat float64 long float64 } func NewGeoPoint(lat, long float64) GeoPoint { return GeoPoint{lat: lat, long: long} } type Map struct { mu sync.RWMutex geoPoints []GeoPoint } func NewMap() Map { return Map{} } func (m *Map) AddPoint(point GeoPoint) { m.mu.Lock() defer m.mu.Unlock() m.geoPoints = append(m.geoPoints, point) } func (m *Map) FindGeoObject(lat, long float64) GeoPoint { m.mu.RLock() defer m.mu.RUnlock() //some logic// }
А теперь иммутабельно:
package main import "slices" type GeoPoint struct { lat float64 long float64 } func NewGeoPoint(lat, long float64) GeoPoint { return GeoPoint{lat: lat, long: long} } type Map struct { geoPoints []GeoPoint } func NewMap() Map { return Map{} } func (m Map) WithNewPoint(point GeoPoint) Map { return Map{geoPoints: append(slices.Clone(m.geoPoints), point)} } func (m *Map) FindGeoObject(lat, long float64) GeoPoint { //some logic// }
В последнем варианте мы не используем Mutex. Но в чём плюс данного подхода? Мы не используем Mutex что-бы что? Тогда для чего нам даны примитивы синхронизации вообще? Почему мы не должны ими пользоваться? Загадка.
Итог
Иммутабельность в ООП подходит далеко не всегда и не для всех бизнес-задач — особенно там, где важна эффективность, “живая” модель объектов, и естественная работа с идентичностью и состоянием. Поэтому выбор между иммутабельностью и изменяемостью должен основываться на специфике вашей задачи.
ссылка на оригинал статьи https://habr.com/ru/articles/909228/
Добавить комментарий