Há algum tempo venho tentando entender o padrão do bolo no Scala e, até que não me sentei e escrevi algum código, ele não funcionou. Aqui está minha tentativa de explicá-lo (principalmente para mim mesmo), espero que possa ajudar outros a obter uma melhor compreensão desse padrão interessante.
Para que serve?
Injeção de dependência, “o caminho do scala” (entre outros usos possíveis, mas esta postagem se concentra neste)
O que é injeção de dependência?
O inverso das pesquisas / dependências nomeadas, por exemplo, se a classe X precisa de uma conexão de banco de dados e a obtém usando
val con = DBConnectionRepository.getByName("appDBConnection")
então isso não é injeção de dependência, é acoplado ao repositório e ao nome (nunca pode ser mudado …)
A injeção de dependência tenta invertê-la, uma maneira simples de fazer isso é por meio de um argumento de construtor não opcional, outra maneira é um def abstrato. Existem muitas outras maneiras de fazer isso (Spring, CDI, Guice), mas esta postagem se concentrará em um padrão chamado “o padrão do bolo”
O que há com o “bolo”, afinal?
Boa pergunta, não sei qual é a etimologia, este comentário é tudo que encontrei
Por que não usar extends
?
Então, em primeiro lugar, por que precisamos de injeção? por que não apenas estender uma característica que precisamos usar ou “injetar”? uma vez que podemos estender vários traços, e cada um pode ter implementações, não é o suficiente?
Bem vamos ver:
Vamos supor que temos uma dependência:
trait FooAble {
def foo() = "here is your foo"
}
E algo que o usa
class BarUsingFooAble extends FooAble {
def bar() = "bar calls foo: " + foo()
}
E o código do cliente
object Main {
def main(args: Array[String]) {
val barWithFoo = new BarUsingFooAble
println(barWithFoo.bar())
}
}
Qual é o problema? bem, primeiro, você está preso a esse específico FooAble
, se você quiser algo que estenda / implemente FooAble
você precisa modificar a classe ou criar outra, mas isso não é exatamente injeção de dependência , o usuário da dependência declara especificamente, não é injetado .
Por que não usar with
?
Por que não podemos usar with
então? por exemplo
object Main {
def main(args: Array[String]) {
val barWithFoo = new BarUsingFooAble with FooAble
println(barWithFoo.bar())
}
}
class BarUsingFooAble {
def bar() = "bar calls foo: " + foo()
}
Bem, é claro que isso não compila, já que BarUsingFooAble não tem um método foo
definido …
Por que não usar métodos abstratos então?
Dependência
abstract class BarUsingFooAble {
def bar() = "bar calls foo: " + foo.foo()
def foo:FooAble //abstract
}
object Main {
def main(args: Array[String]) {
val fooable = new FooAble {}
val barWithFoo = new BarUsingFooAble{
def foo: FooAble = fooable
}
println(barWithFoo.bar())
}
}
Bem, funciona, mas você não prefere usar mixins em vez de implementar métodos abstratos? (embora eventualmente métodos abstratos sejam usados de alguma forma, mas fique comigo)
Anotações de autotipagem / Auto-referências explicitamente digitadas para o resgate
É aqui que as anotações autotipadas ajudam
class BarUsingFooAble {
this: FooAble => //see note #1
def bar() = "bar calls foo: " + foo() //see note #2
}
object Main {
def main(args: Array[String]) {
val barWithFoo = new BarUsingFooAble with FooAble //see note #3
println(barWithFoo.bar())
}
}
Explicação:
Então, o que aconteceu aqui? o que é essa coisa? Bem, basicamente significa que esta classe declara que eventualmente se estenderá de alguma forma (por exemplo, via ).this: FooAble =>
FooAble
with FooAble
Qual é a diferença em estendê-lo? como dito acima, extends
está na verdade estendendo -o e é muito específico do tipo. A anotação de tipo próprio está apenas declarando que esse tipo precisa estender / implementar o tipo anotado, mas ainda não o estende. Ele permite que você “injete” a extensão, portanto, oferece suporte à injeção de dependência.
Mais detalhes:
1) você pode usar this
, self
ou qualquer identificador para a anotação de tipo próprio, consulte aqui para obter mais informações (resposta do próprio Martin Odersky no SO)
2) agora BarUsingFooAble
presume que foi iniciado com (ou algo que o estende)with FooAble
3) se você não usar, receberá um erro de compilação:with FooAble
a classe BarUsingFooAble não pode ser instanciada porque não está em conformidade com seu próprio tipo BarUsingFooAble com FooAble
Múltiplas implementações
Vamos fazer um FooAble
resumo para ilustrar melhor o benefício das anotações de tipo próprio sobre a extensão
trait FooAble {
def foo() : String
}
E ter alguma implementação concreta
trait MyFooAble extends FooAble {
def foo() = "foo impl"
}
Agora nosso código cliente não compilará porque é abstratoFooAble.foo
Isso nos força a usar uma implementação (qualquer implementação)
Portanto, alterá-lo para (ou qualquer outra implementação de FooAble) funcionaráwith MyFooAble
object Main {
def main(args: Array[String]) {
val barWithFoo = new BarUsingFooAble with MyFooAble
println(barWithFoo.bar())
}
}
Essa é a grandeza da injeção de dependência, BarUsingFooAble
depende de FooAble
(qualquer FooAble
implementação) e o cliente é forçado a misturá-la.
E quanto a várias injeções?
Bem, isso também é possível, usando with
por exemplo
Vamos adicionar outra dependência
trait BazAble{
def baz() = "baz too"
}
class BarUsingFooAble {
this: FooAble with BazAble =>
def bar() = s"bar calls foo: ${foo()} and baz: ${baz()}"
}
object Main {
def main(args: Array[String]) {
val barWithFoo = new BarUsingFooAble with MyFooAble with BazAble
println(barWithFoo.bar())
}
}
Você pode usar isso para “forçar” a mistura de qualquer número de dependências desta forma
Por que não usar parâmetros de construtor então?
Boa pergunta, vamos supor que essas 2 dependências
trait FooAble {
def foo() = "here is your foo"
}
trait BazAble{
def baz() = "baz too"
}
E algo que usa a dependência, declarando-a no construtor
class BarUsingFooAble (dep:FooAble with BazAble) {
def bar() = s"bar calls foo: ${dep.foo()} and baz: ${dep.baz()}"
}
E algum código de cliente
object Main {
def main(args: Array[String]) {
val barWithFooAndBaz = new BarUsingFooAble(new FooAble with BazAble)
println(barWithFooAndBaz.bar())
}
}
É melhor / pior do que usar anotações de tipo? Eu diria que é uma questão de estilo e preferência, não consegui encontrar nenhuma diferença mais profunda (sinta-se à vontade para comentar se o fizer)
Falando em parâmetros de construtor, por que não usar implícitos?
Há um ótimo tópico sobre isso no grupo scala-user do Google
Indo mais longe
agora que cobrimos as anotações de tipo próprio, vamos ver como ele pode ser usado para injeção de dependência no mundo real
Envolva-o em um traço de componente (abstrato)
trait FooAbleComponent {
val fooAble: FooAble
class FooAble {
def foo() = "here is your foo"
}
}
trait BazAbleComponent {
val bazAble: BazAble
class BazAble {
def baz() = "baz too"
}
}
Depende dos componentes
class BarUsingFooAble {
this: FooAbleComponent with BazAbleComponent =>
def bar() = s"bar calls foo: ${fooAble.foo()} and baz: ${bazAble.baz()}"
}
Defina a implementação concreta real durante o tempo de injeção
object Main {
def main(args: Array[String]) {
val barWithFoo = new BarUsingFooAble with FooAbleComponent with BazAbleComponent {
val bazAble = new BazAble() //or any other implementation
val fooAble = new FooAble() //or any other implementation
}
println(barWithFoo.bar())
}
}
Mais sobre o raciocínio acima pode ser encontrado neste excelente artigo
É isso, pedaço de bolo …
É a melhor forma de injetar no Scala? Eu pessoalmente prefiro usar CDI e @Inject se estou em um container Java EE / CDI, mas nem sempre isso é possível, e é bom saber as alternativas!
Se você acha que perdi algo / escrevi algo incorreto / totalmente estúpido, sinta-se à vontade para corrigir / sugerir / melhorar nos comentários abaixo