Наследование и трейты
Трейт в 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-иерархии для безопасного моделирования.