Home > Enterprise >  How to define a general interface for several type-specific classes?
How to define a general interface for several type-specific classes?

Time:12-16

While trying to program to interfaces, I regularly find myself in the following situation:

  • I have several very similar classes representing containers or algorithms for different types.
  • I would like to define a common interface for these classes.

Consider, e.g., a string container. Such a container will likely have string processing methods. Since those methods are easily represented using generic interfaces, I am ignoring them. Here, I want to focus on methods that can be used to process or provide references to other string containers:

public class StringContainer {
    StringContainer produce() {
        return new StringContainer();
    }

    void consume(StringContainer stringContainer) {
    }
}

This class can be used just fine in code like:

public class Main {
    public static void main(String[] args) {
        StringContainer stringContainer = new StringContainer();
        stringContainer.produce();
        stringContainer.consume(stringContainer);
    }
}

The problem is: I'm using a concrete class and not an interface to refer to the string container. What if I want to introduce a double container or a list container later and want to leave the rest of the code as is?

Maybe generics could form a solution to this problem? Here is my try. I first define a generic container class:

interface Container<T> {
    Container<T> produce();

    void consume(Container<T> container);
}

I then create type-specific implementations of the form:

public class StringContainer implements Container<String> {
    @Override
    public Container<String> produce() {
        return new StringContainer();
    }

    @Override
    public void consume(Container<String> container) {
    }

    public void consume(StringContainer container) {
    }
}

The above classes can be used as follows:

public class Main {
    public static void main(String[] args) {
        Container<String> stringContainer = new StringContainer();
        stringContainer.produce();
        stringContainer.consume(stringContainer);
    }
}

However, the above approach has several drawbacks:

  1. The consume(Container<String> container) method accepts other types than StringContainer.
  2. In consume(Container<String> container), the parametrized type Container<String> has to be used when processing container. I can't assign it to StringContainer variables (without type checks or casts).
  3. The alternative consume(StringContainer container) method is defined for StringContainer objects, but can't be called from a Container<String> reference.
  4. Finally, to me, the line Container<String> stringContainer = new StringContainer(); has an awkward-looking notation that suggests a diamond operator is missing in new StringContainer().

What is the idiomatic way to define a general interface for several type-specific classes, which doesn't have (all) the above drawbacks?

Should I ignore point 4 and address points 1 and 2 by adding type checks/casts, throwing an UnsupportedOperationException or IllegalArgumentException in case passed objects aren't StringContainers? Or is there another way to use generics? Can type bounds help me, for example? Or should I look for a solution outside of generics?

CodePudding user response:

Is this what you're looking for? It's called a recursive type bound.

interface Container<T extends Container<T>> {
    T produce();

    void consume(T container);
}

class StringContainer implements Container<StringContainer> {
    @Override
    public StringContainer produce() {
        return new StringContainer();
    }
    
    @Override
    public void consume(StringContainer container) {
    }
}

CodePudding user response:

It seems that you have two APIs, and you should treat them separately with separate interfaces. Yes, you can merge them into the same interface with distinct method names.

I think you should have two interfaces for your "containers" and for your "containers of containers". Here's what I'd make it:

interface Container<T> {
    T produce();
    void consume(T container);
}

interface MetaContainer<T, R extends Container<T>> {
    R produceContainer();
    void consumeContainer(R container);
}

class StringContainer implements Container<String>, MetaContainer<String, StringContainer> {
    @Override
    public String produce() {
        return "";
    }

    @Override
    public void consume(String container) {
    }

    @Override
    public StringContainer produceContainer() {
        return this;
    }

    @Override
    public void consumeContainer(StringContainer container) {
    }
}

I implemented both interfaces using the same class to emulate your StringContainer class.

  • Related