Sunday 30 March 2008

Containment Versus Inheritance

In the stdtools package I recently released, there is a class called ResourceLoader. It's purpose is to provide a dictionary-like object that loads resources a game might need, like sound and images. The point is that the images are not loaded until they are accessed. A lazy loading mechanism, one might say. I wanted the object to behave like a dictionary, and make it generic enough so it could be easily adapted for a partical resource type.

The ResourceLoader class does exactly that. It overrides the __getitem__ and __delitem__ hooks to behave like a dictionary, and you can adapt it for a particular resource type by subclassing it, and overriding the load and locate methods. This is all very nice. However, there is a design issue in the class: It uses the dictionary through containment.

What I mean by that, is that the class has an attribute, self.resources, that is a dictionary of all the resources I have currently loaded. If the __getitem__ method is called, the class first tries to retrieve the requested item from the dictionary, and if it is not available, attempts to load it:
class ResourceLoader:
def __init__(self, paths):
self.paths = paths
self.resources = {}

def __getitem__(self, key):
if key not in self.resources:
self.resources[key] = self.locate(key)
return self.resources[key]
So why is this wrong? Remember that this class essentially is a dictionary, with some altered access mechanics. Therefore, it makes much more sense if the class is also derived from a dictionary, instead of containing one. This is the difference between an is-a and a has-a relationship. It makes no sense for this object to have a dictionary attribute if all access of the object is passed onto that dictionary anyway.

There are also concrete benefits to using inheritance in this case. Most importantly, we get all the nice things a dictionary can do, like iterating over it, retrieving it's length, etc. Another bonus is that it is a bit faster, since we don't have to look up self.resources each time. Another benefit we get in this case is that we can forget about the __getitem__ hook, and simply implement the __missing__ method, which is called if a key is missing.

It is not always so clear when to use inheritance, and when to use containment. Think about how your class should behave. If it's behavior is mostly like a dictionary or other type, and the class would benefit from having the methods that type has, inherit it. If the class uses the dictionary more as an implementation detail, and it makes no sense to call something like has_key on your object, it is better to use containment.

I have rewritten the ResourceLoader class as described here, and I'm going to push a new version of it out sometime in the future.

No comments: