O método apply de Scala - ZeFonseca.com
Vimos, ao falarmos de objetos complementares, que uma das vantagens deles era o compartilhamento de dados e comportamentos com uma classe homônima.
Uma das funcionalidades compartilhadas entre objetos complementares e sua respectiva classe é a possibilidade de construir estáticamente objetos do tipo dessa classe, bem como oferecer diversas assinaturas(tipo e quantidade de parâmetros) para esses construtores estáticos.
Outra funcionalidade compartilhada entre classes e seus objetos complementares é a possibilidade de "aplicar" a classe a um determinado tipo de parâmetro. É justamente na construção de instâncias da classe e na aplicação da classe a outros propósitos que a função apply faz o seu trabalho.
Entramos em detalhes sobre essa possibilidade no post sobre objetos complementares, então iremos discutir aqui apenas como o compilador faz essa construção acontecer.
Tudo se sucede na função apply e no fato de objeto complementar e classe terem o mesmo nome, dentro do mesmo pacote.
class Automovel(modelo: String)
def apply(modelo: String) = new Automovel(modelo)
Neste simples exemplo, definimos a classe Automovel e logo em seguida, no mesmo arquivo fonte, um objeto complementar homônimo. Neste objeto implementamos o método apply com um parâmetro do tipo String, exatamente como acontece no construtor padrão da classe.
A partir dessa definição, para criar instâncias e até mesmo coleções de objetos Automovel, não precisamos mais usar new para instanciar cada um : basta usar a sintaxe Automovel("Modelo"):
val carrosVW: Vector[Automovel](Automovel("Fusca"),Automovel("Variante"),
Automovel("Brasilia"),Automovel("Passat"))
// E para criar apenas uma instancia, fazemos assim:
val auto2 = Automovel("Ferrari")
No caso do valor auto2, o compilador infere seu tipo através do retorno de apply, o qual indicará o tipo da classe Automovel. Note que apply se encontra definido no objeto Automovel, não na classe Automovel, ou seja, é um método estático que construiu uma instância de classe. O leitor atento deve perceber que existe aí uma implementação do design pattern "Factory" usando apenas algumas linhas de Scala.
Como sempre, Scala busca oferecer recursos para deixar o código o mais limpo possível, e apply faz parte desse instrumental. Mas o método apply não possui apenas função estética; ele pode também simplificar a construção de objetos usando parâmetros pré-definidos:
def apply() = new Automovel("Carro Popular")
Adicionamos um outro método apply, desta vez sem parâmetros. Agora, a classe Automovel pode ser instânciada com ou sem parâmetros usando apenas o nome da classe como método. Teremos um valor padrão pré-definido, neste caso "Carro Popular":
val carro1 = Automovel()
val carro2 = Automovel("Fiat Uno")
As duas definições são válidas. No caso carro1 será uma instância de "Carro Popular" genérico, e carro2 de um "Fiat Uno"
apply não precisa ser um construtor, na verdade pode exercer qualquer função que desejarmos, até mesmo sem relação com a classe em questão. Como falamos na introdução, apply instrui o compilador como "aplicar" uma classe diante de uma certa assinatura de método. Talvez a aplicação da classe com um certo tipo de parâmetro envolva apenas o cálculo de alguma característica dessa classe, sem construir um novo objeto. Exemplo:
def apply(litros: Int) = litros * 14
val kmComLitros = Automovel(10)
SAIDA> kmComLitros : Int = 140
Neste exemplo adicionamos uma outra função apply ao objeto Automovel. Esta não cria objetos Automovel, mas retorna quantos KM podem ser rodados com determinada quantidade em litros de gasolina. Assim, estamos instruindo o compilador sobre o que fazer quando o objeto for chamado com diferentes tipos de dados. Se chamarmos a classe automóvel com um inteiro, Automovel(20) por exemplo, o compilador entenderá que deve aplicar o último método, e retornar o Int com valor litros * 14. Aqueles que já programam em estilo funcional há mais tempo notarão que se trata do conceito de "aplicação funcional", ou seja, uma instanciação de uma função. Neste caso, estamos aplicando esse conceito a objetos, que são um idioma inexistente na programação puramente funcional. Assim, Scala combina o melhor dos dois mundos, permitindo que não só funções mas também objetos sejam "aplicados" a distintos tipos de dados e a outras assinaturas de funções.
O método apply de Scala ajuda a unir conceitos dos mundos funcional e orientado a objetos. Todas as funções de Scala são objetos, assim os arquitetos da linguagem decidiram que deveria haver uma forma de "aplicar" parâmetros a um objeto através de uma função padrão, assim como aplicamos parâmetros a funções na programação puramente funcional. Tudo isso sem exigir a sintaxe Objeto.apply(p1,p2,p3), permitindo que seja usado apenas Objeto(p1,p2,p3). A função apply instrui então sobre o que fazer com os parâmetros p1,p2,p3 quando aplicados a essa classe.