Once you have forgotten the real type of an object by passing it as one of its super-types there isn't a safe way to get it back because:
subtypeInstance is a supertypeInstance
but
supertype instance is not a subtypeInstance
so logically you can't go back from a super-type instance to a sub-type instance - at least not with any guarantee of safety.
The only way around this is to remember the sub-type for when you need to do sub-type operations. If your language supports them, covariant return types can be used to improve interface definitions so that the sub-type isn't lost unnecessarily:
In ITree:
public function getParent():ITree
In TreeNode:
public function getParent():TreeNode
This way when you call getParent() on a TreeNode, you get a TreeNode, not an ITree. Of course if you call getParent() on exactly the same instance declared as an ITree, you get back an ITree. Once you've forgotten type information, it's too late.
Inside a class you can choose to deal with the real type.You can still talk to the outside world in interface (deliberately losing the super-type information) but the implementation can deal with real types instead of interfaces.
I was experimenting with some code in Java. If you're interested, here it is:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
public class TestTree {
@Test
public void testGetDepth() {
ITreeNode bigTree = TreeNode.create(null, 10);
ITreeNode left = bigTree.getChildren().get(0);
Assert.assertEquals(10, bigTree.getHeight());
Assert.assertEquals(9, left.getHeight());
Assert.assertEquals(0, bigTree.getDepth());
Assert.assertEquals(1, left.getDepth());
}
}
interface ITree {
List<? extends ITree> getChildren();
ITree getParent();
}
interface ITreeNode extends ITree {
@Override
List<? extends ITreeNode> getChildren();
int getDepth();
int getHeight();
}
class TreeNode implements ITreeNode {
private ITreeNode m_parent;
private final TreeNode m_left;
private final TreeNode m_right;
private final List<ITreeNode> m_children;
public static ITreeNode create(ITreeNode parent, int depth) {
TreeNode node = createDescendants(depth);
node.setParents(parent);
return node;
}
private static TreeNode createDescendants(int depth) {
if (depth == 0) {
return new TreeNode(null, null, null);
}
else {
return new TreeNode(null, TreeNode.createDescendants(depth - 1), TreeNode.createDescendants(depth - 1));
}
}
private TreeNode(ITreeNode parent, TreeNode left, TreeNode right) {
m_parent = parent;
m_left = left;
m_right = right;
List<ITreeNode> children = new ArrayList<ITreeNode>();
children.add(left);
children.add(right);
m_children = Collections.unmodifiableList(children);
}
private void setParents(ITreeNode parent)
{
m_parent = parent;
if (m_left != null)
(m_left).setParents(this);
if (m_right != null)
m_right.setParents(this);
}
public List<? extends ITreeNode> getChildren() {
return m_children;
}
public ITreeNode getParent() {
return m_parent;
}
public int getDepth() {
int depth = 0;
if (m_parent != null) {
depth = m_parent.getDepth() + 1;
}
return depth;
}
public int getHeight() {
int leftHeight = (m_left == null) ? 0 : m_left.getHeight() + 1;
int rightHeight = (m_right == null) ? 0 : m_right.getHeight() + 1;
return Math.max(leftHeight, rightHeight);
}
}