Подтвердить что ты не робот

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, который не загрязняет сигнатуры промежуточного метода, не испекает контекст в приемнике статически и по-прежнему безопасен по типу?

4b9b3361

Ответ 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).