Implementar um padrão de fábrica simples com anotações de Primavera 3
tenho um controlador que actualmente chama
MyService myService = myServiceFactory.getMyService(test);
result = myService.checkStatus();
MyService é uma interface com um método chamado checkStatus().
A minha aula de fábrica parece ... isto:@Component
public class MyServiceFactory {
public static MyService getMyService(String service) {
MyService myService;
service = service.toLowerCase();
if (service.equals("one")) {
myService = new MyServiceOne();
} else if (service.equals("two")) {
myService = new MyServiceTwo();
} else if (service.equals("three")) {
myService = new MyServiceThree();
} else {
myService = new MyServiceDefault();
}
return myService;
}
}
A minha classe é assim:
@Autowired
private LocationService locationService;
public boolean checkStatus() {
//do stuff
}
Quando eu executar este código a variável locationService é sempre nula. Eu acredito que isso é porque eu estou criando os objetos dentro da fábrica e autowiring não está acontecendo. Existe uma maneira de adicionar anotações para fazer isso funcionar corretamente?
Obrigado.11 answers
Tem razão, ao criar um objecto manualmente, não está a deixar a mola efectuar a auto-fixação. Considere também a gestão dos seus serviços até à Primavera:
@Component
public class MyServiceFactory {
@Autowired
private MyServiceOne myServiceOne;
@Autowired
private MyServiceTwo myServiceTwo;
@Autowired
private MyServiceThree myServiceThree;
@Autowired
private MyServiceDefault myServiceDefault;
public static MyService getMyService(String service) {
service = service.toLowerCase();
if (service.equals("one")) {
return myServiceOne;
} else if (service.equals("two")) {
return myServiceTwo;
} else if (service.equals("three")) {
return myServiceThree;
} else {
return myServiceDefault;
}
}
}
Mas eu consideraria o design geral um pouco pobre. Não seria melhor ter uma implementação geral e passar one
/two
/three
string como parâmetro extra para checkStatus()
? O que queres alcançar?
@Component
public class MyServiceAdapter implements MyService {
@Autowired
private MyServiceOne myServiceOne;
@Autowired
private MyServiceTwo myServiceTwo;
@Autowired
private MyServiceThree myServiceThree;
@Autowired
private MyServiceDefault myServiceDefault;
public boolean checkStatus(String service) {
service = service.toLowerCase();
if (service.equals("one")) {
return myServiceOne.checkStatus();
} else if (service.equals("two")) {
return myServiceTwo.checkStatus();
} else if (service.equals("three")) {
return myServiceThree.checkStatus();
} else {
return myServiceDefault.checkStatus();
}
}
}
Isto é ainda mal concebido porque a adição de novos MyService
implementação requer MyServiceAdapter
modificação também (violação de SRP). Mas este é realmente um bom ponto de partida (dica: mapa e padrão de estratégia).
O seguinte funcionou para mim:
A interface consiste nos seus métodos lógicos mais o método de identidade adicional:
public interface MyService {
String getType();
void checkStatus();
}
Algumas implementações:
@Component
public class MyServiceOne implements MyService {
@Override
public String getType() {
return "one";
}
@Override
public void checkStatus() {
// Your code
}
}
@Component
public class MyServiceTwo implements MyService {
@Override
public String getType() {
return "two";
}
@Override
public void checkStatus() {
// Your code
}
}
@Component
public class MyServiceThree implements MyService {
@Override
public String getType() {
return "three";
}
@Override
public void checkStatus() {
// Your code
}
}
E a própria fábrica como se segue:
@Service
public class MyServiceFactory {
@Autowired
private List<MyService> services;
private static final Map<String, MyService> myServiceCache = new HashMap<>();
@PostConstruct
public void initMyServiceCache() {
for(MyService service : services) {
myServiceCache.put(service.getType(), service);
}
}
public static MyService getService(String type) {
MyService service = myServiceCache.get(type);
if(service == null) throw new RuntimeException("Unknown service type: " + type);
return service;
}
}
Achei essa implementação mais fácil, mais limpa e muito mais extensível. Adicionar Novo MyService é tão fácil quanto criar outro spring bean implementando a mesma interface sem fazer quaisquer alterações em outros lugares.
Após resposta de DruidKuma
Refactor da sua fábrica com o construtor Autocolante:
@Service
public class MyServiceFactory {
private static final Map<String, MyService> myServiceCache = new HashMap<>();
@Autowired
private MyServiceFactory(List<MyService> services) {
for(MyService service : services) {
myServiceCache.put(service.getType(), service);
}
}
public static MyService getService(String type) {
MyService service = myServiceCache.get(type);
if(service == null) throw new RuntimeException("Unknown service type: " + type);
return service;
}
}
Porque não adicionar a interface FactoryBean ao MyServiceFactory( para dizer à Spring que é uma fábrica), adicionar um registo (String service, MyService instance), então, ter cada um dos Serviços chamada:
@Autowired
MyServiceFactory serviceFactory;
@PostConstruct
public void postConstruct() {
serviceFactory.register(myName, this);
}
Deste modo, poderá separar cada prestador de serviços em módulos, se necessário, e a Primavera recolherá automaticamente todos os prestadores de serviços disponíveis e implantados.
Pode pedir manualmente à Primavera para a Autowire.
Pede à tua fábrica que implemente o formulário de candidatura. Em seguida, forneça a seguinte implementação em sua fábrica:
@Override
public void setApplicationContext(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
E depois fazer o seguinte depois de criar o seu feijão:
YourBean bean = new YourBean();
applicationContext.getAutowireCapableBeanFactory().autowireBean(bean);
bean.init(); //If it has an init() method.
Isto vai autowire o seu serviço de localização perfeitamente bem.
Você também pode definir declarativamente um feijão do tipo ServiceLocatorFactoryBean {[3] } que irá funcionar como uma classe de fábrica. foi apoiado pela Primavera 3.
Uma implementação de FactoryBean que toma uma interface que deve ter um ou mais métodos com as assinaturas (tipicamente, MyService getService () ou MyService getService (String id))) e cria um proxy dinâmico que implementa essa interface
Aqui está um exemplo de implementação do padrão de fábrica usando Primavera
Com base na solução de Pavel Černýaqui podemos fazer uma implementação universal tipada deste padrão. Para isso, precisamos introduzir interface NamedService:
public interface NamedService {
String name();
}
E Adicionar classe abstracta:
public abstract class AbstractFactory<T extends NamedService> {
private final Map<String, T> map;
protected AbstractFactory(List<T> list) {
this.map = list
.stream()
.collect(Collectors.toMap(NamedService::name, Function.identity()));
}
/**
* Factory method for getting an appropriate implementation of a service
* @param name name of service impl.
* @return concrete service impl.
*/
public T getInstance(@NonNull final String name) {
T t = map.get(name);
if(t == null)
throw new RuntimeException("Unknown service name: " + name);
return t;
}
}
Então criamos uma fábrica de concreto de objetos específicos como o MyService.
public interface MyService extends NamedService {
String name();
void doJob();
}
@Component
public class MyServiceFactory extends AbstractFactory<MyService> {
@Autowired
protected MyServiceFactory(List<MyService> list) {
super(list);
}
}
Onde listar a lista de implementações da interface MyService na hora de compilação.
Esta abordagem funciona bem se você tem várias fábricas similares através da aplicação que produzir objetos pelo nome (se produzir objetos por um nome é suficiente a lógica de negócios, é claro). Aqui o mapa funciona bem com String como uma chave, e detém todas as implementações existentes de seus serviços.
Se você tem uma lógica diferente para produzir objetos, esta lógica adicional pode ser movida para outro lugar e trabalhar em combinação com essas fábricas (que recebem objetos pelo nome).
Código
bean id="printStrategyFactory" class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
<property name="YourInterface" value="factory.MyServiceAdapter" />
</bean>
<alias name="myServiceOne" alias="one" />
<alias name="myServiceTwo" alias="two" />
Você poderia instanciar "Annotationconfigationcontext" passando todas as suas classes de serviço como parâmetros.
@Component
public class MyServiceFactory {
private ApplicationContext applicationContext;
public MyServiceFactory() {
applicationContext = new AnnotationConfigApplicationContext(
MyServiceOne.class,
MyServiceTwo.class,
MyServiceThree.class,
MyServiceDefault.class,
LocationService.class
);
/* I have added LocationService.class because this component is also autowired */
}
public MyService getMyService(String service) {
if ("one".equalsIgnoreCase(service)) {
return applicationContext.getBean(MyServiceOne.class);
}
if ("two".equalsIgnoreCase(service)) {
return applicationContext.getBean(MyServiceTwo.class);
}
if ("three".equalsIgnoreCase(service)) {
return applicationContext.getBean(MyServiceThree.class);
}
return applicationContext.getBean(MyServiceDefault.class);
}
}
Tenta isto:
public interface MyService {
//Code
}
@Component("One")
public class MyServiceOne implements MyService {
//Code
}
@Component("Two")
public class MyServiceTwo implements MyService {
//Code
}
Seguindo a resposta de DruidKuma e jumping_monkey
Você também pode incluir opcional e tornar o seu código um pouco mais agradável e mais limpo:
public static MyService getService(String type) {
return Optional.ofNullable(myServiceCache.get(type))
.orElseThrow(() -> new RuntimeException("Unknown service type: " + type));
}