Scala инъекция зависимостей: альтернативы неявным параметрам
Прошу простить длину этого вопроса.
Мне часто нужно создать некоторую контекстуальную информацию на одном уровне моего кода и использовать эту информацию в другом месте. Обычно я использую неявные параметры:
def foo(params)(implicit cx: MyContextType) = ...
implicit val context = makeContext()
foo(params)
Это работает, но требует, чтобы неявный параметр передавался по большому счету, загрязняя методы подписи слоя после компоновки промежуточных функций, даже если они не заботятся об этом сами.
def foo(params)(implicit cx: MyContextType) = ... bar() ...
def bar(params)(implicit cx: MyContextType) = ... qux() ...
def qux(params)(implicit cx: MyContextType) = ... ged() ...
def ged(params)(implicit cx: MyContextType) = ... mog() ...
def mog(params)(implicit cx: MyContextType) = cx.doStuff(params)
implicit val context = makeContext()
foo(params)
Я нахожу этот подход уродливым, но у него есть одно преимущество: оно безопасно. Я с уверенностью знаю, что mog
получит объект контекста нужного типа или не будет компилироваться.
Это облегчит беспорядок, если я смогу использовать некоторую форму "инъекции зависимостей", чтобы найти соответствующий контекст. Кавычки указывают на то, что это отличается от обычных шаблонов инъекций зависимостей, найденных в Scala.
Начальная точка foo
и конечная точка mog
могут существовать на самых разных уровнях системы. Например, foo
может быть контроллером входа пользователя, а mog
может выполнять SQL-доступ. Могут быть одновременно зарегистрированы многие пользователи, но есть только один экземпляр уровня SQL. Каждый раз, когда mog
вызывается другим пользователем, необходим другой контекст. Таким образом, контекст не может быть запечен в принимающем объекте, и вы не хотите объединить два слоя каким-либо образом (например, шаблон Cake). Я также предпочел бы не полагаться на библиотеку DI/IoC, такую как Guice или Spring. Я нашел их очень тяжелыми и не очень хорошо подходит для Scala.
Мне кажется, что мне нужно что-то, что позволяет mog
извлекать правильный контекстный объект для него во время выполнения, немного как ThreadLocal
со стеком в нем:
def foo(params) = ...bar()...
def bar(params) = ...qux()...
def qux(params) = ...ged()...
def ged(params) = ...mog()...
def mog(params) = { val cx = retrieveContext(); cx.doStuff(params) }
val context = makeContext()
usingContext(context) { foo(params) }
Но это упадет, как только асинхронный актер будет задействован в любой точке цепи. Неважно, какую актерскую библиотеку вы используете, если код работает в другом потоке, тогда он теряет ThreadLocal
.
Итак... есть ли трюк, который мне не хватает? Способ передачи информации контекстно в Scala, который не загрязняет сигнатуры промежуточного метода, не испекает контекст в приемнике статически и по-прежнему безопасен по типу?
Ответы
Ответ 1
Стандартная библиотека Scala включает в себя что-то вроде вашего гипотетического "usingContext" под названием DynamicVariable. Этот вопрос содержит некоторую информацию об этом Когда мы должны использовать scala.util.DynamicVariable?. DynamicVariable использует ThreadLocal под капотом, поэтому многие из ваших проблем с ThreadLocal останутся.
Монада-читатель является функциональной альтернативой явной передаче среды http://debasishg.blogspot.com/2010/12/case-study-of-cleaner-composition-of.html. Монада читателя может быть найдена в Scalaz http://code.google.com/p/scalaz/. Тем не менее, ReaderMonad "загрязняет" ваши подписи тем, что их типы должны меняться, и в общем случае монадическое программирование может вызвать много реструктуризации вашего кода, а дополнительные выделения объектов для всех закрытий могут не сильно сидеть, если проблема с производительностью или памятью.
Ни один из этих методов не будет автоматически обмениваться контентом с сообщением о передаче актера.
Ответ 2
Немного поздно для вечеринки, но рассмотрели ли вы использование неявных параметров для своих конструкторов классов?
class Foo(implicit biz:Biz) {
def f() = biz.doStuff
}
class Biz {
def doStuff = println("do stuff called")
}
Если вы хотите иметь новый бизнес для каждого вызова f()
, вы можете позволить неявному параметру быть функцией, возвращающей новый бизнес:
class Foo(implicit biz:() => Biz) {
def f() = biz().doStuff
}
Теперь вам просто нужно предоставить контекст при построении Foo
. Что вы можете сделать следующим образом:
trait Context {
private implicit def biz = () => new Biz
implicit def foo = new Foo // The implicit parameter biz will be resolved to the biz method above
}
class UI extends Context {
def render = foo.f()
}
Обратите внимание, что неявный метод biz
не будет отображаться в UI
. Поэтому мы в основном скрываем эти детали:)
Я написал сообщение в блоге о с использованием неявных параметров для инъекции зависимостей, которые можно найти здесь (бесстыдная самореклама;))
Ответ 3
Я думаю, что инъекция зависимости от лифта делает то, что вы хотите. Подробнее см. wiki с помощью метода doWith().
Обратите внимание, что вы можете использовать его как отдельную библиотеку, даже если вы не используете лифт.
Ответ 4
Вы спросили об этом примерно год назад, но вот еще одна возможность. Если вам нужно только вызвать один метод:
def fooWithContext(cx: MyContextType)(params){
def bar(params) = ... qux() ...
def qux(params) = ... ged() ...
def ged(params) = ... mog() ...
def mog(params) = cx.doStuff(params)
... bar() ...
}
fooWithContext(makeContext())(params)
Если вам нужно, чтобы все методы были внешне видимыми:
case class Contextual(cx: MyContextType){
def foo(params) = ... bar() ...
def bar(params) = ... qux() ...
def qux(params) = ... ged() ...
def ged(params) = ... mog() ...
def mog(params) = cx.doStuff(params)
}
Contextual(makeContext()).foo(params)
Это в основном шаблон торта, за исключением того, что если все ваши материалы вписываются в один файл, вам не нужно все беспорядочное вещество trait
, чтобы объединить его в один объект: вы можете просто вложить их в один объект. Выполнение этого способа также делает cx
корректно лексически охваченным, поэтому вы не окажетесь в забавном поведении, когда используете фьючерсы и актеры и т.д. Я подозреваю, что если вы используете новый AnyVal, вы даже можете отказаться от накладных расходов на выделение объекта Contextual
.
Если вы хотите разделить свои материалы на несколько файлов с помощью trait
s, вам действительно нужен только один trait
для каждого файла, чтобы удержать все и правильно поместить MyContextType
в область видимости, если вам не нужно fancy replaceable-components-through-inheritance, которые имеют большинство примеров шаблонов торта.
// file1.scala
case class Contextual(cx: MyContextType) with Trait1 with Trait2{
def foo(params) = ... bar() ...
def bar(params) = ... qux() ...
}
// file2.scala
trait Trait1{ self: Contextual =>
def qux(params) = ... ged() ...
def ged(params) = ... mog() ...
}
// file3.scala
trait Trait2{ self: Contextual =>
def mog(params) = cx.doStuff(params)
}
// file4.scala
Contextual(makeContext()).foo(params)
В небольшом примере он выглядит немного грязным, но помните, что вам нужно только разбить его на новый признак, если код становится слишком большим, чтобы удобно сидеть в одном файле. К тому моменту ваши файлы достаточно большие, поэтому дополнительные 2 строки шаблона в файле 200-500 строк не так уж плохи.
EDIT:
Это также работает с асинхронным файлом
case class Contextual(cx: MyContextType){
def foo(params) = ... bar() ...
def bar(params) = ... qux() ...
def qux(params) = ... ged() ...
def ged(params) = ... mog() ...
def mog(params) = Future{ cx.doStuff(params) }
def mog2(params) = (0 to 100).par.map(x => x * cx.getSomeValue )
def mog3(params) = Props(new MyActor(cx.getSomeValue))
}
Contextual(makeContext()).foo(params)
Он просто работает, используя вложенность. Я был бы впечатлен, если бы вы могли получить аналогичную функциональность, работающую с DynamicVariable
.
Вам понадобится специальный подкласс Future
, который сохраняет текущий DynamicVariable.value
при его создании, и подключится к методу ExecutionContext
prepare()
или execute()
, чтобы извлечь value
и правильно настроить DynamicVariable
перед выполнением Future
.
Тогда вам понадобится специальный scala.collection.parallel.TaskSupport
, чтобы сделать что-то подобное, чтобы получить работу параллельных коллекций. И специальный akka.actor.Props
, чтобы сделать что-то подобное для , которое.
Каждый раз, когда появляется новый механизм создания асинхронных задач, реализация на основе DynamicVariable
будет ломаться, и у вас появятся странные ошибки, в результате чего вы потянете неправильный Context
. Каждый раз, когда вы добавляете новый DynamicVariable
для отслеживания, вам нужно будет исправить все ваши специальные исполнители, чтобы правильно установить/отключить этот новый DynamicVariable
. Использование вложенности вы можете просто позволить лексическому закрытию заботиться обо всем этом для вас.
(Я думаю, что Future
s, collections.parallel
и Prop
считаются как "промежуточные уровни", которые не являются моим кодом ")
Ответ 5
Подобно неявному подходу, с помощью Scala Макросов вы можете выполнять автоматическую проводку объектов с помощью конструкторов - см. проект MacWire (и извините за саморекламу).
MacWire также имеет области действия (вполне настраиваемый, предоставляется реализация ThreadLocal
). Тем не менее, я не думаю, что вы можете распространять контекст между вызовами актера с помощью библиотеки - вам нужно нести какой-то идентификатор. Это может быть, например, через обертку для отправки сообщений актера или более непосредственно с сообщением.
Затем, пока идентификатор уникален для каждого запроса/сеанса/независимо от вашей области видимости, это просто вопрос поиска вещей на карте через прокси-сервер (например, области MacWire, "идентификатор" здесь не является необходимо, поскольку он хранится в ThreadLocal
).