Наследование и трейты

Трейт в Scala — это интерфейс, который умеет содержать готовый код, и его можно «подмешивать» к классам.

«Композиция через трейты — как сборка из кубиков: каждый кубик добавляет своё умение.»

Наследование позволяет одному классу перенять поведение другого. Базовый класс наследуется через extends, а метод переопределяется словом override.

class Animal(val name: String):
  def sound: String = "..."

class Dog(name: String) extends Animal(name):
  override def sound: String = "Гав"

val d = Dog("Рекс")
println(s"${d.name}: ${d.sound}")  // Рекс: Гав

Трейты — главный инструмент

Трейт (trait) — это как интерфейс из Java, но он может содержать и абстрактные, и уже реализованные методы, и поля. Класс может «подмешать» несколько трейтов — это множественное наследование поведения, которого нет в Java для классов.

trait Greeter:
  def name: String                       // абстрактный — реализует класс
  def greet: String = s"Привет, я $name" // готовая реализация

trait Walker:
  def walk: String = "Иду пешком"

class Person(val name: String) extends Greeter, Walker

val p = Person("Лена")
println(p.greet)  // Привет, я Лена
println(p.walk)   // Иду пешком

Несколько трейтов перечисляются через запятую: extends Greeter, Walker. Класс получает методы всех трейтов сразу.

Абстрактные методы как контракт

Трейт может объявить метод без тела — это обязательство, которое класс должен выполнить.

trait Shape:
  def area: Double          // контракт: реализуй меня

class Square(side: Double) extends Shape:
  def area: Double = side * side

class Circle(r: Double) extends Shape:
  def area: Double = math.Pi * r * r

val shapes: List[Shape] = List(Square(2), Circle(1))
shapes.foreach(s => println(s.area))

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

# Аналог трейтов на Python — абстрактные классы и множественное наследование
from abc import ABC, abstractmethod
import math

class Shape(ABC):
    @abstractmethod
    def area(self): ...

class Greeter:
    def greet(self):
        return f"Привет, я {self.name}"

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side * self.side

print(Square(2).area())  # 4
            Shape (трейт: def area)
           /      \
      Square        Circle
      area=side^2   area=pi*r^2

класс может extends Greeter, Walker  (несколько трейтов)

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

Трейты компилируются в интерфейсы JVM. Реализованные методы трейта попадают в этот интерфейс как default-методы (доступные с Java 8). Когда класс подмешивает несколько трейтов, компилятор Scala линеаризует их в строгом порядке, разрешая конфликты одинаковых методов. Это решает «проблему ромба» множественного наследования предсказуемо: есть чёткое правило, чей метод победит.

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

  • Забыть override. При переопределении конкретного метода override обязателен — иначе ошибка.
  • Конфликт методов из разных трейтов. Если два трейта дают метод с одним именем, нужно явно разрешить конфликт через override.
  • Параметры у трейта. Классические трейты не имели параметров конструктора; в Scala 3 это возможно, но используйте осторожно.

Best practices

  • Используйте трейты для описания способностей («умеет летать», «умеет логировать»).
  • Предпочитайте композицию из небольших трейтов глубоким иерархиям наследования.
  • Помещайте общий контракт в трейт, конкретику — в классы-реализации.

Композиция вместо глубокого наследования

Трейты подталкивают к иному стилю проектирования, чем классическое наследование. Вместо того чтобы строить глубокое дерево классов, где каждый уровень добавляет понемногу, вы описываете отдельные способности маленькими трейтами и собираете из них классы, как из кубиков. «Умеет логировать», «умеет сравниваться», «имеет имя» — каждая способность живёт в своём трейте и подмешивается туда, где нужна.

Этот подход устойчивее к изменениям. Глубокие иерархии хрупки: правка в основании задевает всех потомков. Композиция из независимых трейтов локализует изменения. Линеаризация, которую делает компилятор при подмешивании нескольких трейтов, гарантирует предсказуемый порядок и разрешает конфликты — поэтому множественное наследование поведения в Scala не приводит к хаосу, в отличие от наивного множественного наследования классов в других языках.

Хорошая привычка — проектировать трейты вокруг одной чёткой способности и давать им говорящие имена. Тогда классы читаются как перечисление умений: «это сущность, которая умеет логировать, сравниваться и сериализоваться». Такой словарь способностей делает архитектуру прозрачной и облегчает переиспользование: новую способность достаточно описать один раз и подмешивать туда, где она нужна.

Итоги. Наследование — через extends и override; трейты — гибкие интерфейсы с реализацией, которые подмешиваются по нескольку штук. Дальше — sealed-иерархии для безопасного моделирования.

Проверьте себя
1. Чем трейт отличается от интерфейса Java (классического)?
AНичем
BТрейт может содержать реализованные методы и поля, а не только сигнатуры
CТрейт нельзя наследовать
DТрейт хранит данные в базе
2. Как класс подмешивает несколько трейтов в Scala 3?
Aextends A and B
Bextends A, B
Cextends A + B
Dimplements A, B