Python 68 - Design Issues
Thinking about some design issues, some good, some maybe not so good. Seven footnotes, none useful.1
Today’s core topics are:
- Wrapping Collections
- Inheritance of Concrete Behavior
Related ideas will come to mind.
Wrapping Collections
When I refer to wrapping collections, I mean the practice where, instead of keeping all your Widgets in a list or array, you build a class Widgets that contains a list or array of Widgets. Then you expose whatever behavior you need, and generally do not expose full list or array behavior.
Among my gang, at least, there is a plurality who think that it’s “generally” a good idea to wrap native collections with named, meaningful collection classes. There is a minority who are a bit more blasé2 about the idea, perhaps wrapping occasionally or only rarely.
I myself am about where I often find myself. I think it is almost always a good idea, and yet quite often I do not wrap collections. I also think it is a good idea to test-drive everything, and to go in tiny steps, committing after each one is shown to work. Yet, often I do not do those practices. And often I get in trouble that the practices would probably have avoided or lessened. And am I schooled? Do I learn my lesson? I do not. Or, I learn it and still don’t do the thing as often as I should.
When it comes to wrapping collections, I think there are two main reasons that people would give for doing it.
- Safety
-
The wrapper class limits what can be done to the mess of Widgets. Users can’t just iterate will’e-nill’e3, add and remove things at will, or generally mess around with the Widgets. They can only do what we allow them to do.
-
People citing safety as a reason for wrapping will claim that the practice reduces the chance of class users making mistakes. And they’re surely right, although we have to note that they’re not very trusting, are they? Well, if you had friends like some of mine, you might not be very trusting either. Seriously, though, it isn’t about trust, it’s about the fact that we all make mistakes, and this practice can protect us against some mistakes.
- Coupling and Cohesion
-
Once we commit to putting a Widgets wrapper around all the widgets, we have a new place to bring together all the widget-related behaviors, increasing cohesion and reducing coupling, which are good things.
-
I can think of nothing snarky to say about this viewpoint, even though it would increase the symmetry of these two little sections. The practice of wrapping collections almost invariably increases cohesion, reducing coupling concomitantly4. Let’s look at a recent example. Like from yesterday.
Safe to Emerge
Recall that in our little Asteroids game, the individual flyers, asteroids, missiles, saucers, ships, are collected under Fleet objects, and all the Fleet objects are collection in an object called Fleets.
In the game, after a ship is destroyed, the game is supposed to wait until the screen is safe before starting a new ship. The idea is, no missiles flying, no saucer, and no asteroids too close to the center. That feature was implemented long ago, before there were Fleet and Fleets, and it was implemented in Ship. Converted to use the Fleets, it looked like this:
class Ship:
def safe_to_emerge(self, fleets):
if len(fleets.missiles) > 0:
return False
if len(fleets.saucer_missiles) > 0:
return False
return self.asteroids_far_enough_away(fleets.asteroids)
def asteroids_far_enough_away(self, asteroids):
for asteroid in asteroids:
if asteroid.position.distance_to(u.CENTER) < u.SAFE_EMERGENCE_DISTANCE:
return False
return True
Prior to the Fleet/Fleets thing, these methods were passed the collections they needed to do the job, but they still had the same issues as we see here.
If we look at these two methods for a moment, we see that they suffer from “feature envy”: they have no real references to self
and are concerning themselves entirely with getting information about other collections that, in principle, they shouldn’t know about.
Now before Fleet and Fleets class existed, either these methods had to ask Game for the collections they wanted, or the collections all had to be passed in. If I recall correctly, these methods weren’t even given all the right collections, which may further support my point above about coupling and cohesion.
Be that as it may, yesterday, I noticed the feature envy, and changed the Ship to do this:
if fleets.safe_to_emerge():
And that was supported in Fleets:
def safe_to_emerge(self):
if len(self.missiles) > 0:
return False
if len(self.saucer_missiles) > 0:
return False
return self.all_asteroids_are_away_from_center()
def all_asteroids_are_away_from_center(self):
for asteroid in self.asteroids:
if asteroid.position.distance_to(u.CENTER) < u.SAFE_EMERGENCE_DISTANCE:
return False
return True
This is better. The Fleets object has all the fleets and can access them legitimately as the container for all of them. Furthermore, in the context of fleets, we have a better chance of noticing that while we check for missiles and saucer missiles being on screen, we do not check for the saucer being present. We may or may not want to do the check, but we have a better chance of noticing the issue, because our thoughts, in Fleets, are about all the collections, not about the Ship.
So this is a near-perfect example of “if you build it, they will come”, and more to our point here, a near-perfect example of a wrapped collection providing an opportunity to increase cohesion (moving the idea in) and reducing coupling (eliminating Ship’s detailed knowledge of how Fleets and Fleet objects work).
I find that this almost always happens when I wrap collections, which is why I resolve, once again, to be more consistent about doing it.
YMMV, but I think wrapping native collections almost invariably pays off by providing a place for things to be done and for better ideas to arise.
What about the safety angle? Well, here I’ve not been so careful. Look at what I have in the Fleet class:
class Fleet:
def __init__(self, flyers):
self.flyers = flyers
def __iter__(self):
return self.flyers.copy().__iter__()
def __len__(self):
return len(self.flyers)
def __getitem__(self, item):
return self.flyers[item]
...
By providing those methods, I’ve made a Fleet behave almost entirely like a native collection. You can iterate it, get its length, and even subscript into it. And those methods are there because I do those things. In fact, I do len
in the code we saw above in Fleets, and that code iterates a Fleet (asteroids) as well.
I am sure that the _getitem__
, which provides for subscripting, is in there because it’s needed as well, probably by a test, but I wouldn’t swear that there’s no code that uses it in production.
So I am not as concerned about safety as some people, at least not in this application. I might be more concerned in a larger team working on a larger program, not because I don’t trust them, but because I know that most of them would be human and thus prone5 to human error.
Inheritance of Concrete Behavior
If wrapping collections is commonly thought to be good, inheriting concrete behavior is commonly thought to be bad. I did an entire series of articles in one of my Kotlin versions of Asteroids, responding to GeePaw Hill’s concern over inheritance of concrete behavior. He’s agin6 it.
An interface, in languages that have it, is a definition of methods (and properties) that objects are supposed to implement in order to, well, implement that interface. For example, to implement the Asteroids “Tickable” interface, if we had one, and if you’re an asteroid class, you should implement tick
, because you’re going to be in a fleet, and your fleet is going to call tick
on you, because that’s what Fleet objects to and that’s what Flyer objects had better be ready to do.
And for that matter, Fleet objects have to implement tick
as well, because the Fleets object is going to send tick
to them, so Fleets should also implement Tickable
, if we had such a thing.
These days, it is commonly held that classes should inherit from interfaces, abstract definitions of what they must do, and then they should each implement all the requirements of that interface. And it is commonly held that classes should never inherit from other concrete classes, only from interfaces or abstract classes, which are basically empty classes with no concrete behavior, mostly used as interface definitions in languages without an explicit notion of interface.
Why is it not good, in people’s minds, to inherit concrete behavior? Well, it’s surely a bit more complicated. Let’s take an example in Asteroids where I do just that thing. Asteroids has a generic Fleet class, including this tick method:
class Fleet:
def tick(self, delta_time, fleets):
result = True
for flyer in self:
result = flyer.tick(delta_time, self, fleets) and result
return result
Basically, tick
in a Fleet just ticks all the members of the fleet. The rigmarole with the result allows it to return True if all the members return True. (I think we make no use of this and that we’ll remove it.)
I began with just the Fleet object, and it seemed to make things better. Then I thought of making specialize subclasses of Fleet, to handle specific capabilities like noticing that we have no ships on the screen.
So I have:
class ShipFleet(Fleet):
def tick(self, delta_time, fleets):
ships = fleets.ships
if len(ships) == 0:
self.ship_timer.tick(delta_time, fleets)
super().tick(delta_time, fleets)
return True
So my ShipFleet overrides Fleet’s tick
method … and then calls it after it has done its own thing.
Is this bad? Well, it has drawbacks, even in this small case. If I build a subclass of Fleet, my purpose will be to override tick
because that’s basically all that Fleet does, and if I forget to call super().tick
, whatever objects are in that Fleet will not get a chance to tick, and that will probably be bad.
On the other hand, if I inherit just an abstract tick
and implement my tick without remembering to tick all my children, exactly the same bad thing will happen. Either way, I have to remember. In the code here, I have to remember one thing: super.tick(...)
. In the more “correct” don’t inherit concrete behavior that would be bad version, I have to remember
result = True
for flyer in self:
result = flyer.tick(delta_time, self, fleets) and result
return result
Honestly, I think I have a better shot at getting it right by calling super()
.
There are other, arguably stronger objections to inheritance of concrete behavior, such as the diamond problem where, for some almost certainly contrived reason you find yourself inheriting from two classes each of which inherits from some fourth class and you just don’t know which class will even provide the thing you want to inherit. Yeah, well, this is a bit like objecting to swimming because if you wear a concrete life vest you’ll drown. How about just not doing multiple inheritance? Doesn’t it seem like that’s the real problem here, bunkie?
But seriously …
I don’t mean to completely minimize the objections to inheritance of concrete behavior. Personally, I think that one of the purposes of inheritance is to serve as a programmer’s hack to avoid duplication, and used that way, it avoids duplication, with a side order of a bit of complexity.
The other purpose of inheritance, inheritance of an interface, is there to help the compiler know, and to help the human know, what has to be done to build one of these Fleet things or WhizBangWidget things.
In Kotlin, my most recent foray into a language with the interface notion, if I were to declare an interface IFleet
and specify that tick
was a method of that interface, and if later I were to create ShipFleet
and forget to implement tick
, the compiler (and IDE) would tell me. I couldn’t forget to do it. (I could still do it incorrectly, of course.)
In naïve7 Python, you just build your subclass and when someone calls tick
on it, if you forgot, the program will break. There are some “dunder” methods in Python that can be used to check for this situation. You do it programmatically and it’s pretty weird. Here’s a Real Python article that will start you down the path. Basically you’re using reflection to analyze what the objects implement and where in the hierarchy it’s found.
What do you advise, Ron?
Honestly, I have no general advice. In this simple case of ShipFleet inheriting from Fleet we seem to see that it’s not ideal either way in a language like Python. There’s no useful way to force you to implement the tick
method, and no useful way to force you to call super().tick()
. You pays your money and you takes your choice. I chose to use super
, mostly just because I started with a concrete Fleet and extended my thinking to specialized subclasses of it.
An idea …
The word “specialized” triggered an idea. What if we implemented Fleet’s tick like this:
def tick(self, delta_time, fleets):
result = self.before_tick(delta_time, fleets)
for flyer in self:
result = flyer.tick(delta_time, self, fleets) and result
return self.after_tick(delta_time, fleets) and result
And then we could implement GenericFleet with empty before and after methods, and when we come to do ShipFleet, it inherits from Fleet, not GenericFleet, and we give it those two methods, both of which it must implement, even if empty?
I think that might give me a better world in this case. If I ever do implement a Fleet subclass and forget one of the before/after methods, it will error immediately upon use. And I don’t override any concrete behavior, though I do inherit it.
Interesting. I might just refactor to do that. But not this morning. That might be better … in this case.
Summary
We’ve briefly explored two design notions, the first that it is generally a good idea to wrap native collections with meaningful specialized collections, the second the notion that concrete inheritance is, well, not as good as it might be.
- Wrapping
- Here, I come down firmly on the pro-wrapping side. My experience is that it nearly always pays off by providing a new place to stand and to build useful behavior. I am not so concerned about the safety issue: it’s the provision of a new notion that makes the idea work for me.
- Concrete Inheritance
- I remain willing to use concrete inheritance where it seems to make sense to me. I would be less inclined to use it in a large program, or in a language with compile-time type checking. And the idea above is interesting enough that I’ll try it, but it does not bear directly on the question of inheritance of concrete code It does address a common case, the overriding of concrete code and then including a
super()
call. -
I recall that in Smalltalk there was a very complex class hierarchy, and that there were very commonly calls to
super
. I do not, however, recall enough of the details to be able to draw any lessons. That was a quarter of a century ago after all.
Summary Summary
Probably my bottom line is something like this:
-
I generally prefer to wrap native collections with my own specific collection types.
-
I feel that I should lean away from inheriting concrete behavior, but I have often found it to be useful. Perhaps there was a better way that I didn’t see at the time.
See you next time!
-
Sometimes you just have to entertain yourself. ↩
-
Obligatory complaint about my occasional use of French phrases as if I were some kind of debonair savoir-faire cosmopolitan bon-vivant. ↩
-
Willy-nilly. I just use the ancient form because it was popular in my youth. Or to irritate people. Why not both? ↩
-
This one’s for Gitte. ↩
-
“Prone” means face down. Why are we “face down” to human error? The more I think about this, the more I wish I were not thinking about it. ↩
-
Against, archaic, like YT. ↩
-
There he goes again avec le français. ↩