1

I have a TemplateEngine interface whose implementations would be MoustacheTemplateEngine, FreemarkerTemplateEngine etc.

public interface TemplateEngine<T> {
    public T compileTemplate(String templateStr);
    public void merge(T t, Map<String, Object> data);
    public String getTemplateLang();
}

The purpose of compileTemplate method is to enable caching of compiled templates.

Here is an implementation example:

import com.github.mustachejava.Mustache;

public class MoustacheTemplateEngine implements TemplateEngine<Mustache> {

    @Override
    public Mustache compileTemplate(String templateStr) {
        // return compiled template;
    }

    @Override
    public void merge(Mustache compiledTemplate, Map<String, Object> data) {
        // merge
    }

    @Override
    public String getTemplateLang() {
        return "moustache";
    }
}

I wish to create a factory that returns a TemplateEngine depending on the template language supplied. The factory, and the client that would use the factory, do not know anything about TemplateEngine implementations.

public class TemplateEngineFactory {

    private Map<String, TemplateEngine<?>> TEMPLATE_ENGINE_REGISTRY = new HashMap<>();

    @PostConstruct
    public void init() {
        // Scan all TemplateEngine impls in classpath and populate registry
    }

    public TemplateEngine<?> getTemplateEngine(String templateLang) {
        return TEMPLATE_ENGINE_REGISTRY.get(templateLang);
    }

}

A client would use the factory as below.

Map<String, Object> data = new HashMap<>();
data.put("name", "Tom");
TemplateEngine<?> templateEngine = factory.getTemplateEngine("moustache");
Object compiledTemplate = templateEngine.compileTemplate("Hi {{name}}");
templaeEngine.merge(compiledTemplate, data); // compile error

The error being The method merge(capture#3-of ?, Map<String,Object>) in the type TemplateEngine<capture#3-of ?> is not applicable for the arguments (Object, Map<String,Object>).

I understand the error and I know my API design is flawed due to the use of wildcard in factory. My question is how to design a factory for such use case and avoid unsafe casts?

Somu
  • 3,593
  • 6
  • 34
  • 44
  • Why don't you type your `TemplateEngineFactory` ? So when you instanciate some of the implementation you want, instead of using a wildcard, you wouldn't have any trouble calling any method of your `TemplateEngine`. – DamCx Jan 17 '18 at 13:50
  • Maybe you should consider hiding concrete template implementation (e.g. `Moustache`) behind a common interface as well? – yegodm Jan 17 '18 at 13:53
  • @yegodm yes but that does not solve the problem as I want to avoid type-casting the compiled template in `merge` method. – Somu Jan 17 '18 at 13:56
  • Maybe you can delegate `merge()` to that interface as well? – yegodm Jan 17 '18 at 14:01
  • @yegodm I'll still have to use an unsafe cast at some point until I get the generics right. – Somu Jan 17 '18 at 14:09
  • But you won't need to keep `TemplateEngine` generic. `TemplateEngine` produces you one of `Template` instances based on the selected language. That concrete implementation knows how to `merge(Map data)`. – yegodm Jan 17 '18 at 14:14
  • @yegodm `merge` method accepts a `Template` so that clients can cache templates after calling `compileTemplate` only once per template and supply compiled templates from cache. – Somu Jan 17 '18 at 14:20
  • @FedericoPeraltaSchaffner then `CompiledTemplate` interface should have methods like `setRawTemplate` and `getRawTemplate` since `MoustacheTemplateEngine.merge` requires the raw template. This again means either I use generic type or code with `Object` and type-cast in `merge` method, e.g. `if(compiledTemplate.getRawTemplate() instanceof Mustache) { Mustache m = (Mustache) compiledTemplate.getRawTemplate(); }` where `Mustache` is `com.github.mustachejava.Mustache`. – Somu Jan 17 '18 at 16:17
  • @Somu Then use `Vistor` pattem where `CompiledTemplate` visits a `TemplateEngine`. Let your template select the proper logic in the engine. For example, invocation chain might look like this: `engine.merge(CompiledTemplate t, data) -> t.merge(engine, data) -> engine.mergeMoustache(t, data)`. – yegodm Jan 17 '18 at 17:46

2 Answers2

0

You would need to put an unsafe cast at some point. The best option would be,

public <T> TemplateEngine<T> getTemplateEngine(String templateLang) {
    return (TemplateEngine<T>) TEMPLATE_ENGINE_REGISTRY.get(templateLang);
}

And then you can call it like,

TemplateEngine<Mustache> templateEngine = factory.getTemplateEngine("moustache");
Codebender
  • 14,221
  • 7
  • 48
  • 85
  • This is what i meant with my comment on the question, I did similar things on some projects, and it works really well – DamCx Jan 17 '18 at 13:57
  • Isn't there a better way to design the factory so that I can avoid unsafe cast completely? – Somu Jan 17 '18 at 14:01
  • 1
    Also, this means a client needs to know about implementation details (in this case `Mustache` class) to use the factory. This is a no go for me. – Somu Jan 17 '18 at 14:05
  • There is no way to know exactly what type if `TemplateEngine` client is asking for. Pass `Class` as argument and do `class.cast`. This at least gets rid of warning and gives you more flexibility to handle what type caller code is asking for. – tsolakp Jan 17 '18 at 16:47
-1

Just remove the wildcard, replace this:

TemplateEngine<?> templateEngine = factory.getTemplateEngine("moustache");

to this:

TemplateEngine templateEngine = factory.getTemplateEngine("moustache");.

You don't need to explicit the wildcard when you call the factory to get the TemplateEngine you want.

R. Karlus
  • 2,094
  • 3
  • 24
  • 48