In visual representations of object oriented software designs, people traditionally draw subclass relations using an arrow that points from the subclass to the superclass. This seemed strange to me when I first learned it -- intuitively it seemed like the arrow should go the other direction. I remember reading a justification somewhere, based on the argument that subclasses "knew about" their superclass, but not vice versa. That argument always seemed weak to me.
However, I just realized that the direction of the arrow actually corresponds to implication in logic. In particular, the Liskov Substitutability Principle is exactly the same as Modus Ponens.
Cute.
Posted on June 7, 2006 08:23 PM
More languages articles
Of course, in Haskell code, the "inheritance" arrow goes in the other direction - from superclass to subclass. So it means "is implied by", even though it looks like "implies". This irritates me.
Posted by: Robin Green at June 9, 2006 02:16 PMRobin, I'm not sure what you're referring to. The only arrows I know about in Haskell are "->" which is used for functions (very similar to implication in a constructive proof), and "=>" which is used for type constraints (also similar to implication, in that if you meet the constraints, then the consequent holds).
The closest thing I know of to inheritance in Haskell is typeclass inheritance, and that's specified using "instance" declarations rather than arrows. Can you clarify what you mean?
Posted by: Kim at June 12, 2006 07:21 PMI thought it was odd, too, the first time I saw it. But it turns out that "weak" reason you cite is actually at the core of what makes UML useful.
In UML diagrams, there are three kinds of arrows:
- Association -- If Class A has a member whose type is Class B, a solid arrow is drawn from A to B.
- Inheritance -- If Class A is a subclass of Class B, a solid arrow with an open triangle arrowhead is drawn from A to B
- Dependence -- If Class A depends on Class B in some other way (e.g., Class A has a method with a parameter of type B), a dashed arrow is drawn from A to B
All of these are dependences: The specification of Class A relies on the existence of Class B, or "knows about it" as you put it. In programming language terms, the source code for Class A will mention Class B, but not vice versa.
It's very useful that all three of these arrows point the same way. It lets you reason about program structure by just looking at the diagram as a graph.
For example, if I'm compiling classes separately, in what order do I need to compile them? Answer: reverse topological order, considering all arrows.
If I'm building software in modules, do I have a cyclic dependence that will make it hard to maintain modules separately? Answer: draw dotted lines around the classes within each module, consider the reduced graph (again considering all arrows) and see if you've got cycles.
Having the arrows point this way also helps illustrate certain kinds of program transformations (aka refactorings). Examples:
- Dependence Inversion Principle (DIP): Martin Fowler's class example: a Copier class uses a FileReader and a FileWriter (solid arrows from Copier to each of FileReader and FileWriter). The Copier algorithm cannot be reused with other types of I/O. Refactored solution has Copier use a Reader interface and a Writer interface (solid arrows from Copier to Reader and Writer), with FileReader and FileWriter implementing the interfaces (solid-open arrows from FileReader to Reader and FileWriter to Writer). The "inversion" is visible in that the arrow on FileReader (or FileWriter) has now been flipped.
- Breaking a cyclic dependence by inserting an interface (very similar to DIP): A uses B and B uses A (solid arrows in both directions). If they need to go in separate modules, introduce an interface by having A use iB, which B implements. Now you've got a solid arrow from A to iB, a solid arrow from B to A, and a solid-open arrow from B to iB. Graph is now acyclic.
- Replacing inheritance with association. Class A was implemented as a subclass of Class B, but not because A is really a kind of B, but merely because A has a B and has some operations in common. In the improved code, A has a pointer to a B, and delegates methods as necessary. This frees A to inherit from some more appropriate base (e.g.). The arrow still points in the same direction, only the arrowhead has changed; this helps illustrate that the relationships have not fundamentally changed.
Of course, all of the above would still hold if you flipped around *all* the arrows. But while the direction of the inheritance arrow is somewhat counter-intuitive, surely the direction of the association arrow is not: if A has a B, everybody wants to draw the arrow from A to B since that direction coincides with the way we all think about "pointers".
This was an epiphany for me when I figured it out long ago, and it's one of the reasons I find UML so useful.
Some systems maintain the distinction between specialization and generalization, based on the direction of the arrow: i.e. the order in which the classes are constructed, the direction of information flow, and dependency.
Specialization is a top-down selection of existing abstractions to combine and extend, but generalization is a bottom-up abstraction and restriction of common existing features.
Mik