Объекты-одиночки и companion-объекты

В Scala нет статических членов — вместо них есть нечто более чистое: объекты-одиночки.

«Один объект на всю программу — это не ограничение, а гарантия: общее состояние живёт в одном месте.»

Иногда нужен не шаблон для множества объектов, а ровно один экземпляр — например, для утилит или конфигурации. В Scala его создают словом object. Такой объект существует в единственном числе и создаётся лениво при первом обращении.

object MathUtils:
  val pi = 3.14159
  def square(x: Int): Int = x * x

println(MathUtils.pi)         // 3.14159
println(MathUtils.square(5))  // 25

Это замена статическим методам и полям из Java: вместо static вы просто кладёте всё в object.

Companion-объект

Если object назван так же, как класс, и лежит в том же файле, он становится companion-объектом (компаньоном). Класс и его компаньон видят приватные члены друг друга. Это идеальное место для фабричных методов.

class Circle(val radius: Double):
  def area: Double = math.Pi * radius * radius

object Circle:
  def fromDiameter(d: Double): Circle =
    Circle(d / 2)     // фабричный метод

val c = Circle.fromDiameter(10)
println(c.radius)   // 5.0

Метод apply — магия вызова

Если в объекте определить метод apply, его можно вызывать, просто написав имя объекта со скобками. Так Scala создаёт «конструкторы-фабрики».

object User:
  def apply(name: String): String = s"Пользователь: $name"

println(User("Аня"))   // вызов apply -> Пользователь: Аня

Та же идея на Python ▶

# В Python роль object играет модуль или класс со staticmethod
import math

class MathUtils:
    pi = 3.14159
    @staticmethod
    def square(x):
        return x * x

print(MathUtils.pi)
print(MathUtils.square(5))

# Аналог apply — метод __call__ на экземпляре-одиночке
class UserFactory:
    def __call__(self, name):
        return f"Пользователь: {name}"
User = UserFactory()
print(User("Аня"))   # вызов как функции
class Circle ... (много экземпляров)
object Circle ...  (ОДИН экземпляр-компаньон)
        |
   видит приватное класса
   хранит фабрики: Circle.fromDiameter(...)

Как работает под капотом (JVM)

JVM не знает про объекты-одиночки. Scala реализует object как класс с приватным конструктором и единственным статическим экземпляром в поле MODULE$. Companion-объект и класс компилируются в два отдельных .class-файла (Circle.class и Circle$.class), которые специально получают доступ к приватным членам друг друга. Метод apply — обычный метод; компилятор просто разрешает звать его без имени apply.

Частые ошибки

  • Искать static. В Scala его нет — используйте object.
  • Класть companion в другой файл. Он должен быть в том же файле, что и класс, иначе не получит доступ к приватным членам.
  • Хранить изменяемое состояние в object. Один экземпляр на всю программу — это глобальное состояние, источник багов в многопоточности.

Best practices

  • Используйте object для утилит, констант и фабрик.
  • Размещайте фабричные методы (apply, fromXxx) в companion-объекте.
  • Избегайте изменяемого состояния в одиночках, чтобы не плодить глобальные переменные.

Одиночки и чистота глобального состояния

Объекты-одиночки решают реальную проблему: иногда нужна именно одна точка для утилит, констант или фабрик. Scala делает это чище, чем статические члены Java, потому что object — полноценный объект, который может наследовать трейты и участвовать в паттерн-матчинге. Это не «особый случай» языка, а естественная часть его модели.

Companion-объект — это паттерн, который вы будете встречать постоянно. Стандартная библиотека построена на нём: List(1, 2, 3) работает потому, что у List есть companion с методом apply. Размещая фабрики в компаньоне, вы держите «как создать объект» рядом с самим классом, но отдельно от его экземпляров. Главное правило гигиены — не превращать одиночку в свалку изменяемого глобального состояния, иначе вернутся все проблемы глобальных переменных.

Companion-объекты вы будете встречать в стандартной библиотеке буквально на каждом шагу, поэтому понимание их роли окупается сразу. Каждый раз, когда вы пишете List(...), Some(...) или Map(...), за кулисами вызывается apply соответствующего companion-объекта. Узнав этот паттерн один раз, вы перестаёте видеть в подобных вызовах магию и начинаете понимать устройство библиотеки изнутри.

Итоги. object — одиночка, заменяющий static; companion-объект делит имя с классом и хранит фабрики; apply позволяет звать объект как функцию. Дальше — наследование и трейты.

Проверьте себя
1. Что такое companion-объект?
AЛюбой объект в проекте
Bobject с тем же именем, что и класс, в том же файле, с доступом к его приватным членам
CОбъект, который нельзя создать
DКопия класса
2. Что позволяет метод apply в объекте?
AЗапускать программу
BВызывать объект как функцию: User("Аня") вместо User.apply("Аня")
CДелать объект приватным
DНаследоваться от класса