Cake Pattern em Scala / Anotações de tipo próprio / Auto-referências explicitamente digitadas – explicado

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 FooAblevocê 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 withentã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 foodefinido …

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 =>FooAblewith FooAble

Qual é a diferença em estendê-lo? como dito acima, extendsestá 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, selfou 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 BarUsingFooAblepresume 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 FooAbleresumo 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, BarUsingFooAbledepende de FooAble(qualquer FooAbleimplementaçã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