> [name=zed]
> * нужно уйти от билдера, он смещает внимание от фабрики
> * добавить описание способа с функцией конструтором - по анаологии с mutableListOf() и MutableStateFlow()
## Введение
При разработке библиотек часто бывает так, что необходимо предоставить разработчику способ создавать объект в зависимости от переданных параметров.
При этом, необходимо скрыть от пользователя библиотеки конкретные реализации классов. Одним из способов решения такой задачи является паттерн статический метод фабрика (далее просто фабрика).
Допустим есть библиотека для получения прогнозов, которая умеет запрашивать прогноз погоды у разных источников, объединять их в один и отдавать клиенту.
В библиотеке есть интерфейс `WeatherForecaster`, который реализовывают все классы клиенты различных сервисов. Для простоты в этом интерфейсе определен только один метод, который позволяет получить прогноз погоды за указанную дату в указанном месте `requestForecast(date, location)`.
```kotlin=
interface KeyValueStorage {
operator fun <T> get(key: String): T?
operator fun <T> set(key: String, value: T?)
companion object {
fun memoryStorage(): KeyValueStorage
fun file(file: File): KeyValueStorage
fun database(name: String): KeyValueStorage
}
}
```
```kotlin
interface WeatherForecaster {
suspend fun requestForecast(
date: LocalDate,
location: Location,
): Forecast
companion object {
const val PROVIDER_WEATHER_FORECAST = "WEAHTER_FORECAST"
const val PROVIDER_MY_FORECASTS = "MY_FORECASTS"
const val PROVIDER_POTATO_FORECAST = "POTATO_FORECAST"
// провайдеры передаются как имя(одно из PROVIDER_*) + API-ключ для конкретного провайдера
fun create(
vararg providesrs: Pair<String, String>,
): WeatherForecaster {
val forecasters = providers.mapNotNull { (provider, key) ->
when(provider) {
PROVIDER_WEATHER_FORECAST -> WeatherForecast(key)
PROVIDER_MY_FORECASTS -> MyForecasts(key)
PROVIDER_POTATO_FORECAST -> PotatoForecast(key)
else -> null
}
}
if (forecasters.isEmpty) error("Couldn't create forecaster for given providers ${providers.map { it.first }}")
return CompositeForecaster(forecasters)
}
}
}
```
А объект WeatherForecaster создается следующим образом:
```kotlin=
val forecaster = Weather.create(
WeatherForecaster.PROVIDER_WEATHER_FORECAST to "API_KEY",
WeatherForecaster.PROVIDER_POTATO_FORECAST to "POTATO_KEY"
)
```
Плюсом данного решения является то, что реализации для конкретных провайдеров можно сделать `internal`. И можно будет подменить сменить реализацию любого провайдера, не сломав клиентский код.
К минусам данного решения можно отнести то, что оно выглядит очень джавово. Легко перепутать ключи у провайдеров. А так же, можно использовать неверные имена провайдеров.
## Идеоматичная фабрика
Можно переписать метод create в более идеоматичном для Kotlin виде. Для этого нужно воспользоваться фичей языка Kotlin - перегрузка операторов. В языке Kotlin можно перегружать некоторые операторы с помощью ключевого слова operator. Каждому оператору, который можно перегружать соответствует определенный метод. Например, если нужно перегрузить оператор `+`, то нужно реализовать для класса функцию или создать экстеншн-функцию с ключивым словом `operator` и именем `plus`. Таким образом сделано сложение списков в стандартной библиотеке.
Для того, что бы улучшить пример из библиотеки прогнозов погоды, необходимо перегрузить оператор `()`. Для этого нужно реализовать функцию `invoke()` у нужного нам объекта.
```kotlin=
interface WeatherForecaster {
suspend fun requestForecast(
date: LocalDate,
location: Location,
): Forecast
companion object {
const val PROVIDER_WEATHER_FORECAST = "WEAHTER_FORECAST"
const val PROVIDER_MY_FORECASTS = "MY_FORECASTS"
const val PROVIDER_POTATO_FORECAST = "POTATO_FORECAST"
operator fun invoke(
vararg providesrs: Pair<String, String>,
): WeatherForecaster {
// ...
}
}
}
```
А создаваться объект WeatherForecaster будет уже следующим образом:
```kotlin=
val forecaster = Weather(
WeatherForecaster.PROVIDER_WEATHER_FORECAST to "API_KEY",
WeatherForecaster.PROVIDER_POTATO_FORECAST to "POTATO_KEY"
)
```
Теперь фабрика выглядит как конструктр и нам сразу понятно, что здесь создается объект.
Что еще дает такой подход? При использовании конструторов, мы не можем передать внутрь конструктора, лямбду для конфигурации билдера. Но с использованием `invoke()` оператора, можно это исправить. Можно улучшить пример. Сечас можно легко ошибится и передать как не правильный провайдер, так и указать чужой API-ключ одному из провайдеров.
```kotlin=
interface WeatherForecaster {
suspend fun requestForecast(
date: LocalDate,
location: Location,
): Forecast
class Builder private constructor() {
fun enableWeatherForecast(apiKey: String) {
// ...
}
fun enableMyWeather(apiKey: String) {
// ...
}
fun enablePotatoWeather(apiKey: String) {
// ...
}
fun build() :WeatherForecaster {
// ...
}
}
companion object {
operator fun invoke(block: Builder.() -> Unit) {
val builder = Builder()
builder.block()
return builder.build()
}
}
}
```
Давайте теперь посмотрим, как будет выглядеть использование нового "конструктора".
```kotlin=
val forecaster = WeatherForecaster {
enablePotatoForecast("POTATO_KEY")
enableMyForecast("MY_FORECAST_KEY")
}
```
Теперь стало сложнее сделать ошибку при инициализации прогнозов погоды. Получился удобный DSL для создания и конфигурирования объектов из библиотеки.
Что, немаловажно, такой код будет удобно расширять. Если у какого-либо прогноза погоды появится дополнительные параметры или добавится новый источник прогнозов, будет легко расширить DSL.
## Чем плох указанный подход
Если планируется испольозвание библиотеки из Java кода, то необходимо продумать адаптеры для этой функции. Особенно для случая с билдером.
Не совсем понятно можно ли возвращать `null` при создании объекта через такой конструктор или нет. С одной стороны, возвращение `null` - это хороший способ обработать ситуацию когда мы не можем создать объект по какой-либо причине: не правильные параметры, пропущенный аргумент и т.д. С другой стороны, возникает дисонанс связанный с тем, что программист не ожидает от "конструктора" нуллабл значения. Здесь наверное стоит пользоваться рантайм проверками с выбрасыванием исключения, если какой-либо из параметров передан неправильно.
Если разработчик использует не Intellij IDEA для разработки, то у него возможно могут возникнуть трудности при использовании подсказок.
## Выводы
Язык котлин обладает мощными средствами для организации кода. Рассмотренный подход позволяет улучшить шаблон - статический метод фабрика. Сделать его идеоматическим способом для языка Kotlin. Превратить его в понятный и хорошо расширяемый DSL, который удобно и понятно использовать.