Что такое self-type и где он полезен?
В Scala self-type (или self-type annotations) — это механизм, позволяющий указывать, какие другие типы обязательно должны быть смешаны с данным trait или class. Это не наследование, а ограничение компиляции, обеспечивающее, что нужная зависимость будет присутствовать. Self-type используется для внедрения зависимостей, разделения обязанностей, модульности, тестируемости и композиции поведения.
Синтаксис self-type
trait A {
def doA(): Unit
}
trait B {
self: A =>
def doB(): Unit = {
doA() // компилятор разрешает, потому что A гарантирован
println("Doing B")
}
}
Это означает: trait B можно использовать только с типом A. Иначе — ошибка компиляции.
Как это работает
Когда мы объявляем self: A =>, мы не наследуем A, но обязываем, чтобы любой, кто микширует B, также обеспечивал реализацию A.
Использование:
class MyClass extends A with B {
def doA(): Unit = println("Doing A")
}
val x = new MyClass
x.doB()
// Output:
// Doing A
// Doing B
Если попытаться сделать class MyClass extends B, не реализовав A, то:
Error: illegal inheritance; self-type MyClass does not conform to A
Отличие от наследования
trait B extends A
Это означает, что B наследует A, и может вызывать doA напрямую. Но это жесткое связывание: B всегда содержит A.
А self: A => — это мягкое требование, навязанное пользователю: "ты должен обеспечить A".
Где self-type полезен
1. Dependency injection (внедрение зависимостей)
trait Database {
def query(sql: String): List\[String\]
}
trait UserService {
self: Database =>
def getUsers(): List\[String\] = query("SELECT \* FROM users")
}
class ProdDatabase extends Database {
def query(sql: String) = List("user1", "user2")
}
class App extends ProdDatabase with UserService
val app = new App
app.getUsers()
// List("user1", "user2")
Таким образом, UserService ничего не знает про конкретную реализацию базы — только про то, что она будет.
2. Тестируемость
Можно подмешивать фиктивные реализации:
class MockDB extends Database {
def query(sql: String) = List("testUser")
}
class TestService extends MockDB with UserService
val test = new TestService
test.getUsers() // List("testUser")
3. Композиция поведения
Self-type позволяет строить сквозные абстракции, не полагаясь на иерархию наследования:
trait Auth {
def currentUser: String
}
trait Logging {
def log(msg: String): Unit = println(msg)
}
trait SecureLogging {
self: Auth with Logging =>
def secureLog(msg: String): Unit =
log(s"\[${currentUser}\] $msg")
}
Использование:
class App extends Auth with Logging with SecureLogging {
def currentUser = "admin"
def log(msg: String) = println(s"LOG: $msg")
}
new App().secureLog("Access granted")
// LOG: \[admin\] Access granted
4. Множественное связывание зависимостей
trait A
trait B
trait RequiresBoth {
self: A with B =>
}
Такой trait не может быть использован без одновременного наличия A и B. Это проверяется на этапе компиляции.
Использование с class
Self-type работает не только в trait, но и в class:
class MyClass {
self: Logging =>
def doSomething(): Unit = log("Hello")
}
Такой класс не может быть создан без Logging:
trait Logging {
def log(s: String): Unit
}
class MyLogger extends Logging {
def log(s: String): Unit = println(s)
}
val obj = new MyClass with MyLogger
obj.doSomething() // Hello
Self-type alias
Можно использовать другое имя для self-ссылки:
trait Service {
selfAlias: Logging =>
def run(): Unit = selfAlias.log("Running")
}
Имя selfAlias действует как this, но привязан к типу Logging.
Пример с Play Framework (или других DI)
Self-type идеально ложится в архитектуру, где модули не должны жёстко зависеть от друг друга. Например, можно собрать слои приложения из trait'ов:
trait Repos {
def getData(): String
}
trait Services {
self: Repos =>
def process(): String = s"Processed: ${getData()}"
}
trait Controllers {
self: Services =>
def run(): Unit = println(process())
}
И собрать приложение:
object App extends Repos with Services with Controllers {
def getData() = "123"
}
App.run()
Self-type + Cake Pattern
Механизм self-type лежит в основе cake pattern — архитектурного подхода в Scala, при котором зависимости собираются по слоям и "перемешиваются" через self-type. Это альтернатива DI-фреймворкам (например, Guice, Spring).
Отличие от this
Self-type — это типовая аннотация, используемая компилятором для проверки структуры. this — это указатель на текущий экземпляр. С self-type можно даже присвоить другое имя вместо this.
trait T {
self => // обычное имя по умолчанию
}
Можно так:
trait T {
my => // теперь вместо this можно писать my
}
Self-type — один из самых мощных инструментов в Scala, позволяющий строить гибкие, слабо связанные и типобезопасные архитектуры, особенно в крупных проектах и библиотеках.