javagenericsobserver-pattern

How to write polymorphic function extending abstract class which implements an interface generic function?


I'm trying to implement Observable pattern with an abstract class (so that my subclasses don't provide a common implementation). I want polymorphic functions for different Observer types.

I have an interface IObservable:

public interface IObservable { /* also used version IObservable<O> */
  <O> void registerObserver(O o);
  <O> void removeObserver(O o);
}

I wrote abstract class ObservableBase:

public abstract class ObservableBase implements IObservable {
  protected abstract <O> Set<O> getCollection(O o);

  @Override
  public <O> void registerObserver(O o) {
    getCollection(o).add(o);
  }

  @Override
  public <O> void removeObserver(O o) {
    getCollection(o).remove(o);
  }
}

And I want my concrete class to implement IObservable for different types of Observers:

// This is what it'd look like if I just implemented it by hand for generic interface 
public ConcreteClass implements IObservable<Observer1>, IObservable<Observer2> {
  @Override
  public void registerObserver(Observer1 o) {}

  @Override
  public void registerObserver(Observer2 o) {}

  @Override
  public void removeObserver(Observer1 o) {}

  @Override
  public void removeObserver(Observer2 o) {}
}

My idea is to put register/remove functionality in the base abstract class and provide only collection for this operations. But I cant wrap my hand around this. If I declare in the ConcreteClass specific type for getCollection, compiler complains "Method does not override method from its superclass".

Something like:

public ConcreteClass extends ObservableBase {
  protected Set<Observer1> getCollection(Observer1 o) {/*...*/}

  protected Set<Observer2> getCollection(Observer2 o) {/*...*/}
}

Is it possible?


Solution

  • First, I will recreate what I believe to be the intended class hierarchy with the correct syntax. This is to eliminate any confusion as I attempt to answer your question.


    Is it possible?

    No, you cannot have some class which implements both Observable<Observer1> and Observable<Observer2>. See this other stack overflow question. Ultimately, those generic methods would have the same signature and clash, as the signature in this case is defined by the type bounds.

    You can, however, achieve something similar with a different AbstractObservable implementation:

    public abstract class AbstractObservable<O> implements Observable<O> {
    
        private final Set<Class<?>> allowedTypes;
        private final Set<O> observers;
    
        @SafeVarargs
        protected AbstractObservable(Class<? extends O>... allowedTypes) {
            this.allowedTypes = Set.of(allowedTypes);
            this.observers = new HashSet<>();
        }
    
        //
    
        @Override
        public void registerObserver(O observer) {
            this.checkType(observer);
            this.observers.add(observer);
        }
    
        @Override
        public void unregisterObserver(O observer) {
            this.checkType(observer);
            this.observers.remove(observer);
        }
    
        private void checkType(Object o) {
            for (Class<?> allowed : this.allowedTypes) {
                if (allowed.isInstance(o)) return;
            }
            throw new IllegalArgumentException(
                    "Observer of type " + o.getClass().getName() +
                            " is not permitted");
        }
    
    }
    

    Now you can declare observables with more complex type constraints:

    public class GenericObservable extends AbstractObservable<Object> {
    
        public GenericObservable() {
            super(Object.class);
        }
        
    }
    
    public class FooAndBarObservable
            extends AbstractObservable<Object>
    //                                 ^
    // Instead of Object, this can be another common supertype of Foo and Bar
    {
    
        public FooAndBarObservable() {
            super(Foo.class, Bar.class);
        }
    
    }
    

    Personally, I do not see the motivation. I see 2 reasonable solutions to this issue: