Реализуйте простой фабричный шаблон с аннотациями Spring 3 - PullRequest
41 голосов
/ 17 июня 2011

Мне было интересно, как я могу реализовать простой фабричный шаблон с аннотациями Spring 3.В документации я видел, что вы можете создавать bean-компоненты, которые вызывают фабричный класс и запускают фабричный метод.Мне было интересно, если бы это было возможно, используя только аннотации.

У меня есть контроллер, который в данный момент вызывает

MyService myService = myServiceFactory.getMyService(test);
result = myService.checkStatus();

MyService - это интерфейс с одним методом с именем checkStatus ().

Мой класс фабрики выглядит так:

@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;
    }
}

Класс MyServiceOne выглядит так:

@Autowired
private LocationService locationService;

public boolean checkStatus() {
      //do stuff
}

Когда я запускаю этот код, переменная locationService всегда пуста.Я полагаю, это потому, что я сам создаю объекты внутри фабрики, а автопроводка не происходит.Есть ли способ добавить аннотации для правильной работы?

Спасибо

Ответы [ 10 ]

62 голосов
/ 07 сентября 2016

У меня сработало следующее:

Интерфейс состоит из ваших логических методов плюс дополнительный метод идентификации:

public interface MyService {
    String getType();
    void checkStatus();
}

Некоторые реализации:

@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
    }
}

А сама фабрика выглядит следующим образом:

@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;
    }
}

Я нашел такую ​​реализацию проще, чище и гораздо более расширяемой. Добавить новый MyService так же просто, как создать другой Spring Bean-компонент, реализующий тот же интерфейс без каких-либо изменений в других местах.

31 голосов
/ 17 июня 2011

Вы правы, создавая объект вручную, вы не позволяете Spring выполнять автопроводку.Рассмотрите возможность управления вашими сервисами также Spring:

@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;
        }
    }
}

Но я бы посчитал, что общий дизайн довольно скудный.Не лучше ли иметь одну общую реализацию MyService и передать one / two / three строку в качестве дополнительного параметра в checkStatus()?Чего вы хотите достичь?

@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();
        }
    }
}

Это все еще плохо спроектировано, потому что добавление новой реализации MyService также требует модификации MyServiceAdapter (нарушение SRP).Но на самом деле это хорошая отправная точка (подсказка: карта и шаблон стратегии).

9 голосов
/ 05 января 2012

Почему бы не добавить интерфейс FactoryBean в MyServiceFactory (чтобы сообщить Spring, что это фабрика), добавить регистр (служба String, экземпляр MyService), а затем вызвать каждый из сервисов:

@Autowired
MyServiceFactory serviceFactory;

@PostConstruct
public void postConstruct() {
    serviceFactory.register(myName, this);
}

ThisТаким образом, вы можете при необходимости разделить каждого поставщика услуг на модули, и Spring автоматически подберет всех развернутых и доступных поставщиков услуг.

8 голосов
/ 01 марта 2013

Вы можете вручную попросить Spring выполнить автоматическое подключение.

Пусть ваша фабрика реализует ApplicationContextAware. Затем предоставьте следующую реализацию на вашем заводе:

@Override
public void setApplicationContext(final ApplicationContext applicationContext {
    this.applicationContext = applicationContext;
}

и затем сделайте следующее после создания вашего компонента:

YourBean bean = new YourBean();
applicationContext.getAutowireCapableBeanFactory().autowireBean(bean);
bean.init(); //If it has an init() method.

Это автоматически подойдет для вашего LocationService.

6 голосов
/ 09 апреля 2013

Вы также можете декларативно определить bean-компонент типа ServiceLocatorFactoryBean , который будет действовать как класс Factory. поддерживается Spring 3.

Реализация FactoryBean, которая принимает интерфейс, который должен иметь один или несколько методов с подписями (обычно MyService getService () или MyService getService (String id)) и создает динамический прокси, который реализует этот интерфейс

Вот пример реализации шаблона Factory с использованием Spring

Еще один наглядный пример

3 голосов
/ 08 марта 2019

Следующий ответ ДруидКума

Лит-рефактор его фабрики с конструктором с автопроводкой:

@Service
public class MyServiceFactory {

    private static final Map<String, MyService> myServiceCache = new HashMap<>();

    @Autowired
    public 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;
    }
}
2 голосов
/ 09 июня 2015

Полагаю, вы используете org.springframework.beans.factory.config.ServiceLocatorFactoryBean. Это значительно упростит ваш код. За исключением MyServiceAdapter вы можете только создать интерфейс MyServiceAdapter с методом MyService getMyService и с союзниками для регистрации ваших классов

код

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" />
1 голос
/ 08 марта 2016

Вы можете создать экземпляр "AnnotationConfigApplicationContext", передав все ваши классы обслуживания в качестве параметров.

@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);
    }
}
0 голосов
/ 09 июля 2019

По решению Павла Черного здесь мы можем сделать универсальную типизированную реализацию этого шаблона. Для этого нам нужно ввести интерфейс NamedService:

    public interface NamedService {
       String name();
    }

и добавить абстрактный класс:

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;
    }
}

Затем мы создаем конкретную фабрику конкретных объектов, таких как 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);
    }
}

где Вывести список реализаций интерфейса MyService во время компиляции.

Этот подход прекрасно работает, если у вас есть несколько одинаковых фабрик в приложении, которые производят объекты по имени (если, конечно, бизнес-логики достаточно для создания объектов по имени). Здесь map хорошо работает с String в качестве ключа и содержит все существующие реализации ваших сервисов.

если у вас другая логика для создания объектов, эту дополнительную логику можно переместить в другое место и работать в сочетании с этими фабриками (которые получают объекты по имени).

0 голосов
/ 01 мая 2019

Попробуйте это:

public interface MyService {
 //Code
}

@Component("One")
public class MyServiceOne implements MyService {
 //Code
}

@Component("Two")
public class MyServiceTwo implements MyService {
 //Code
}
...