Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java generics - too complicated? How to simplify?

Originally I had posted the question on CodeReview, but this is more suited for StackOverflow probably.

I'm coding for a multi-step process, using Java 6. Say there are 3 of these steps.
Each accepts the same type of input. Lets begin.

This is the object which is passed as input to each step. This object acts as a wrapper for another type of object, alongside some steps' shared values. Be aware names are translated to a more generic domain and in english, originals are in Italian.

public class EntityStepInput<T extends Entity> {
    public final T entity;
    public boolean modified;
    public boolean canceled;

    public EntityStepInput(final T entity) {
        this.entity = entity;
    }
}

This is the interface used by each step.

public interface EntityStep<T extends EntityStepInput<? extends Entity>> {
    void process(final T stepInput) throws Exception;
}

Now, 2 out of the 3 steps must accept an EntityStepInput which contains a Entity or any type derived from it.

public class FirstEntityStep implements EntityStep<EntityStepInput<? extends Entity>> {
    @Override
    public void process(final EntityStepInput<? extends Entity> stepInput) throws Exception {}
}

public class SecondEntityStep implements EntityStep<EntityStepInput<? extends Entity>> {
    @Override
    public void process(final EntityStepInput<? extends Entity> stepInput) throws Exception {}
}

The last step must accept an EntityStepInput which contains a specific type derived from Entity.

public class ThirdEntityStep implements EntityStep<EntityStepInput<? extends DerivedEntity>> {
    @Override
    public void process(final EntityStepInput<? extends DerivedEntity> stepInput) throws Exception {}
}

The usage is pretty straighforward. I have overloaded methods which accept different types of Entitys. What follows is a simplified version.

public void workWithEntity(final DerivedEntity entity) throws Exception {
    final EntityStepInput<DerivedEntity> stepInput = new EntityStepInput<DerivedEntity>(entity);

    stepOne.process(stepInput);
    stepTwo.process(stepInput);
    stepThree.process(stepInput);
}

As you can see the DerivedEntity type is able to use all of the steps.

public void workWithEntity(final OtherDerivedEntity entity) throws Exception {
    final EntityStepInput<OtherDerivedEntity> stepInput = new EntityStepInput<OtherDerivedEntity>(entity);

    stepOne.process(stepInput);
    stepTwo.process(stepInput);
}

And here another type of Entity cannot use the last step, which is what I want.

Now, this has became quite complex with generics. I fear who will read my code after I'm gone won't understand and sooner or later a mess is going to be made.

Is this simplifiable? What would your approach be like to respect as much as possibile the single responsibility principle?

Edit. Entity hierarchy is as follow:

Entity > DerivedEntity
Entity > OtherDerivedEntity
like image 603
LppEdd Avatar asked Jul 14 '18 14:07

LppEdd


People also ask

Are generics complex types?

A generic type or generic class is a complex type that can be combined with other different types to produce a new type. The most common application of generic types is for “container” classes like lists, stacks, and queues.

Is it good to use generics Java?

Code that uses generics has many benefits over non-generic code: Stronger type checks at compile time. A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.

When would you not use generics in Java?

Limitation of generics Firstly, primitive types (like int , long , byte , …) are not allowed to be used in generics. It means whenever you need to parameterize your generic type with a primitive one, the respective class wrapper ( Integer , Long , Byte , …) has to be used instead.


1 Answers

Here's my second attempt at an answer. I think the system that you're using currently looks good; it provides compile-time checking, as you don't want to allow Step #3 to attempt to process any type which is not a DerivedEntity (or one of its children).

You can simplify this by using a List<Consumer<? extends Entity>>, but you lose compile-time type checking for Step #3 and are forced to ameliorate it by using instanceof:

Java 8+ solution:

List<Consumer<Entity>> processes = new ArrayList<>();

processes.add(entity -> {
    // Process first step.
});

processes.add(entity -> {
    // Process second step.
});

processes.add(entity -> {
    if (!(entity instanceof DerivedEntity)) {
        System.out.println("Step 3: The entity must be a DerivedEntity!");
        return;
    }
    // Process third step.
});

To pass an Entity through the pipeline, it's as simple as:

processes.forEach(consumer -> consumer.accept(entity));

Java 6+ solution (let's create our own Consumer interface!):

public interface Consumer<T> {
    void accept(T t);
}

The same code as above, but using our Consumer interface:

List<Consumer<Entity>> processes = new ArrayList<Consumer<Entity>>();

processes.add(new Consumer<Entity>() {
    @Override
    public void accept(Entity entity) {
        // Process first step.
    }
});

processes.add(new Consumer<Entity>() {
    @Override
    public void accept(Entity entity) {
        // Process second step.
    }
});

processes.add(new Consumer<Entity>() {
    @Override
    public void accept(Entity entity) {
        if (!(entity instanceof DerivedEntity)) {
            System.out.println("Step 3: The entity must be a DerivedEntity!");
            return;
        }
        // Process third step.
    }
});

Entity entity = new DerivedEntity();

for (Consumer<Entity> consumer : processes) {
    consumer.accept(entity);
}
like image 181
Jacob G. Avatar answered Nov 17 '22 01:11

Jacob G.