Multiple language components/classes [OOP/Patterns

2019-07-27 10:51发布

问题:

I have several components for which there exists different versions, depending on the language used by the system (configurable, and can be changed at runtime). For example, I have an interface ("component") for Tokenizer, and two concrete implementations for english and chinese, like so:

public interface Tokenizer {
    List<String> tokenize(String s);
}

public class EnglishTokenizer implements Tokenizer {
    List<String> tokenize(String s) { ... };
}

public interface ChineseTokenizer implements Tokenizer {
    List<String> tokenize(String s) { ... };
}

Now, in many classes of my code, I need to get a language specific implementation of some of those components (Tokenizer, Parser, and many others), and I was wondering what's the most elegant way to achieve this? I thought of using one of the following approaches:

  • Each component (such as Tokenizer), would have a factory (singleton) that, given a language enum, would return the appropriate language specific implementation, like so (this would require many factories):

    public enum TokenizerFactory {
        SINGLETON;
        private Map<Language, Tokenizer> cache;
        public getTokenizer(Language) {
            return cache.get(Language); 
        }
    }
    
  • Have a (quite large) Language class, that would be instantiated with a specific language enum, and would have many different methods to get the language specific components. Then, at runtime, I could easily switch between languages (which is one of my goals). Like so:

    public class Language {
        public Language(LanguageEnum) {/* load language specific components*/};
        public Tokenizer getTokenizer() {/* language specific tokenizer */};
        public Parser getParser() {/* language specific parser */};
    }
    

What is the most appropriate way to achieve what I'm trying to do? How can I improve my code?

回答1:

Use dependency injection.

Spring Framework is an extremely useful piece of software and my personal favorite but there are many alternatives such as Google Guice.

Using Spring, you would define two (three, fifteen, ...) separate contexts, one per language, and obtain needed component from appropriate context. It's similar to your second approach but without using Language class. For example:

# English context: english.xml 

<bean id="Tokenizer" class="EnglishTokenizer"/>
<bean id="Parser" class="EnglishParser"/>

...

# Your code

ApplicationContext englishContext = ...; // context itself is injected
Parser englishParser = (Parser) englishContext.getBean("Parser");

Another alternative is to have a single context but prefix your bean ids with your language, e.g. "English-Tokenizer" and "Chinese-Tokenizer".

If you've never used dependency injection before, this may sound like too much work for a result that can be achieved via factory and / or dynamic class loading :-) But it's not - and it can do so much more (you can configure properties / dependencies of your components; you don't have to worry about caching or maintaining your own singletons, etc...) that once you start using it you'll wonder how you've ever lived without it :-)

Update (answers questions in 2nd comment).

Here's a sample "ComponentLocator" pattern. ComponentLocator is a singleton that has no dependencies on Spring. Its instance (and implementation) is injected by the context.

public abstract class ComponentLocator {
  protected static ComponentLocator myInstance;

  protected abstract <T> T locateComponent(Class<T> componentClass, String language);

  public static <T> T getComponent(Class<T> componentClass, String language) {
    return myInstance.locateComponent(componentClass, language);
  }
}

Implementation of ComponentLocator assumes beans in your context are named as their interface names followed by semicolon and language (e.g. "com.mypackage.Parser:English"). ComponentLocatorImpl must be declared as bean in your context (bean name doesn't matter).

public class ComponentLocatorImpl extends ComponentLocator
    implements ApplicationContextAware {
  private ApplicationContext myApplicationContext;

  public void setApplicationContext(ApplicationContext context) {
    myApplicationContext = context;
    myInstance = this;
  }

  @Override
  protected <T> T locateComponent(Class<T> componentClass, String language) {
    String beanName = componentClass.getName() + ":" + language;
    return componentClass.cast(myApplicationContext.getBean(beanName, componentClass));
  }
}

In your code elsewhere (in main()?) you're going to load ApplicationContext:

ApplicationContext ctx = new ClasspathXmlApplicationContext("components.xml");

Note that you don't actually need to refer to the context directly anywhere else in the application. Wherever you need to get your components you just do:

Parser englishParser = ComponentLocator.getComponent(Parser.class, "English");
Parser chineseParser = ComponentLocator.getComponent(Parser.class, "Chinese");

Note that the above is just one possible approach and it assumes that you're pretty much only putting your language-dependent classes in the context. In your case that's probably the best (due to requirement of having all languages available simultaneously) otherwise you'd be replicating all your classes (once per language), so your A/B/C question is probably not applicable here.

But if you do have A/B/C dependency what you can do is (I'm assuming A, B, C are interfaces and Aimpl, Bimpl, Cimpl are their implementations):

<bean id="A" class="Aimpl">
  <property name="B" ref="B"/>
</bean>

<bean id="B" class="Bimpl">
  <property name="C" ref="C"/>
</bean>

<bean id="C" class="Cimpl">
  <property name="tokenizer" ref="Tokenizer:English"/>
</bean>

Your implementations would need to have setB(), setC() and setTokenizer() methods. This is easier then constructor injection, though latter is also possible.



回答2:

Consider the dimensions of change. Use cases such as "Add a new language" and "Add a new component". How readily can you do this, how readily do you avoid mistakes.

I'm not clear how your map is populated in the first case, some kind of registration scheme? Am I right in thinking that responsibility for completeness is spread across many classes. Also I'm always suspicous of Singletons.

In the second case if you add a new conmponent then you must add one new method in the language class, and make sure it works. Adding new Languages seems to be localised to the constructor (and presumably some further implmentation methods).

Overall I prefer the second approach.



回答3:

I agree with the Spring answer, IOC would be the way to go. The non-framework approach would be to use AbstractFactory.