The problem you are describing is called double dispatch. The name comes from the fact that you need to decide which bit of code to execute (dispatch) based on the types of two objects (hence: double).
Normally in OO there is single dispatch - calling a method on an object causes that object's implementation of the method to execute.
In your case, you have two objects, and the implementation to be executed depends on the types of both objects. Fundamentally, there is a coupling implied by this which "feels wrong" when you've previously only dealt with standard OO situations. But it's not really wrong - it's just slightly outside the problem domain of what the basic features of OO are directly suited to solving.
If you're using a dynamic language (or a static-typed language with reflection, which is dynamic enough for this purpose) you can implement this with a dispatcher method in a base class. In pseudo-code:
class OperatorBase
{
bool matchCompareCriteria(var other)
{
var comparisonMethod = this.GetMethod("matchCompareCriteria" + other.TypeName);
if (comparisonMethod == null)
return false;
return comparisonMethod(other);
}
}
Here I'm imagining that the language has a built-in method in every class called GetMethod
that allows me to look up a method by name, and also a TypeName property on every object that gets me the name of the type of the object. So if the other class is a GreaterThan
, and the derived class has a method called matchCompareCriteriaGreaterThan, we will call that method:
class SomeOperator : Base
{
bool matchCompareCriteriaGreaterThan(var other)
{
// 'other' is definitely a GreaterThan, no need to check
}
}
So you just have to write a method with the correct name and the dispatch occurs.
In a statically typed language that supports method overloading by argument type, we can avoid having to invent a concatenated naming convention - for example, here it is in C#:
class OperatorBase
{
public bool CompareWith(object other)
{
var compare = GetType().GetMethod("CompareWithType", new[] { other.GetType() });
if (compare == null)
return false;
return (bool)compare.Invoke(this, new[] { other });
}
}
class GreaterThan : OperatorBase { }
class LessThan : OperatorBase { }
class WithinRange : OperatorBase
{
// Just write whatever versions of CompareWithType you need.
public bool CompareWithType(GreaterThan gt)
{
return true;
}
public bool CompareWithType(LessThan gt)
{
return true;
}
}
class Program
{
static void Main(string[] args)
{
GreaterThan gt = new GreaterThan();
WithinRange wr = new WithinRange();
Console.WriteLine(wr.CompareWith(gt));
}
}
If you were to add a new type to your model, you would need to look at every previous type and ask yourself if they need to interact with the new type in some way. Consequently every type has to define a way of interacting with every other type - even if the interaction is some really simple default (such as "do nothing except return true
"). Even that simple default represents a deliberate choice you have to make. This is disguised by the convenience of not having to explicitly write any code for the most common case.
Therefore, it may make more sense to capture the relationships between all the types in an external table, instead of scattering it around all the objects. The value of centralising it will be that you can instantly see whether you have missed any important interactions between the types.
So you could have a dictionary/map/hashtable (whatever it's called in your language) that maps a type to another dictionary. The second dictionary maps a second type to the right comparison function for those two types. The general CompareWith function would use that data structure to look up the right comparison function to call.
Which approach is right will depend on how many types you're likely to end up with in your model.