Sajid
Sajid

Reputation: 1011

Design : Best way to solve this?

I have come around this design problem many times so far and I have always solved it the following way. However, I was just wondering if this is the right way to do it or if there is any better way. The language is Java but I would guess this problem would appear in other Strongly Typed Systems as well.

The problem goes like this:

abstract class P
{
   int p1; int p2;
}

class P1 extends P {
   int p3; int p4;
}

class P2 extends P {
   int p5; int p6;
}

abstract class PHandler {
    void handle(P p)
}

class P1Handler{
    void handle(P p) {
          P1 castedP = (P1) p;
          //.....handle logic with P1
    }
}

class P2Handler{
    void handle(P p) {
          P2 castedP = (P2) p;
          //.....handle logic with P2
    }
}

final class PHandlerFactory {
    PHandler getPhandler(P p) {
       //test on P , if/else on either InstanceOf or some attribute of P
       return concrete P
    }
}

// client code

P p = getPFromSomewhereAtRuntime();
PHandler pHandler = factory.get(p);
pHandler.handle(p);

Now, this code never gave me full satisfaction.

1st, I don't like the casting in concrete PHandler. Concrete PHandlers know they type they want to handle at compile time. Why wait till runtime? (Its a Java language limitation that can be avoided with any techniques like double dispatch? couldn't get my head around it)

2nd, The factory violets the OPEN-CLOSED-PRINCIPLE. I usually implement it by reflection/properties file to avoid OCP. EDIT: As we continue to add more instance of P, we need to keep changing the factory (Unless using reflection)

I have also used Annotations on Concrete P implementations. Again, it violets OCP and even worse, P can have different type of Handlers with different purpose.

I would really like to know the experts view on this. Again, this code mostly run in Spring container so any other AOP solution would interest me too.

Upvotes: 2

Views: 169

Answers (2)

Boris the Spider
Boris the Spider

Reputation: 61148

If you know the concrete implementations of P, i.e. if they are not provided from somewhere, then you could use a visitor pattern:

abstract class P {

    int p1;
    int p2;

    abstract void visit(final PVisitor visitor);
}

class P1 extends P {

    int p3;
    int p4;

    @Override
    void visit(PVisitor visitor) {
        visitor.doStuff(this);
    }
}

class P2 extends P {

    int p5;
    int p6;

    @Override
    void visit(PVisitor visitor) {
        visitor.doStuff(this);
    }
}

interface PVisitor {

    void doStuff(P1 p);

    void doStuff(P2 p);
}

So you have a Collection<PVisitor> of your visitor logic then you visit your P instance with your PVisitors and polymorphism will work out what's what.

EDIT

Following the OP's comment I had an Idea.

What about an abstract factory pattern combined with a visitor pattern and some generics.

The thinking is that we know that if we add a new type of Handler then something needs to change - logically it should be the HandlerFactory implementations and not the Ps. What if we have an abstract HandlerFactory and each P implementation is responsible for returning a instance that handles it.

The factory then has a number of methods that return specific handlers, i.e. getDatabaseHandler etc. These Handlers can the inherit from a generic Handler class.

The idea looks something like this:

abstract class P {

    int p1;
    int p2;

    public abstract HandlerFactory<? extends P> getHandlerFactory();
}

class P1 extends P {

    int p3;
    int p4;

    @Override
    public HandlerFactory<P1> getHandlerFactory() {
        return new P1HandlerFactory(this);
    }
}

class P2 extends P {

    int p5;
    int p6;

    @Override
    public HandlerFactory<? extends P> getHandlerFactory() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
}

abstract class HandlerFactory<T extends P> {

    private T t;

    public HandlerFactory(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

    public abstract DatabaseHandler<T> getDatabaseHandler();

    public abstract JMSHandler<T> getJMSHandler();
}

abstract class Handler<T extends P> {

    private final T t;

    public Handler(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

abstract class DatabaseHandler<T extends P> extends Handler<T> {

    public DatabaseHandler(T t) {
        super(t);
    }

    public abstract void persist(Connection con);
}

abstract class JMSHandler<T extends P> extends Handler<T> {

    public JMSHandler(T t) {
        super(t);
    }

    public abstract void send();
}

class P1HandlerFactory extends HandlerFactory<P1> {

    public P1HandlerFactory(P1 t) {
        super(t);
    }

    @Override
    public DatabaseHandler<P1> getDatabaseHandler() {
        return new P1DatabaseHandler(getT());
    }

    @Override
    public JMSHandler<P1> getJMSHandler() {
        return new P1JMSHandler(getT());
    }
}

class P1DatabaseHandler extends DatabaseHandler<P1> {

    public P1DatabaseHandler(P1 p1) {
        super(p1);
    }

    @Override
    public void persist(Connection con) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
}

class P1JMSHandler extends JMSHandler<P1> {

    public P1JMSHandler(P1 p1) {
        super(p1);
    }

    @Override
    public void send() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
}

So each implementation of P comes with a companion HandlerFactory, adding a new implementation of P only requires implementing a HandlerFactory for it. The use of generics allows, say, P1A and P1B which inherit from P1 to just use a P1Handler if they do not require any changes.

If a new type of Handler is added then all the HandlerFactory implementations would need to change to have a method to get that type of Handler but the Ps would not need to change.

Usage would look like

final P1 p1 = new P1();        
final DatabaseHandler<P1> databaseHandler = p1.getHandlerFactory().getDatabaseHandler();

So you can get a specific database handler for an instance from that instance.

Upvotes: 1

Daniel Kaplan
Daniel Kaplan

Reputation: 67340

As @bmorris591 said, "generics is the way to tidy that up". But that may only be because your example is simple enough to do it that way.

It really depends on the context. The way your code is written kind of reminds me of the visitor pattern. That's a way to do double dispatch. It has some pros and cons. The biggest con is it can complicate your code.

In some situations you make P be the interface of the logic the PHandler would have like this:

interface P
{
   void handle();
}

class P1 implements P {
   int p3; int p4;
   void handle () {...}
}

class P2 implements P {
   int p5; int p6;
   void handle () {...}
}

Again, this has pros and cons. It may not make sense from an OO perspective to put the handle implementation inside the P implementations. It's all about the context.

Upvotes: 2

Related Questions