Я хочу понять, как лучше всего использовать среду ZIO, то есть тип R в ZIO[R, E, A].
В этом твите Джон Де Гус приводит 3 простых правила, когда использовать среду R.
- Методы внутри определений сервисов (типы) НИКОГДА не должны использовать среду.
- Реализации служб (классы) должны принимать все зависимости в конструкторе.
- Весь остальной код («бизнес-логика») должен использовать среду для использования сервисов.
На домашней странице ZIO также приведен пример, который, похоже, следует этому совету.
Моя путаница в основном связана с тем, что ZIO считает «услугами» и что такое «бизнес-логика». В примере домашней страницы ZIO, указанном выше, «бизнес-логика» выглядит примерно так:
val app =
for {
id <- DocRepo.save(...)
doc <- DocRepo.get(id)
_ <- DocRepo.delete(id)
} yield ()
Это слишком просто, чтобы донести до меня суть. Это «приложение» полностью умещается в одну функцию, использующую абстрактный репозиторий. Более реалистичным случаем было бы то, что эта логика является, например, частью бизнес-логики для обработки какого-либо запроса, например метода DocumentHttpServer#handleFoo
. Но в таком случае разве DocumentHttpServer
не является «услугой»? Если да, то не следует ли DocRepo
передать его в качестве аргумента конструктора и не следует ли handleFoo
использовать его напрямую?
Для ясности, я понимаю, что этот DocumentHttpServer будет реализован следующим образом:
trait DocumentHttpServer {
def handleFoo(args: FooArgs): Task[Unit] // this is a service so should have R=Any
}
class DocumentHttpServerImpl(repo: DocRepo) extends DocumentHttpServer {
def handleFoo(args: FooArgs) =
for {
id <- repo.save(...) // using the constructor argument to match R=Any in trait
doc <- repo.get(id)
_ <- repo.delete(id)
} yield ()
}
В заключение, ошибаюсь ли я, полагая, что «бизнес-логика» описана в каком-то «сервисе ZIO», и если да, то его интерфейс не должен использовать среду?
ОБНОВЛЕНИЕ 1. Для ясности вопрос в том, когда СЛЕДУЕТ использовать среду вне основной функции и тестов.
Общий рейтинг:
Более длинный ответ
«Бизнес-логика» (в пункте 3) немного вводит в заблуждение, поскольку Сервисы также могут содержать бизнес-логику. Например, предположим, что у вас есть UserService
, для которого требуется HttpClient
(извлекает некоторые данные из общедоступного API) и UserRepo
(сохраняет/избирает пользователей в базе данных).
Здесь вам, вероятно, понадобится некоторая бизнес-логика, например, чтобы запретить пользователям иметь одно и то же имя пользователя и обрабатывать ошибки в UserRepo и HttpClient. Поскольку UserService — это сервис, вы хотите поместить зависимости в конструктор:
class UserServiceImpl(userRepo: UserRepo, httpClient: HttpClient) extends UserService {
def createUser(name: String): Task[Unit] = ???
}
Игнорируя другие промежуточные службы, вы, вероятно, захотите, чтобы сервер работал до закрытия приложения, которое обрабатывает входящие запросы. Здесь та же история. Сервер — это служба, поэтому зависимости помещаются в конструктор, а не в среду:
class ServerImpl(???) extends Server {
def start(): Task[Unit] = ???
}
trait Server {
def start(): Task[Unit]
// in reality, this would probably be ZIO[Scope, Throwable, Unit], since your Server probably
// has some finalizer to close down the server properly, but let's leave that out for now
}
// let's include the companion object with an accessor method
object Server {
def start(): ZIO[Server, Throwable, Unit] = ZIO.serviceWith[Server](_.start)
}
Далее давайте соберем приложение. Поскольку app
не является слоем (отредактируйте: сервис), вы хотите вызвать метод доступа, в среде которого есть Сервер:
val app =
for {
_ <- ZIO.logInfo("Starting server")
_ <- Server.start()
} yield ()
def run = app.provide(ServerImpl.layer, ???, UserRepo.layer, HttpClient.layer)
При вызове предоставления вы начинаете сверху и добавляете уровни один за другим, пока не удовлетворите все требования среды.
И последнее замечание: методы сопутствующего объекта не считаются служебным кодом и должны использовать среду. Двумя типичными случаями являются метод, создающий ZLayer, который должен быть методом сопутствующего объекта класса, и метод(ы) доступа, который должен быть реализован в сопутствующем объекте признака.
Примечание. Методы доступа могут быть полезны для каждой службы, чтобы упростить написание тестов, даже несмотря на то, что методы в сервисах, находящихся глубже в дереве зависимостей, никогда не будут вызываться с методами доступа в рабочем коде.
Примечание 2. Среда также используется для безопасности ресурсов (Область действия), а также может использоваться для вещей, необходимых для каждого обрабатываемого запроса, таких как контекст пользователя или подключение к базе данных (например, из пула базы данных). Область видимости — это первоклассная функция в ZIO, а UserContext/DbConnection — это вымышленные примеры.
Обновленный ответ
Вы хотите сказать, что хотя методы общедоступной службы, реализующие соответствующий признак, НЕ используют методы доступа, другие классы, используемые этой службой, могут использовать эти методы доступа? Несет ли каждый метод службы ответственность за предоставление всех уровней для таких классов бизнес-логики?
Нет, если вы пишете сервис с зависимостями, вам следует получить доступ к этим зависимостям через конструктор. Это касается родителей и детей в дереве зависимостей. Рассмотрим Foo, для которого требуется Bar:
class FooImpl(bar: Bar) extends Foo {
def someMethod(): Task[String] = ???
}
class BarImpl(baz: Baz) extends Bar {
def otherMethod: Task[String] = ???
}
И Foo, и Bar являются службами, что означает, что они следуют шаблону обслуживания, определенному в документации ZIO. Не следует использовать методы доступа для реализации какой-либо из этих служб.
Еще одно обновление
Как и в вашем ответе, они говорят, когда НЕ СЛЕДУЕТ использовать среду. Вопрос в том, когда СЛЕДУЕТ использовать среду?
Обычно вам не следует использовать среду, поскольку большая часть кода должна быть написана в сервисе. Так к чему вся эта возня с окружающей средой, если мы ею почти не пользуемся?
Технически все ваши сервисы используют среду, но не те методы, которые они реализуют. Рассмотрим, как создать слой из сервиса, используя приведенный выше пример Foo/Bar:
object FooImpl {
def layer: ZLayer[Bar, Nothing, Foo] = ZLayer {
bar <- ZIO.service[Bar]
} yield FooImpl(bar)
}
Вы можете думать об этом ZLayer как о ZIO[Bar, Nothing, Foo]
. ZLayer — это место, где вы используете среду для своих сервисов.
Однако вы не можете поместить весь свой код в сервис, потому что в какой-то момент вам нужно подключить ваше приложение для его запуска, то есть ваш «основной» метод или «запуск», как это называется в приложении ZIO. Здесь вы вызываете один или несколько методов доступа.
Подумайте о том, что должно делать ваше приложение при запуске. Для серверного приложения вы в основном просто хотите запустить сервер и, возможно, записать что-то в журнал, как в примере, который я показывал ранее. Для более простого приложения или во время разработки более крупного приложения, возможно, вам просто нужно выполнить несколько вызовов базы данных и закрыть приложение. Затем для этого вам нужно вызвать методы доступа.
Другое распространенное исключение, когда вы хотите использовать методы доступа, — это тестирование ваших сервисов. В тесте вы должны вызвать Foo.someMethod()
, в окружении которого есть Foo
, чтобы вы могли предоставить свою реализацию и подтвердить, что результат соответствует вашим ожиданиям.
Вы просили разъяснений по поводу UserRepo.
Примечание. У меня недостаточно знаний, чтобы посоветовать вам сделать это в производстве. Это теоретический пример исключения из правил. Лично я бы сделал это только в том случае, если бы библиотека, которую я использовал, работала именно так.
class UserRepoImpl() extends UserRepo {
def create(name: String): ZIO[Connection, Throwable, Unit] = ???
}
Затем у нас есть ConnectionPool:
class ConnectionPoolImpl() extends ConnectionPool {
def connection(): ZIO[Scope, Throwable, Connection] = ???
}
Теперь мы можем позволить UserService убедиться, что для обработки одного запроса используется только одно соединение:
class UserServiceImpl(userRepo: UserRepo, connectionPool: ConnectionPool) extends UserService {
def createTwoUsers(name1: String, name2: String) = ???
// get a connection from pool
// create user 1
// create user 2
// provide the connection to the resulting effect
// wrap everything in ZIO.scoped to show that we're done with the connection
}
Спасибо за этот подробный ответ. Для пояснения: хотите ли вы сказать, что, хотя методы общедоступной службы, реализующие соответствующий признак, НЕ используют методы доступа, другие классы, используемые этой службой, могут использовать эти методы доступа? Несет ли каждый метод службы ответственность за предоставление всех уровней для таких классов бизнес-логики? Для полноты картины не могли бы вы привести игрушечный пример того, как соединение с базой данных будет использоваться в UserRepo для удовлетворения интерфейса, определенного в признаке?
Второй вопрос: является ли это официальной лучшей практикой, поддерживаемой официальной документацией Zverge или ZIO, или это практика, с которой вы добились успеха? Если первое, можете ли вы дать ссылку на какую-либо подтверждающую документацию?
Обновил свой ответ. Я не совсем понимаю, что вы имеете в виду, говоря о UserRepo. Документация ZIO — хорошее место для начала. У Ziverge также есть серия под названием Zymposiium, где они загружают множество разговоров об этих вещах.
Ссылки, которые вы разместили в своем ответе, также связаны с моим вопросом. Как и в вашем ответе, они говорят, когда НЕ СЛЕДУЕТ использовать среду. Вопрос в том, когда СЛЕДУЕТ использовать среду?
Обновил ответ еще раз. Я не думаю, что смогу добавить что-то еще к этому ответу. Надеюсь, это ответит на ваш вопрос. Если у вас есть дополнительные вопросы, я бы посоветовал вам заглянуть в официальный дискорд, где многие участники активны и могут помочь вам более подробно ответить на вопросы.
Кроме того, я увидел, что случайно написал в исходном ответе «слой» вместо «сервис». Внесено изменение, чтобы прояснить это.
Я приму это, но, честно говоря, я до сих пор не понимаю, как реализация может одновременно использовать среду и соблюдать первое правило в TLDR; ваш метод UserRepo.create теперь использует среду R, что нарушает правило.
Создание слоя val
находится в сопутствующем объекте признака сервиса. Это не реализация сервиса.
Если вам нужны больше примеров использования среды вне реализации сервиса, я предлагаю вам взглянуть на библиотеки ZIO, например github.com/zio/zio-kafka
Помимо упомянутого вами твита, в документации ZIO есть страница об этом: zio.dev/reference/service-pattern/…