So I know there are some Ruby-ites who have read this blog in the past, maybe you could give me some advice? Sorry this entry is so long, man I write too much.
I’m having trouble switching my brain from understanding good class hieararchy design in a statically typed language (like Java), to Ruby’s world of ‘duck typing’ which I’m still not entirely comfortable with.
I need to work with ‘holdings’ statements in my ILS, or really as expressed by my OPAC, HIP3. There are basically three classes involved:
#1: ‘Copies’: A bad name, but the one HIP uses. For bibs under serial control, there is a ‘copy’ level which basically (over-simplified) aggregates items in a particular run. For instance, a copy might represent v.-v.20 of a particular title in a particular library location, all on the shelf together.
#2: Serial Items: Underneath a copy, there is a serial item, representing a particular object on the shelf (could be a bound volume or a loose volume or even some other media).
#3: Monographic Items: Non-serial-controlled items don’t have the copy level, there are just plain Items.
All of these three classes can be abstracted as what I’ll call a ‘holding’. They all have some attributes and perhaps functions in common. For instance, they all have a call #, a location code, and a collection code. They also of course have some unique things (like the parent-child relationship between Copy and Serial Item).
There isn’t really any super/sub-class relationship between these three classes. You can’t say any one of them is any of the others–you can just say that all three are Hip3Holdings.
They also don’t neccesarily need to share any code. The way they perform their common API might be entirely different.
In Java, I might define an Interface “Hip3Holding”. Java interfaces define API that implementors must implement, but provide no code (implementation) of their own. So in this case all three classes would implement Hip3Holding, but there wouldn’t be any other class hiearchy between them. This leaves any of them free to have any super-class they might want at another time. Client code can still ask if an object is an “instanceof Hip3Holding” and all three will say ‘yes’. Which means they commit to providing the API defined there, you can treat them all like an abstract Hip3Holding.
Alternately, if they did need to share some code in common, I might make Hip3Holding a class, not just an interface, that all three sub-classed from. Hip3Holding would probably be an abstract class, you can’t instantiate any Hip3Holdings, you need to instantiate the specific concrete sub-classes. Now they can inherit common code, and of course they are all still “instanceof Hip3Holding”.
We could make this more complicated. All three are abstractly Hip3Holdings, but #2 and #3 are also abstractly Hip3ItemHoldings, which might be a bit more specific but still common to both of them.
In Java you could handle still this with either interfaces or abstract classes. Interfaces can extend each other, so we could have another interface Hip3ItemHolding extending Hip3Holding which #2 and #3 implemented. Or we could have an intervening abstract class Hip3ItemHolding which they subclassed. Then we could ask ‘instanceof Hip3ItemHolding’, and have an object tell us that it’s committed to providing that more specific API too.
All of these designs are possible, and they all to me are potentially good designs, depending on the specifics of the situation.
But in ruby ‘duck typing’, my instincts for good design fail me. We don’t have interfaces, although we do have modules. I don’t think we have abstract classes? (But I could be wrong). My instincts are built around static typing in the first place, which we don’t have in Ruby either. So what can we do in Ruby?
We could do nothing, just provide all three classes with no relation between them at all. They all happen to implement common methods, which a client can just call—but there’s no way to ask “kind_of? Hip3Holding” in general. There’s no real tracking of what API is common to Hip3Holding, which still seems dangerous to me–if we add a method to our abstract idea of Hip3Holding, we’ve got to remember to add it to all these specific classes (which aren’t marked as being Hip3Holdings even), or else we’ll get an error–but not until run-time at an unpredictable point! So this is something we couldn’t even do this way in a statically typed language–but it still doesn’t seem like a good idea to me!
Okay, so we can provide a Hip3Holding Module instead, and include it in all three classes. If they don’t need to share any code in common, it might be an empty module. Now we can at least ask “kind_of? Hip3Holding” , we’ve at least marked which classes are. There’s still no tracking or self-documentation of what API is actually supposed to be common to Hip3Holding. But it’s good that we can’t actually instantiate a ‘Hip3Holding’–since it’s an abstract idea.
If there is some common code, we could put it in the module. But there will definitely be some common API that does not have common code. For instance, each class might implement requestForUser() in an entirely different way, but all three need to provide it. So we could leave this method out of the module entirely, and now it’s just up to the programmer to remember that it’s part of our notion of Hip3Holding and maintain that. Or we could put it in the module but have the implementation just raise an exception? Or be silently no-op?
Or instead of a module, we could provide an actual super class Hip3Holding. Which seems weird to me, because I don’t think there’s any way to make it abstract, but client code should really never instantiate a Hip3Holding directly, it really is an abstract concept. Not sure what the pros and cons of this vs a module are, my instincts fail me.
And when we get to the idea of a notion of an Hip3ItemHolding that’s more specific than Hip3Holding but still abstract—I have no idea. Ruby modules can’t sub-class or extend each other, I don’t think. So we could have a Hip3ItemHolding module that’s entirely unrelated (except in our minds) to Hip3Holding? We could leave it out entirely and just have the whole concept be just in our heads (and in certain API that Serial Item and Mono Item just ‘happen’ to implement in common (duck typing?)). If we go the route of having Hip3Holding be an actual class (which is meant to be abstract, but we can’t enforce that), we could instead still have Hip3ItemHolding be an actual class sub-classing that (also meant to be abstract without us enforcing it).
Help! What’s the way to model this? If anyone reaches the end.