3

I'm creating a toolset in Java consisting of many Natural Language Processing (NLP) tools, such as Tokenizer, POS tagger, Lemmatizer, Sentence parsing, etc.

I want to put all good and publicly available tools into my toolset, all sharing the same interface, so that I can change library easily without change of API.

The project structure is something like this:

tokenizer/
    Tokenizer.java (an interface)
    LibraryFooTokenizer.java (from first library)
    LibraryBarTokenizer.java (from second library)
    DefaultTokenizer.java (a default tokenizer)
lemmatizer/
    Lemmatizer.java
    LibraryFooLemmatizer.java
    LibraryBarLemmatizer.java
    DefaultLemmatizer.java

The Library*Tokenizer.java are implementing Tokenizer.java, so I can directly use Tokenizer in my applications without needing to know the implementation details. So far so good.

My question is regarding the implementation of DefaultTokenizer.java. It's supposed to be for someone who just want to use a Tokenizer, who perhaps doesn't really know from the list of available Tokenizer which one is the best.

Should it be a subclass of the currently best library, or should it implement the interface and has the best library as a member, or else?

Currently I use the second approach as follows:

package tokenizer;

public class DefaultTokenizer implements Tokenizer{
    public Tokenizer tokenizer;

    public DefaultTokenizer(){
        tokenizer = new LibraryFooTokenizer();
    }

    @Override
    public String[] tokenizer(String text){
        return tokenizer.tokenize(text);
    }
}

Is it better than this one?

package tokenizer;

public class DefaultTokenizer extends LibraryFooTokenizer{}

The points that I'm considering are the ease of modification, in case later LibraryBarTokenizer is updated and becomes better, I want to update to that one.

You can also review this in term of ease of debugging, is it nice with common IDE (I'm currently using Eclipse)?

I'm currently using this as internal research toolset, to compare various implementations. There are some use cases why DefaultTokenizer might be required:

  1. Sometimes we might decide that we want to change the library for Tokenizer (an update makes it better, change of license makes it (in)appropriate). Now, on codes that doesn't care about which Tokenizer is used, but only requiring the best one, using Default makes change easier, since we just need to change which library is the default.

  2. It might be the case that there will be someone trying to use a Tokenizer component while developing new component. While it's true that in research one is supposed to know the details of each component, sometimes we want to test something for which the Tokenizer is not that crucial to the result, but is required. In that case, taking the default might be good (and if the result changes if the Tokenizer is changed, then there is something wrong with how the Tokenizer is used).

Those reasons might not be too compelling, but we decided to use such default implementation, and currently I'm looking for ways to improve how we implement the default, hence this question.

It might be the case that using default is that bad for reasons I haven't considered. In that case, that also qualifies as an answer.

But I will be glad if someone can offer more insights on how this can be improved, or whether it doesn't matter much which way I choose.

2
  • You can't subclass some arbitrary class unless the documentation says it's safe; otherwise, your code will break randomly when the superclass gets changed. Even if it's safe, inheritance creates a really tight coupling. It's usually a bad idea. – Doval Sep 16 '14 at 11:22
  • Oh, the LibraryFooTokenizer is my own wrapper of the original library, so I have full control over it. I thought it's clear from the package structure, since you can't include other library's code inside your package just like that. – justhalf Sep 16 '14 at 11:29
6

You are missing option 3 (and more).

Don't have a DefaultTokenizer.

There is no value, and it leads people to make choices that reduce maintainability later. Additionally, the problem can be solved with sensible documentation.

Reasons not to have the Default:

  1. you actually can't change it later, because people expect the default to be a specific representation, and they just use default because it was convenient.
  2. you will need to continuously maintain option 1, where you wrap the instance.
  3. you have an ugly empty implementation for option 2.

So, just don't do it. Do you see a 'DefaultList' class, or a 'DefaultMap' class? Do you see a 'DefaultInputStream', or 'DefaultNumber'? no.

Default class implementations are bad. Let people read your JavaDoc, and tell them which one is expected to be used in common cases. If the differences were not important, then you would not have the two implementations, so document the differences, and explain when to use which one.

4
  • Thanks for your swift reply! I tend to agree with "the problem can be solved with sensible documentation". However I find your analogy not really suitable for this case. There is no "DefaultMap" class because each map (say, TreeMap and HashMap) has different behaviour (different complexity, different ordering). The same goes for InputStream and Number. But for this one all the library act as something that receive an input sentence and gives an output sentence, with accuracy being the only variable. – justhalf Sep 16 '14 at 4:44
  • @justhalf: Then, is one of the implementations more accurate than another? In that case, why the hell would you tell clients to choose between two? – jhominal Sep 16 '14 at 8:15
  • The real reason there's no Default<DataStructure> is because Java's package system doesn't allow you to substitute classes with equivalent signatures. It's not a bad idea; Java just can't implement it. – Doval Sep 16 '14 at 11:19
  • @jhominal: like I said in the question, because that might change. The libraries are often still under continuous development. – justhalf Sep 16 '14 at 11:30
1

The composition approach (including another Tokenizer in DefaultTokenizer) will be easier to modify in the future. If possible, you shouldn't allow the Library*Tokenizers to be subclassable at all. That said, as long as the rest of the code can only reference a DefaultTokenizer through the Tokenizer interface you'll be able to change how it's implemented without breaking anyone's code.

2
  • So you are saying that there is no compatibility issue with the options I provided. How about other aspects, such as some caveats? Or perhaps emphasize more on the difference? – justhalf Sep 16 '14 at 14:56
  • 1
    @justhalf There's no compatibility issue if no one can hold a reference to DefaultTokenizer. In practice that means making DefaultTokenizer a private inner class of whichever class has the getDefaultTokenizer() method. You could also make it package private if you're sure no one will try to put new code in your package. The main difference is that inheritance creates a tight coupling so you may need to do more work to refactor the DefaultTokenizer in the future, and it also prevents you from making the other Tokenizers final which you really should do if possible. – Doval Sep 16 '14 at 15:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.