
В парадигме ООП объекты взаимодействуют друг с другом. Первоначальная идея такого взаимодействия, впервые появившаяся в языке Smalltalk, заключалась в том, что объект A отправлял сообщение объекту B. В языках, разработанных позднее, используется вызов методов. В обоих случаях возникает один и тот же вопрос: как объект ссылается на другие объекты, чтобы достичь желаемых результатов?
В этой статье я рассматриваю проблему передачи зависимостей объекту. Я рассмотрю несколько вариантов и проанализирую их преимущества и недостатки.
Внедрение через конструктор
Для внедрения зависимости через конструктор вы передаете зависимости в качестве параметров конструктору.
class Delivery(private val addressService: AddressService,
private val geoService: GeoService,
private val zoneId: ZoneId) {
fun computeDeliveryTime(user: User, warehouseLocation: Location): ZonedDateTime {
val address = addressService.getAddressOf(user)
val coordinates = geoService.getCoordinates(location)
// возвращайте дату и время
}
}
Внедрение через конструктор — это, безусловно, самый распространенный способ передачи объекту его зависимостей: на протяжении примерно десяти лет моей практики все кодовые базы, с которыми я сталкивался, использовали внедрение конструктора.
У меня есть небольшая проблема с внедрением через конструктор: такая операция хранит зависимости в виде полей, как и состояние. Глядя на сигнатуру конструктора, невозможно отличить состояние от зависимостей без правильной типизации.
Это меня напрягает. Давайте рассмотрим другие способы.
Передача параметров
Вместо того, чтобы хранить зависимости вместе с состоянием, мы можем передавать зависимость при вызове метода.
class Delivery(private val zoneId: ZoneId) {
fun computeDeliveryTime(addressService: AddressService,
geoService: GeoService,
user: User, warehouseLocation: Location): ZonedDateTime {
val address = addressService.getAddressOf(user)
val coordinates = geoService.getCoordinates(location)
// возвращайте дату и время
}
}
Разделение состояния и зависимостей теперь очевидно: первое хранится в полях, а второе передается в качестве параметров функции. Однако ответственность за обработку зависимости перемещается на один уровень выше в цепочке вызовов. Чем длиннее цепочка вызовов, тем более громоздкой она становится.
class Order() {
fun deliver(delivery: Delivery, user: User, warehouseLocation: Location): OrderDetails {
// Как-нибудь получите адрес и геослужбы
val deliveryTime = delivery.computeDeliveryTime(addressService, geoService, user, warehouseLocation)
// возвращайте другие детали
}
}
Обратите внимание: длина цепочки вызовов также представляет проблему при внедрении через конструктор. Вам необходимо разработать код для места вызова так, чтобы он был как можно ближе к месту создания зависимости.
ThreadLocal
В устаревшем коде используется ThreadLocal:
Этот класс предоставляет переменные, локальные в пределах потока. Эти переменные отличаются от своих обычных аналогов тем, что каждый поток, который обращается к одной из них (через метод get или set), имеет свою собственную, независимо инициализированную копию переменной. Экземпляры ThreadLocal обычно являются приватными статическими полями в классах, которые должны связать состояние с потоком (например, ID пользователя или ID транзакции).
Например, приведенный ниже класс генерирует уникальные идентификаторы, локальные в пределах каждого потока. Идентификатор потока назначается при первом вызове ThreadId.get() и остается неизменным при последующих вызовах
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Атомарное целое число, содержащее следующий идентификатор потока, который будет назначен
private static final AtomicInteger nextId = new AtomicInteger(0);
// Локальная переменная потока, содержащая идентификатор каждого потока
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Возвращает уникальный идентификатор текущего потока, присваивая его при необходимости.
public static int get() {
return threadId.get();
}
}
Мы можем переписать вышеуказанный код, используя ThreadLocal:
class Delivery(private val zoneId: ZoneId) {
fun computeDeliveryTime(user: User, warehouseLocation: Location): ZonedDateTime {
val addressService = AddressService.get()
val geoService = GeoService.get()
// возвращайте дату и время
}
}
ThreadLocal может быть настроен либо в цепочке вызовов, либо при первом вызове. Независимо от этого, самым большим недостатком этого подхода является то, что он полностью скрывает зависимость. Невозможно понять связь, просто посмотрев на конструктор класса или сигнатуру функции; необходимо прочитать исходный код функции.
Кроме того, реализация может быть выполнена просто по паттерну синглтон и обладать теми же недостатками.
Контекст Kotlin
Последний подход является специфическим для Kotlin и только что был переведен из экспериментальной версии в бета-версию в Kotlin 2.2.
Контекстные параметры позволяют функциям и свойствам объявлять зависимости, которые неявно доступны в окружающем контексте.
С контекстными параметрами вам не нужно вручную передавать значения, такие как службы или зависимости, которые являются общими и редко меняются в наборах вызовов функций.
Вот как мы можем перенести вышеуказанный код в контекстные параметры:
class Delivery(private val zoneId: ZoneId) {
context(addressService: AddressService, geoService: GeoService)
fun computeDeliveryTime(user: User, warehouseLocation: Location): ZonedDateTime {
// возвращайте дату и время
}
}
А вот как это вызвать:
context(addressService,geoService) {
delivery.computeDeliveryTime(user, location)
}
Обратите внимание, что вызов может быть вложен на любом уровне внутри context.
Итоги
Подход |
Плюсы |
Минусы |
Внедрение через конструктор |
Тестируемый |
Смешивает состояние и зависимости |
Передача параметров |
Тестируемый |
Шумный |
ThreadLocal |
Скрывает связанность |
|
Параметр контекста |
Извлечение глубоко вложенных зависимостей |
Ограничено Kotlin |
Думаю, я буду продолжать использовать внедрение через конструктор, если только не буду программировать на Kotlin. Если буду иметь дело с Kotlin — то с удовольствием стану использовать контекстные параметры, даже несмотря на то, что они находятся в бета-версии.
Комментарии (9)

MEJIOMAH
17.10.2025 10:27Господи какой ужас

UbuRus
17.10.2025 10:27Пример ужасный конечно (это не DI конечно, он просто загрязняет коллера своими зависимостями), но по сути своей context parameter отличная замена thread local / scoped values для того чтобы писать 100% правильно работающий код. В случае с использованием любой многопоточности context parameters единственный способ который на этапе компиляции отловит то что контекст забыли передать. Цена конечно у этого есть в плане того, что контексты попадают в API методов. Но в целом и писать и тестировать это приятно

NN1
17.10.2025 10:27Scala implicit.
Улучшенный в 3.0 https://www.baeldung.com/scala/scala-3-implicit-redesign
akardapolov
В Java ближайший аналог — Scoped Values (JEP 429).
UbuRus
Scoped Values это скорее coroutineContext
Ближайший аналог просто передавать вручную контекст как параметр везде (что заграмождает код)