Внедрение зависимости Scala: альтернативы неявным параметрам - PullRequest
19 голосов
/ 08 декабря 2011

Прошу прощения за длину этого вопроса.

Мне часто нужно создавать некоторую контекстную информацию на одном уровне моего кода и использовать эту информацию в другом месте. Я обычно использую неявные параметры:

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 Pattern). Я также предпочел бы не полагаться на библиотеку DI / IoC, такую ​​как Guice или Spring. Я нашел их очень тяжелыми и не очень подходящими для Скалы.

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

Ответы [ 5 ]

10 голосов
/ 08 декабря 2011

Стандартная библиотека Scala включает что-то вроде вашего гипотетического "usingContext", называемого DynamicVariable.Этот вопрос содержит некоторую информацию об этом Когда нам следует использовать scala.util.DynamicVariable? .DynamicVariable использует ThreadLocal под капотом, поэтому многие ваши проблемы с ThreadLocal останутся.

Монада ридера - это функциональная альтернатива явной передаче среды http://debasishg.blogspot.com/2010/12/case-study-of-cleaner-composition-of.html. Монада Reader находится в Scalazhttp://code.google.com/p/scalaz/. Однако ReaderMonad «загрязняет» ваши сигнатуры тем, что их типы должны измениться, и в общем случае монадическое программирование может привести к значительной реструктуризации вашего кода, а дополнительные выделения объектов для всех замыканий могут не соответствовать требованиям, если производительностьили проблема памяти.

Ни один из этих методов не будет автоматически обмениваться контекстом при отправке сообщения актера.

6 голосов
/ 08 мая 2012

Немного опоздал на вечеринку, но вы рассматривали возможность использования неявных параметров для ваших конструкторов классов?

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. Таким образом, мы в основном скрываем эти детали:)

Я написал сообщение в блоге о , использующем неявные параметры для внедрения зависимостей, которые можно найти здесь (бесстыдное самореклама;))

2 голосов
/ 08 декабря 2011

Я думаю, что внедрение зависимости от lift делает то, что вы хотите.См. вики для получения подробной информации о методе doWith ().

Обратите внимание, что вы можете использовать его как отдельную библиотеку, даже если вы не используете lift.

1 голос
/ 30 апреля 2013

Подобно неявному подходу, с помощью Scala Macros вы можете выполнять автоматическое подключение объектов с помощью конструкторов - см. Мой проект MacWire (и извините за саморекламу).

MacWire такжеимеет области действия (вполне настраиваемый, предоставляется реализация ThreadLocal).Однако я не думаю, что вы можете распространять контекст по вызовам акторов с помощью библиотеки - вам нужно иметь при себе некоторый идентификатор.Это может быть сделано, например, через оболочку для отправки сообщений субъекта или, более точно, непосредственно с сообщением.

Тогда, пока идентификатор уникален для запроса / сеанса / какой бы ни была ваша область, это просто вопрос просмотра вещейна карте через прокси-сервер (как это делают области MacWire, «идентификатор» здесь не нужен, поскольку он хранится в ThreadLocal).

1 голос
/ 04 января 2013

Вы спрашивали об этом примерно год назад, но есть еще одна возможность.Если вам когда-либо нужно вызвать только один метод:

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 в область видимости, если вам не нужна необычная вещь заменяемых компонентов через наследование, которую есть в большинстве примеров шаблонов тортов.

// 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 строк не так уж и плохи.

РЕДАКТИРОВАТЬ:

Этотоже работает с асинхронными вещами

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 s считаются как "промежуточные слои, которые не являются моимикод ")

...