Implement a simple factory pattern with Spring 3 annotations

前端 未结 11 1997
旧巷少年郎
旧巷少年郎 2020-12-12 10:37

I was wondering how I could implement the simple factory pattern with Spring 3 annotations. I saw in the documentation that you can create beans that call the factory class

相关标签:
11条回答
  • 2020-12-12 11:04

    You are right, by creating object manually you are not letting Spring to perform autowiring. Consider managing your services by Spring as well:

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

    But I would consider the overall design to be rather poor. Wouldn't it better to have one general MyService implementation and pass one/two/three string as extra parameter to checkStatus()? What do you want to achieve?

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

    This is still poorly designed because adding new MyService implementation requires MyServiceAdapter modification as well (SRP violation). But this is actually a good starting point (hint: map and Strategy pattern).

    0 讨论(0)
  • 2020-12-12 11:14

    You can manually ask Spring to Autowire it.

    Have your factory implement ApplicationContextAware. Then provide the following implementation in your factory:

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

    and then do the following after creating your bean:

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

    This will autowire your LocationService perfectly fine.

    0 讨论(0)
  • 2020-12-12 11:16

    Following answer of DruidKuma

    Litte refactor of his factory with autowired constructor:

    @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;
        }
    }
    
    0 讨论(0)
  • 2020-12-12 11:16

    Why not add the interface FactoryBean to MyServiceFactory (to tell Spring that it's a factory), add a register(String service, MyService instance) then, have each of the services call:

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

    This way, you can separate each service provider into modules if necessary, and Spring will automagically pick up any deployed and available service providers.

    0 讨论(0)
  • 2020-12-12 11:23

    Based on solution by Pavel Černý here we can make an universal typed implementation of this pattern. To to it, we need to introduce NamedService interface:

        public interface NamedService {
           String name();
        }
    

    and add abstract class:

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

    Then we create a concrete factory of specific objects like 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);
        }
    }
    

    where List the list of implementations of MyService interface at compile time.

    This approach works fine if you have multiple similar factories across app that produce objects by name (if producing objects by a name suffice you business logic of course). Here map works good with String as a key, and holds all the existing implementations of your services.

    if you have different logic for producing objects, this additional logic can be moved to some another place and work in combination with these factories (that get objects by name).

    0 讨论(0)
  • 2020-12-12 11:26

    You could instantiate "AnnotationConfigApplicationContext" by passing all your service classes as parameters.

    @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 讨论(0)
提交回复
热议问题