public class ServiceLocator {
public AccountTransferService revolveAccountTransferService(){
...
}
public TaxCalculationService revolveTaxCalculationService() {
...
}
public OrderService revolveOrderService() {
...
}
...
}
Localizar a implementação de um serviço.
Em aplicações orientadas a serviços ou que de outra forma utilizam o padrão Service é necessário encontrarmos a implementação de um determinado contrato. Especialmente quando a implementação desse serviço existe remotamente.
Nos primórdios da tecnologia JEE, antes da versão EJB 3.0 que inclui a Injeção de Dependência era necessário localizar a implementação de um serviço manualmente. Como isto era comum, nasceu o padrão Service Locator. Hoje em dia ele é menos utilizado explicitamente, mas ainda é usado em código de infa-estrutura e dentro de motores de injeção.
O objetivo principal é encapsular a localização física do serviço real que implementa o contrato (interface) que queremos. Se o processo de procura for demorado, muitas vezes o service locator tem também um papel de cache. O Service Locator também pode esconder a localização física da implementação sendo comum o uso do padrão Proxy para implementar serviços remotos.
O objeto service locator é simples e vem em dois sabores. O primeiro é o service locator especifico. Cada método deste objeto serve para encontrar um serviço diferente.
public class ServiceLocator {
public AccountTransferService revolveAccountTransferService(){
...
}
public TaxCalculationService revolveTaxCalculationService() {
...
}
public OrderService revolveOrderService() {
...
}
...
}
Esta forma de implementação é direta e estabelece que cada serviço terá um método resolve
associado. Esta forma é fortemente tipada e era ideal na era pré-generics já que a alternativa era usar cast explicito toda a vez que fizessemos a chamada.
A outra forma de implementação, mais comum, passa por entender que sempre existe uma interface associada ao serviço. Em Java isso significa que existe sempre uma interface associada. Então podemos usar a própria classe como chave para encontrar a implementação. Com o uso de generics, isso fica ainda mais fácil.
public class ServiceLocator {
public <I> I revolveService(Class<I> serviceContratInterface){
...
}
}
É possivel incrementar este mecanismo estabelecendo que a interface deve implementar alguma outra interface padrão (normalmente chamada Service
) para que não seja possivel usar qualquer tipo de interface Java. Imagine a confusão ao tentar usa CharSequence
, ou até mesmo java.sql.Connection
como interface.
Mas como o Service Locator realmente localiza a implementação ? Depende do contexto em que estiver aplicando o padrão e de onde estiverem as implementações reais. Seja como for, o serviço está localizado em algum tipo de registro (padrão Registry). No mundo JEE é comum que a implementação esteja registrada usando JNDI, que é uma forma de Naming Directory. Mas um simples Map
pode ser suficiente.
public class MapServiceLocator {
private final Map<String, Object > services = new HashMap<>();
public <I> I revolveService(Class<I> serviceContratInterface){
Object service = services.get(serviceContratInterface.getName());
if (service == null){
throw new ServiceNotFoundException(serviceContratInterface);
}
return serviceContratInterface.cast(service);
}
public void addService(Class<I> serviceContratInterface , I implementation){
services.put(serviceContratInterface.getName(), implementation);
}
}
Adicionamos um método para que possamos registrar o serviço. Neste caso, "fazer o registro" significa colocar o nome da interface e o objeto em um mapa. Não é recomendável colocar um objeto Class
como chave de um Map
, por isso usamos o nome completamente qualificado da classe, que é único. A recuperação do serviço é simples, basta consultar no mapa. Pode não parece à primeira vista, mas este é o mecanismo básico por detrás de qualquer motor de injeção de dependencias.
Normalmente é suficiente uma implementação de Service Locator que se baseia unicamente no contrato do serviço. Esta implementação mais simples é suficiente para muitas aplicações e casos, contudo, não é suficiente em geral. Eexistem muitas implementações possíveis para o mesmo contrato e porque cada uma delas terá alguma vantagem ou característica especifica em que estamos interessados, pode ser necessário utilizarmos algum parâmetro na nossa pesquisa. A interface do Service Locator seria então, algo como :
public class ServiceLocator {
public <I> I revolveService(Class<I> serviceContratInterface, Map<String, String> params){
...
}
}
Imagine que queríamos encontrar a implementação de GeoLocalizationService
que é um serviço que descobre as coordenadas geográficas dado um endereço, e vice-versa. Várias implementações são possíveis. Algumas usarão serviços de terceiros que poderão ser pagos. Falando de geolocalização um parâmetro importante é a precisão. Poderíamos também estar interessados em saber a altitude do local e não apenas a longitude e latitude. Para um mesmo contrato temos parâmetros para a implementação: custo, precisão e o uso da altitude. Poderíamos usar um serviço grátis com menos precisão e que não nos dá a altitude para a maioria das aplicações, mas provavelmente teríamos alguns erros nas entregas e talvez valesse investir em um serviço pago quando estivermos trabalhando em áreas mais densamente populadas.
Veja que este parâmetros que estamos passando ao Service Locator não servem para o uso do serviço em si mesmo, mas para localizar o serviço. Poderíamos usar esta mesma técnica para distinguir entre implementações locais ou remotas ou entre usar JNDI ou Webservices.
O padrão Service Locator é realmente muito utilizado e assume um papel central sempre que falamos em serviços ou implementações do padrão Service. Este padrão desempenhou um papel essencial nos sistemas JEE de antigamente e desempenha um papel fundamental nos motores de injeção de hoje em dia. Mesmo debaixo dos panos, ele ainda está presente.
Ele é presente também em sistemas embarcados onde várias implementações são possíveis para o mesmo serviço ( o serviço de localização é um exemplo).
A localização com parâmetros é a implementação completa do padrão e é usada em tecnologias como Jini (um mecanismo distribuído de serviços) e na Print API da JSE. A localização com parâmetros está também presente na implementação de motores de injeção e torna possivel qualificar as implementações com propriedade ligeiramente diferentes e depois especificar qual delas queremos injetar.
A Java Print Service API (JPS) define a classe PrintServiceLookup
que permite localizar implementações de PrintService
. Este é um exemplo direto do uso de um Service Locator que permite localizar por parâmetros. No caso da JPS os parâmetros são um objeto DocFlavor
e um objeto PrintRequestAttributeSet
.
Obviamente o padrão Service Locator está relacionado ao padrão Service já que serve essencialmente para fazer a ponte entre o contrato publico e a implementação particular. Também está relacionado ao padrão Registry que pode ser usado como forma centralizada de registrar os serviços e usado pelo Service Locator para encontrar qual implementação vai com qual interface. O padrão Proxy pode ser usado para conectar com implementações remotas ou para criar implementações especiais que mudam de servidor ou de implementação do serviço. Por exemplo, implementações que delegam as chamadas a pontos remotos diferentes com base em estado de rede ou outras métricas ou que mudam de servidor quando o servidor que estava sendo usado deixou de responder, como numa arquitetura de micro serviços.
[1] Deepak Alur, John Crupi, Dan Malks Core J2EE Patterns: Best Practices and Design Strategies: Prentice Hall Professional (2003)