Fabiitch
Fabiitch

Reputation: 51

Java design pattern for use different implementation of method with an abstract class as param

I got a problem of conception in my programm.

I got 4 classes (ChildA, ChildB, ChildC) who extends a parent class (Parent)

i got a method like it :

public void doInteraction(Parent a, Parent b){
a.doActionWith(b);
b.doActionWith(a);
}

My 3 child class and parent class :

public abstract class Parent {
    public abstract void doActionWith(Parent other);
}

class ChildA extends Parent {

    @Override
    public void doActionWith(Parent other) {
        System.out.println("childA use doActionWith abstract");
    }

    public void doActionWith(ChildA chila) {
        System.out.println("childA use doActionWith childA");
    }

    public void doActionWith(ChildB chilb) {
        System.out.println("childA use doActionWith childB");
    }

    public void doActionWith(ChildC chilb) {
        System.out.println("childA use doActionWith childC");
    }

}

class ChildB extends Parent {

    @Override
    public void doActionWith(Parent other) {
        System.out.println("childB use doActionWith abstract");
    }

    public void doActionWith(ChildA childa) {
        System.out.println("childB use doActionWith childA");
    }

    public void doActionWith(ChildB chilb) {
        System.out.println("childB use doActionWith childB");
    }

    public void doActionWith(ChildC chilb) {
        System.out.println("childB use doActionWith childC");
    }

}

class ChildC extends Parent {

    @Override
    public void doActionWith(Parent other) {
        System.out.println("childB use doActionWith abstract");
    }

    public void doActionWith(ChildA childa) {
        System.out.println("childB use doActionWith childA");
    }

    public void doActionWith(ChildB chilb) {
        System.out.println("childB use doActionWith childB");
    }

    public void doActionWith(ChildC chilc) {
        System.out.println("childC use doActionWith childC");
    }

}

When i call a.doActionWith(b); in doInteraction method : the abstract method is always used event the jvm knows the real type of a and b.

Why java dont use specific method with argument match exactly the type of param ?

The only solution i find , is to cast object to their child type. But i need to check their type with instance of.(imagine if i got 20 child class). And i remember my studies that an instance of reveals a bad conception.

I try some stuff like

    public abstract <C extends Parent> void doActionWith(C other);

but same. I imagine a design pattern solve this problem but i dont find it. Maybe i should use interface, but i don't see how i should do it.

Thanks for reading.

fabien.

Upvotes: 1

Views: 1043

Answers (3)

user11595728
user11595728

Reputation:

In one sentence, your problem is that you want to use double dispatch and that is not directly supported by Java. In other words, you want to select a method implementation based on the runtime type of two arguments.

Because it's not supported by Java, you have to emulate the mechanism. There are different ways of doing this. The Visitor pattern is one, but if you really have a situation where any concrete subclass can work with any other concrete subclass, it's not ideal.

Here's another solution, based on the concept of service provider interfaces. Basically, subclasses register their method implementations, and the dispatch based on the target argument is done explicitly by an inherited method. This is a boiled-down code sample that assumes final children classes, but it can be extended to support additional polymorphic flexibility.

public abstract class Parent {

    protected IdentityHashMap<Class<?>, Consumer<Parent>> handlers 
        = new IdentityHashMap<>();

    public final void doActionWith(Parent p) {
        if( !handlers.containsKey(p.getClass())) 
            throw new UnsupportedOperationException();

        handlers.get(p.getClass()).accept(p);
    }
}

final class A extends Parent {
    public A() {
        handlers.put(A.class, a -> System.out.println("A works with A"));
        handlers.put(B.class, b -> System.out.println("A works with B"));
        handlers.put(C.class, c -> System.out.println("A works with C"));
    }
}

final class B extends Parent {
    public B() {
        handlers.put(A.class, a -> System.out.println("B works with A"));
        handlers.put(B.class, b -> System.out.println("B works with B"));
        handlers.put(C.class, c -> System.out.println("B works with C"));
    }
}

final class C extends Parent {
    public C() {
        handlers.put(A.class, a -> System.out.println("C works with A"));
        handlers.put(B.class, b -> System.out.println("C works with B"));
        handlers.put(C.class, c -> System.out.println("c works with C"));
    }
}

Note: In a realistic application it will be easier and safer to write handler code if it can refer to the specific methods of individual children classes without having to downcast. This can be achieved by adding a generic method in Parent:

protected <T extends Parent> void addHandler(Class<T> clazz, 
    Consumer<? super T> handler) {
    handlers.put(clazz, (Consumer<Parent>)handler);
}

Upvotes: 1

IlyaMuravjov
IlyaMuravjov

Reputation: 2492

You can use the visitor pattern:

public interface Parent {
    default void actWith(Parent parent) {
        parent.accept(this);
    }
    void accept(Parent parent);
    void doActionWith(ChildA childA);
    void doActionWith(ChildB childB);
    void doActionWith(ChildC childC);
}

public class ChildA implements Parent {
    @Override
    public void accept(Parent parent) {
        parent.doActionWith(this);
    }

    @Override
    public void doActionWith(ChildA childA) {
        System.out.println("childA use doActionWith childA");
    }

    @Override
    public void doActionWith(ChildB childB) {
        System.out.println("childA use doActionWith childB");
    }

    @Override
    public void doActionWith(ChildC childC) {
        System.out.println("childA use doActionWith childC");
    }
}

public class ChildB implements Parent {
    @Override
    public void accept(Parent parent) {
        parent.doActionWith(this);
    }

    @Override
    public void doActionWith(ChildA childA) {
        System.out.println("childB use doActionWith childA");
    }

    @Override
    public void doActionWith(ChildB childB) {
        System.out.println("childB use doActionWith childB");
    }

    @Override
    public void doActionWith(ChildC childC) {
        System.out.println("childB use doActionWith childC");
    }
}

public class ChildC implements Parent {
    @Override
    public void accept(Parent parent) {
        parent.doActionWith(this);
    }

    @Override
    public void doActionWith(ChildA childA) {
        System.out.println("childC use doActionWith childA");
    }

    @Override
    public void doActionWith(ChildB childB) {
        System.out.println("childC use doActionWith childB");
    }

    @Override
    public void doActionWith(ChildC childC) {
        System.out.println("childC use doActionWith childC");
    }
}
public static void interact(Parent a, Parent b) {
    a.actWith(b);
    b.actWith(a);
}

Upvotes: 1

ernest_k
ernest_k

Reputation: 45319

You're relying on both static and dynamic binding, but at least one of these is hurting your code unnecessarily:

abstract Parent.doActionWith(Parent) allows you to achieve two things:

  • When implemented in ChildA, ChildB, and ChildC, the behavior can be customized to fit exactly what's desired in those sub-classes. This is the benefit of dynamic binding. However, dynamic binding is based on the object on which the method is invoked, not on the runtime class of the parameter that's passed. This means that the compiler will always choose Parent.doActionWith(Parent) in your case because that's the only one the Parent type knows of.
  • You can call the doActionWith method with an argument that's typed as any sub-class of Parent. However, since the static (compile-time) type of the objects on which you're invoking the method is Parent, the method selected will always be doActionWith(Parent); again, because Parent knows of no other signature than doActionWith(Parent).

As a quick recap:

  • The compiler decides which signature of the method will be called. That's based on the declared data types of the objects.
    Because the declared type of the argument is Parent, then it doesn't matter what the runtime class of the argument will be: the method signature called will the one that declares Parent as parameter type.
  • The runtime decides which implementation of the compile-time-chosen signature will be executed. You are getting this, but it's bound to be the method with Parent as parameter type in the subclass (ChildA, ChildB, or ChildC, depending on the runtime class of the object on which the method is called).

In other words, if you want both static and dynamic bindings to apply, you need to:

  1. Declare all the overloaded methods in the abstract class
  2. Change the signature of doInteraction(Parent a, Parent b) to have all combinations of applicable sub-classes in parameter lists.

Clearly, this is not desirable. The following is my opinion: I'd simplify things by:

  1. Getting rid of overloaded methods
  2. Overriding the implementation of the same signature of doActionWith(Parent) in each sub-class

With this, the key is how you override the doActionWith method. Ideally, this method should not need to know the runtime class of its argument, and this is a good thing.

Upvotes: 1

Related Questions