Class relationship design problem
I have a situation where several classes are of the same "type", but they each have different behavior. They don't really share enough of the same functionality to have a heirarchy, but I would like to be able to treat them as the same type as they will be stored together.
Here's an analogy, say I have a card game where the four suits of the deck each mean different things in terms of gameplay. The hearts are used differently than the spades, etc, so there isn't really any shared behavior besides the fact that they're all cards, and can be stored in a deck, or in a player's hand together.
My question is how should I design the classes. If I just have them all extend a base card class, I solve the problem of storing them in a List together, but then I have to do type checking anytime I use them (blech!). If I give them each their own separate type, I can store them as objects in the same List, but I still have to do type checking (also blech).
Does anyone know of an established pattern, or any solution that I could use to solve this?
# 1
I'd say your description is a bit too vague. To me, it sounds like that, at least, their commonality is how they get stored. Hence, a common Interface could be implemented by each of the classes wrt. handling for storage and retrieval.
What exactly do you mean with "same type", if they don't share much of behaviour?
# 2
> I'd say your description is a bit too vague. To me,
> it sounds like that, at least, their commonality is
> how they get stored. Hence, a common Interface could
> be implemented by each of the classes wrt. handling
> for storage and retrieval.
That was one of my ideas, but then when I need to call a method that isn't common between the classes, I'd need to use instanceof or reflection to determine it's type.
> What exactly do you mean with "same type", if they
> don't share much of behaviour?
I mean the same type is that they're all cards, but they each have different functionality. Here's an example:
public class Card {
// base class
}
public class Heart extends Card {
public void operationA() { }
}
public class Spade extends Card {
public void operationB() { }
}
Since they don't share methods, Card couldn't be an interface with the common methods, nor could the class Card have those methods.
Now I can store the cards in a List<Card>, but to retrieve a Heart for example from the List and call operationA, I'd need to determine it's type first. That will be a lot of type checking throughout my program, there has to be a better way.
# 3
> when I need to
> call a method that isn't common between the classes,
> I'd need to use instanceof or reflection to determine
> it's type.
If there is only one place where you have to branch depending on the actual type, this is not so much of an issue. Not aesthetical, I agree, but not necessarily a sign of "bad design".
The risk is if such conditionals are scattered throughout many places in the program, especially if the range of types may evolve in the future: then you have to traverse all those many places and mayvbe add the conditional block for a new type...
You might consider the Visitor pattern; although its primary intent is to enable to add operations without altering the classes operated on, it also does a nice job at hiding the actual type to the client code.
On the other hand, adding a new type to be visited is a bit tedious (you have to maintain all the Visitor classes).
# 4
model the operations, eg AbstractOperation extended by OperationA and OperationB, each taking Heart and Spade respectively; with the argument for the abstract implementation taking Object as the argument.
# 5
> That was one of my ideas, but then when I need to
> call a method that isn't common between the classes,
> I'd need to use instanceof or reflection to determine
> it's type.
Sure, but that's how it works. Who else but you will be able to determine what kind of object you have and what to do with it? If they have no common functionalities (for example, same method but different operation), I cannot think of a way to unify them. jduprez gave another example on how to kind of unify operations. But if you not even have a similar context ...
> > What exactly do you mean with "same type", if they
> > don't share much of behaviour?
>
> I mean the same type is that they're all cards, but
> they each have different functionality. Here's an
> example:
No, your example showed two totally different types, which have nothing in common. If types are "similar", there will be commonalities. And, of course, Card could as well be an empty Interface. If there are no commonalities, you could also use a List<Object>.
I'm quite sure, your description is far too abstract wrt. what you actually have at hand. If you stay with cards and a play, I could at least think of each card (regardless of its color) operating on the same context, e.g.:public abstract class Card {
public abstract Context operate(Context context);
}
where each subclass would implement operate to operate on a given context and returning an affected context, e.g.:public class Heart extends Card {
public Context operate(Context context) {
if (context.getCard() instanceof Heart) {
return context.getChildContext(nextCard);
}
return context;
}
}
(no good code, just for demonstration purpose).
# 6
maybe,
class Card {
// base class
}
class Heart extends Card {
//public void operationA() { }
}
class Spade extends Card {
//public void operationB() { }
}
class AbstractCardOp
{
public void doIt( Object o ) {
System.out.println( "aco do: " + this.toString() + o );
}
}
class OperationA extends AbstractCardOp
{
public void doIt( Heart o ) {
System.out.println( this.toString() + o );
}
}
class OperationB extends AbstractCardOp
{
public void doIt( Spade o ) {
System.out.println( this.toString() + o );
}
}
public class CardOp
{
public static void main( String[] args ) {
OperationA opA = new OperationA();
OperationB opB = new OperationB();
opA.doIt( new Heart() );
opA.doIt( new Spade() );
opA.doIt( new Heart() );
opA.doIt( new Spade() );
System.out.println( "" );
opB.doIt( new Heart() );
opB.doIt( new Spade() );
opB.doIt( new Heart() );
opB.doIt( new Spade() );
}
}
# 7
Well, this provides a bit more flexibility, without the need to change Card when adding operations. Correlates to the Command Design Pattern.
# 8
> Sure, but that's how it works. Who else but you will
> be able to determine what kind of object you have and
> what to do with it? If they have no common
> functionalities (for example, same method but
> different operation), I cannot think of a way to
> unify them. jduprez gave another example on how to
> kind of unify operations. But if you not even have a
> similar context ...
Yeah, that was part of the problem, finding a way to automatically determine type while still having a shared parent type.
> No, your example showed two totally different types,
> which have nothing in common. If types are "similar",
> there will be commonalities.
That's true, there aren't really any commonalities in their functionality, I'm really just trying to shoehorn polymorphism into this for convinience. It seems like I'm creating more work than necessary for myself. Maybe I should just give up on trying to share a type between them and just keep a collection of different collections, each holding a different type. Hmmm...
> And, of course, Card
> could as well be an empty Interface. If there are no
> commonalities, you could also use a List<Object>.
> I'm quite sure, your description is far too abstract
> wrt. what you actually have at hand. If you stay with
> cards and a play, I could at least think of each card
> (regardless of its color) operating on the same
> context, e.g.:> public abstract class Card {
> public abstract Context operate(Context
> context);
>
>
where each subclass would implement operate to
> operate on a given context and returning an affected
> context, e.g.:> public class Heart extends Card {
>public Context operate(Context context) {
>if (context.getCard() instanceof Heart) {
> return context.getChildContext(nextCard);
> }
> return context;
>
>
(no good code, just for demonstration purpose).
# 9
> maybe,
>
> >
> class Card {
>// base class
>
> lass Heart extends Card {
>//public void operationA() { }
>
> class Spade extends Card {
>//public void operationB() { }
>
> class AbstractCardOp
> {
>public void doIt( Object o ) {
> tem.out.println( "aco do: " + this.toString() + o );
>}
>
> class OperationA extends AbstractCardOp
> {
>public void doIt( Heart o ) {
> tem.out.println( this.toString() + o );
>}
>
> class OperationB extends AbstractCardOp
> {
>public void doIt( Spade o ) {
> tem.out.println( this.toString() + o );
>}
>
> public class CardOp
> {
>public static void main( String[] args ) {
> rationA opA = new OperationA();
> OperationB opB = new OperationB();
> opA.doIt( new Heart() );
> opA.doIt( new Spade() );
> opA.doIt( new Heart() );
> opA.doIt( new Spade() );
> System.out.println( "" );
> opB.doIt( new Heart() );
> opB.doIt( new Spade() );
> opB.doIt( new Heart() );
> opB.doIt( new Spade() );
>}
>
>
Very interesting, I'll have to think about this harder and see if I can use it to make my design simpler. Although now that I've worked out the design more, I'm not sure I'll try to have a common base type at all.
Thanks for all the good ideas everyone! You've given me a lot to work with!
