Search code examples
javagenericscomposition

Java - Generalize different classes, similar methods (without changing the subclasses?)


Not sure if this is possible, but I have a case in which two interfaces have the same method. These are interfaces that are given, so I can't change them.

Given Interfaces

interface SomeGivenService {
    boolean authenticate(String username, String password);
    Object someSpecialMethod(Object param);
}

interface AnotherGivenService {
    boolean authenticate(String username, String password);
    String aGreatMethod();
    String sayHello();
}

To consume this service, I created a class and did some handling in case this service throws an error.

class SomeGivenServiceConsumer {

    SomeGivenService a;

    public SomeGivenServiceConsumer(SomeGivenService a) {
        this.a = a;
    }

    public authenticate(MyUserPassBean bean) {
        try {
            a.authenticate(bean.username, bean.password);
        } catch (Exception e) {
            throw new MyException();
        }
        ...
    }
}

class AnotherGivenServiceConsumer {

    AnotherGivenService a;

    public AnotherGivenServiceConsumer(AnotherGivenService a) {
        this.a = a;
    }

    public authenticate(MyUserPassBean bean) {
        try {
            a.authenticate(bean.username, bean.password);
        } catch (Exception e) {
            throw new MyException();
        }
        ...
    }
}

Is it possible to avoid this code duplication in my consumers? I probably will have many of them and wanted to avoid this duplicated code. I initially thought of changing my consumer to receive an interface that implements this authentication, but as I can't change the given interfaces, not sure if this is even possible.

Is it possible to have a "Generic interface which has a method?" or use some design pattern? Any ideas? What I was trying:

class AnotherGivenServiceConsumer {

    AnotherGivenService a;
    GivenServiceAuthenticable b;

    public AnotherGivenServiceConsumer(AnotherGivenService a, 
                                       GivenServiceAuthenticable b) {
        this.a = a;
        this.b = b;
    }

    public authenticate(MyUserPassBean bean) throws MyException {
        return b.authenticate(bean.username, bean.password);
    }
}

interface GivenServiceAuthenticable<T> {
    boolean authenticate(T givenService, MyUserPassBean bean);
}

class GivenServiceAuthenticableImpl<T> implements GivenServiceAuthenticable<T> {
    boolean authenticate(T givenService, MyUserPassBean bean) {
        try {
            //this won't compile as it's a generic class..
            t.authenticate(bean.username, bean.password); 
        } catch (Exception e) {
            throw new MyException();
        }
        ...
    }
}

Other problem is how to instantiate this object if I can't change it to implement my new objects?


Solution

  • You can use the template pattern to implement the common functionality in a base class, while delegating the single varying line to subclasses:

    abstract class ConsumerBase {
        public void authenticate(MyUserPassBean bean) {
            try {
                authenticate(bean.username, bean.password);
            } catch (Exception e) {
                throw new MyException();
            }
            //...
        }
    
        protected abstract boolean authenticate(String username, String password);
    }
    
    class SomeGivenServiceConsumer extends ConsumerBase {
    
        SomeGivenService a;
    
        public SomeGivenServiceConsumer(SomeGivenService a) {
            this.a = a;
        }
    
        @Override
        protected boolean authenticate(String username, String password) {
            return a.authenticate(username, password);
        }
    }
    
    class AnotherGivenServiceConsumer extends ConsumerBase {
    
        AnotherGivenService a;
    
        public AnotherGivenServiceConsumer(AnotherGivenService a) {
            this.a = a;
        }
    
        @Override
        protected boolean authenticate(String username, String password) {
            return a.authenticate(username, password);
        }
    }