Reputation: 51
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
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
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
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:
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.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:
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.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:
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:
doActionWith(Parent)
in each sub-classWith 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