Desenvolvimento

4 set, 2013

Dependências e módulos em Scala

Publicidade

Assisti a duas apresentações sobre como traits criam um sistema “realmente, realmente, realmente ” modular no Scala, uma vez que:

Injeção de dependência é como um sistema de módulos pobre que não possui checagem de tipos.

Eu gostaria de saber o que Injeção de Dependência (ID) quer dizer nesse caso, pois para mim injeção de dependência é como checagem de tipos. Eu entendo que Scala é uma coisa nova e sua habilidade de fazer cakes enormes é tentadora, mas sério… vamos parar de blá, blá, blá. Descrever algo como sendo “realmente, realmente, realmente interessante mesmo”, “profunda”, ou qualquer outra coisa não prova nada.

Por que o pattern cake é melhor do que a injeção baseada no construtor “clássico”? Como um simples interface/trait não é melhor do que esse mecanismo que utiliza traits aninhados, aliases de tipos abstratos e uma longa lista de traits misturados? Ou… ok, permita-me refazer a frase. QUANDO o pattern cake é melhor do que a injeção baseada no construtor “clássico”? Eu ainda estou para ver uma resposta convincente. Por outro lado, posso te dizer quando o pattern cake é categoricamente pior do que construtores normais.

Mas as pessoas talvez não conheçam a injeção baseada em construtor… não posso culpá-las, uma vez que o Spring tem se esforçado bastante para escondê-la por trás de seus enormes arquivos XML. Uma coisa que pode ajudar a aceitar esse tipo de ID é pensar nela como um aplicativo parcial em relação a uma coleção de funções relacionadas. Tenho certeza de que você já ouviu falar sobre isso, certo?

Aqui está o que eu considero uma injeção baseada no construtor “clássico”. Temos um trait Cache e duas implementações concretas, uma baseada em Mongo e uma baseada em Redis.

[scala]

trait Cache[K, V] {
def get(k: K): Option[V]
def set(k: K, v: V): Cache[K, V]
}

class MongoCache[K, V]() extends Cache[K, V] {
override def get(k: K) = ???
override def set(k: K, v: V) = ???
}

class RedisCache[K, V]() extends Cache[K, V] {
override def get(k: K) = ???
override def set(k: K, v: V) = ???
}

[/scala]

Também temos dois repositórios abstratos para estudantes e professores.

[scala]

trait StudentRepository {
def all: Seq[Student]
def get(id: String): Option[Student]
}

trait TeacherRepository {
def all: Seq[Teacher]
def get(id: String): Option[Teacher]
}

[/scala]

Há duas implementações que suportam chache, as quais podem ser inferidas ao observar os argumentos dos construtores.

[scala]

/**
* A PostgreSQL backed implementation that supports caching. Notice how
* the cache dependency is passed through the constructor.
*/
class PostgresStudentRepository(cache: Cache[String, String]) extends StudentRepository {
override def all = ???
override get get(id: String) = ???
}

/**
* Same thing as with PostgresStudentRepository.
*/
class PostgresTeacherRepository(cache: Cache[String, String]) extends TeacherRepository {
override def all = ???
override get get(id: String) = ???
}

[/scala]

Agora que temos alguns componentes, vamos construir um gráfico de objetos/módulos e executar o sistema todo.

[scala]

class Main {
def main(args: Array[String]): Unit = {
val mongoCache = new MongoCache[String, String]
val redisCache = new RedisCache[String, String]
val studentRepository = new PostgresStudentRepository(mongoCache)
val teacherRepository = new PostgresTeacherRepository(redisCache)

// Both repositories can now be passed to some other components
// via constructor injection.
val system = new System(studentRepository, teacherRepository)
system.start()
}
}

[/scala]

Tente fazer a mesma coisa utilizando o pattern cake. Você descobrirá rapidamente que não será capaz de fornecer duas diferentes implementações para o módulo Cache. E isso porque, no final, quando você junta todos os módulos, você mistura todos os membros de traits no mesmo “namespace” (Eu uso o significado literal aqui, um espaço para nomes, não o significado de “pacote”).

[scala]

trait CacheModule {
type Cache[K, V] <: CacheLike[K, V]

def Cache[K, V]: Cache[K, V]

trait CacheLike[K, V] {
def get(k: K): V
def set(k: K, v: V): Cache[K, V]
}
}

trait MongoModule extends CacheModule {
override def Cache[K, V] = new Cache[K, V]

class Cache[K, V] extends CacheLike[K, V] {
override def get(k: K): V = ???
override def set(k: K, v: V): Cache[K, V] = ???
}
}

trait RedisModule extends CacheModule {
override def Cache[K, V] = new Cache[K, V]

class Cache[K, V] extends CacheLike[K, V] {
override def get(k: K): V = ???
override def set(k: K, v: V): Cache[K, V] = ???
}
}

trait StudentRepositoryModule {
type StudentRepository <: StudentRepositoryLike

def StudentRepository: StudentRepository

trait StudentRepositoryLike {
def all: Seq[Student]
def get(id: String): Option[Student]
}
}

trait TeacherRepositoryModule {
type TeacherRepository <: TeacherRepositoryLike

def TeacherRepository: TeacherRepository

trait TeacherRepositoryLike {
def all: Seq[Teacher]
def get(id: String): Option[Teacher]
}
}

trait PostgresStudentRepositoryModule extends StudentRepositoryModule {
self: CacheModule =>

override def StudentRepository = new StudentRepository

class StudentRepository extends StudentRepositoryLike {
override def all: Seq[Student] = ???
override def get(id: String): Option[Student] = ???
}
}

trait PostgresTeacherRepositoryModule extends TeacherRepositoryModule {
// This module needs a CacheModule. Just as in the non-cake case, when
// the TeacherRepository’s constructor required a Cache instance, in
// this case we declare our dependency on caching using a self-type.
self: CacheModule =>

override def TeacherRepository = new TeacherRepository

class TeacherRepository extends TeacherRepositoryLike {
override def all: Seq[Teacher] = ???
override def get(id: String): Option[Teacher] = ???
}
}

[/scala]

Aqui é onde as coisas começam a ficar interessantes. Perceba como podemos providenciar um RedisCacheModule.

[scala]

class Main {
def main(args: Array[String]): Unit = {
val app = new System with PostgresStudentRepositoryModule
with PostgresTeacherRepositoryModule
with MongoCacheModule
// with RedisCacheModule is impossible…
}
}

[/scala]

Eu concordo que o pattern cake parece legal, mas não vi um só argumento sólido para a sua adoção. Ele parece legal apenas por causa de alguns recursos de linguagens novas da JVM, como traits, composição de traits em uma chamada de site e self-types.

Ambas as abordagens são typesafe. Além disso, na minha opinião, a abordagem antiga é de mais fácil compreensão porque é um pattern antigo, mas esse não é um bom argumento, uma vez que o tempo resolverá facilmente essa questão. Entretanto, como você também pode notar, o pattern cake está impondo uma séria limitação em quantas implementações diferentes você pode fornecer para uma única trait.

Outro ponto é que ambos os objetos criados a partir de traits, assim como os objetos criados a partir de classes, são objetos. Ex.: valores de primeira classe (first-class). Você ainda pode usar classes e construtores baseados em ID se gostar dessa ideia de módulos de primeira classe. Uma classe seria apenas uma fábrica de módulos.

Há no entanto um caso especial habilitado pelo pattern cake, que parece impossível de alcançar utilizando classes normais, e trata-se de composição de interfaces abstratas.

Não estou certo se posso descrever isso com clareza, mas… no exemplo acima, pode-se imaginar qua a implementação de PostgresStudentRepository.get verificará primeiro o cache e, apenas quando não houver valores no cache irá executar a query na base de dados. Esse pedaço de lógica de cache pode ser abstraído, mas perceba que isso depende de dois traits: Cache e StudentRepository. O que podemos usar no Scala para declarar essas dependências? Self-types, é claro:

[scala]

/**
* This trait provides the caching boilerplate, but leaves undefined
* the mechanism that provides a fresh, non-cached, Student. Also, we
* don’t specify anything about the implementation of Cache. These are
* the two interfaces: StudentRepository.slowGet and Cache.get that
* we’re composing within this trait.
*/
trait CachingStudentRepository extends StudentRepository {
self: Cache =>

override def get(id: String): Option[Student] = {
self.get(id).map(makeStudent(_)).orElse(slowGet(id))
}

def slowGet(id: String): Option[Student]
}

[/scala]

A única forma que eu vejo de implementar isso sem usar o pattern cake é substituindo o self-type por um membro do tipo cache abstract:

[scala]

trait CachingStudentRepository extends StudentRepository {
def cache: Cache

override def get(id: String): Option[Student] = {
cache.get(id).map(makeStudent(_)).orElse(slowGet(id))
}

def slowGet(id: String): Option[Student]
}

[/scala]

Isso irá causar problemas de namespacing se um CachingTeacherRepository também declarar um membro cache abstrato. É na verdade o mesmo problema do qual sofre o pattern cake. Você não será capaz de providenciar diferentes implementações para a mesma interface.

Uma solução simples é acrescentar o nome da trait no início do membro: cachingStudentRepositoryCache. Não é muito legal, certo? Seria mais legal se o Scala permitisse que pudéssemos renomear membros herdados de traits como digamos… o PHP 5.4 faz, uma linguagem que todos amamos odiar:

[php]

<?php

trait A {
public function smallTalk() {
echo ‘a’;
}
public function bigTalk() {
echo ‘A’;
}
}

trait B {
public function smallTalk() {
echo ‘b’;
}
public function bigTalk() {
echo ‘B’;
}
}

class Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
}
}

class Aliased_Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
B::bigTalk as talk;
}
}

[/php]

O pattern cake. Quando?

Ok, esse foi um pequeno chilique em relação ao pattern cake. Acredito que as pessoas deram crédito demais a algo que não acrescentou benefício algum a uma coisa que já éramos capazes de fazer no Java. Qualquer argumento em contrário será muito apreciado. Não escrevi este artigo para provar que alguém estava errado. Na verdade, o escrevi para que eu soubesse qual a melhor ferramenta para a tarefa. E quando se trata de ID em Scala, não vejo nenhum argumento claro de que o pattern cake é melhor para mim.

Oh… estou mentindo. Na verdade, há uma situação em que não há escolha. É quando você não tem controle sobre seus próprios construtores, como em uma API Servlet. Ou quando você nem mesmo tem construtores. Ex.: os objetos de controle do Framework Play! (forçar objetos no Framework Play! é mais uma vez algo que não vejo uma boa razão para se fazer, mas isso é assunto para outro artigo).

Então, estou curioso para ver um exemplo claro de quando eu realmente terei que usar o pattern cake porque a injeção baseada em construtores não dará conta do trabalho.

Campos não explorados

Há duas coisas que ainda não explorei neste artigo:

  1. Se algum desses dois modelos de ID ajuda ou atrapalha um estilo imutável.
  2. Currying mais à esquerda vs. currying mais à direita de ID, onde o currying mais à esquerda é representado pela injeção baseada em construtores, e o currying mais à direita é representado pela monad reader.

Referências

Vídeos

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://igstan.ro/posts/2013-06-08-dependencies-and-modules-in-scala.html