Sunday, April 19, 2009

toward an open closed equals and subclassing strategy

I have to say that this is a very buzz title.

I'm going to write about equals, subclassing, interchangeability between concrete representations... stuffs like that.

Object identity is difficult to define for me.

In some languages like java the .equals() as defined in the Object class indicates that the other object is the same object. It is stored the same memory location.
That implementation is the same as the '==' operator.

The equals is supposed to be overriden for any not mutable object so the meaning will be: object with the same state.
The java Set apis don't store duplicate objects, where an object is a duplicate of another if the former and the latter are equals (respect of the equals method implementation).

Some other examples can be less obvius:
think about classes that manages some rational numbers api we are wording on.

Will 1/2 equals to 2/4 for some rational numbers apis we are implementing?

And what if we talk about "fractions".
Some math guys (domain experts) would tell us that the two fracitions "1/2" and "2/4" are technically not equals, but the two "rational numbers" 1/2 and 2/4 are the same.

New Rational(1,2).equals(new Rational(2,4)) will be true, and new Fraction(1,2).equals(2,4) will be false.

So our choose depends of the name.

Does it make sense? mmm... yes, as long as we agree that we act under the umbrella of algebraic principles like"rational numbers are actually the equivalence classes of fractions, where the equivalence is defined as the fraction n/d is equivalent to n2/d2 if ..... [any definition that you like. One is: (n/d) * (d2/n2) = a/b where a and be are two equals relative number]"

Too heavy!

Yeah... right, but this is the art of computer programming: how much I'll go deep to the domain, to be consistent with some common sense future expectation, apply some design principles and so on...

Or we can avoid that: for example we are not going to write a unit test like
assertEquals(new Rational(1,2), new Rational(2,4));

Any time we display to the user a fraction, then we do GCD calculation and reduce the fraction, so the system will work perfectly as a black box.

Some other guy can get my code and say: for the I think that we deserve that kind of test, and if it does not work, then we have a problem. Even if there is no domain specification, customer requirement, about that. It's just common sense, comes up with least astonishing result, or because, for my math, algebraic background it is really strange that the domain is modelled without recognizing such obvius thing.

Thinking in this way is ok, and you can go much more deeper, also with much more implications, and the suggested solution can be reasonabile, but not obvius in the way it could be implemented.

Coexistence of different implementations of complex numbers: polar and rectangular.

There can even be different actual classes for them. Why not? Team A assumed rectangular representation, team B assumed polar representation and they developed their own implementations.

They were thinking about the "same" thing, but they did not implemented them in a compatibile way.

Now do we want make those classes working together? How?

Will we make them collapse in the same class? With two different constructor? One that takes the real and img parameter, and another one that takes the angle and magnitude?

Can be done, if the two constructor are not both considered as the same type (float, or double, for instance).

Another way to think that is like a pattern that abstracts the actual representation by a factory, and a way to define a metalevel description of equivalence.

The object will be seen only by an interface, (a "contract") with all the "observers" methods, and the equals can be implemented just cheking the fact that the other class implements the contract interface and that the interface defined observers produce all the same result.

Makes sense?
Can be: we are looking for something like

a = myObjectFactory.getObject(1,1);
b= myObjectFactory.getObject(sqrt(2),math.PI/4)
//a.equals(b) is true

and, moreover, we are free to use any implementation, and decide at run time to select any of them without changing the behavior of the program.

The next step is the ability to hide also the object creation diversity, disinguishing from the "apparent" implementation and the "actual" implementation:

a = myObjectFactory.getObject(1,1);
// a.getClass() will return Polar
b= myObjectFactory.getObject(1,1,);

// a.getClass() will return rectangular
// a.equals(b) is true

We need to add at some metaclasses level the code conversion between different representations, and some guarantee about interchangeability, like with a lot of unit tests, even with random generated samples.

Let's go back to plain numbers. Another issue can be the "extensions":
we defined math about relative numbers, and we would like to extend those relative numbers to rationals.

They will be perfectly compatible. you can do 1+ 1/2 and get 3/2 as result.

We can see rationals as a class extension of naturals, and make the things work so 1 and 1/1 are considered equals.

Making that kind of thing works, can be not trivial, if you don't want surprises. There are two kind of equals implementations: instanceof and getClass().
The "instanceof" based implementation of equals does not respect the equals contract (being reflexive, transitive, simmetric), however the .getClass() based implementation returns false for objects that belongs to different classes.
So comparison with subclasses will be always false, then 1 and 1/1 will not be equals.
We can modify the rational number extendings the tests, including checking if the class belongs to the rational subclass.

That would be necessary for any extensions, and in that sense our equals implementation is not closed to extension.

There is an equals implementation tecnique, based on tree traversal, (google keywords: "secrect of equals") that is actually open to extensions and closed to modification.

Being always closed to modification can be impossible.
You will much more likely try to be closed to modification and open to extensions respect of the more likely future code evolutions (strategical clousure).
Fortunately for the equals there exist a closed (to subclassing) solution, so there is not strategy to think.
It is not so easy to understand, and any IDE generates the equals only according to the "instance of" or "getClass" alternatives.
Moreover, nowadays people don't like subclassing at all.

The mainstream approach is avoiding subclassing and prefer composition, so seems to me that nobody also cares about smart equals strategy open to extensions respect of subclassing.

No comments: