Kotlin 167: Finalize finalized
Finalize existed because an object could be destroyed by another object. The unwritten rule now is that objects do not destroy other objects. Can we get rid of finalize?
It’s 0530. The above could be a bad idea.
I should add that nothing prevents one object from destroying another. We should at least look into that, and, ideally, arrange that it can’t be done, if we want the rule.
And, after a bit more thought, I’m questioning the whole idea. Maybe finalize
is an elegant solution to the limited knowledge our objects have of each other. Let’s see what we’re doing with finalize
.
IDEA is happy to give me a list of the 13 implementors of finalize
.
Asteroids finalize by adding their splits if they are capable of being split. I think that adding things was the primary purpose for finalize
, which returns a list of objects to be added to the mix.
override fun finalize(): List<ISpaceObject> {
val objectsToAdd: MutableList<ISpaceObject> = mutableListOf()
if (splitCount >= 1) {
objectsToAdd.add(asSplit(this))
objectsToAdd.add(asSplit(this))
}
return objectsToAdd
}
Saucer sends itself a wake up call so that it’ll start moving the other direction.
override fun finalize(): List<ISpaceObject> {
wakeUp()
return emptyList()
}
Ship does a lot of work and adds nothing. This is a kind of set up for next time through, like Saucer’s wakeUp
.
override fun finalize(): List<ISpaceObject> {
if ( deathDueToCollision() ) {
position = U.CENTER_OF_UNIVERSE
velocity = Velocity.ZERO
heading = 0.0
} else {
position = U.randomPoint()
}
controls.recentHyperspace = false
return emptyList()
}
Missile, SaucerMaker, Score, ScoreKeeper, ShipChecker, ShipDestroyer, ShipMaker, Splat, WaveChecker, and WaveMaker all do nothing, returning emptyList()
.
I think we’d best double check to be sure no object is destroyed other than by its own hand.
There are 21 senders of Transaction.remove
. Every single one of them is remove(this)
, an object removing itself.
- Push Down
- If the only use of
Transaction.remove
is to remove the object that has been handed theTransaction
, we might want to provide some kind of safety mechanism if we’re going to make it a rule that objects can only be removed by themselves. It’s easy to imaging a situation where we do want to remove another object: what if we were to revise collisions to consider both partners together, instead of sending each of them a symmetric message? But … if we were to do that, we’d probably need to revamp a number of things, and we’d surely find what objects were doing prior to their unfortunate but necessary demise. -
We’ll not worry about contingencies and will consider changing
Transaction
so that it cannot be used to remove anyone but its current holder. We do pass transactions around sometimes, such as when we pass over a collection accumulating changes, so that would require a bit of care. -
Not a concern for now. Pop Up.
We can remove the requirement for finalize
by removing it from the ISpaceObject
interface, and by removing the call to it that presently sits in Transaction
, making sure that every lost object gets that final chance.
Alternately, we could put finalize
into the Subscriptions
and remove the requirement that everyone implement it, yet leave it in place for those who would like to have it.
I think that’s best. Changes the rule from “you must implement finalize” to “you may subscribe to finalize”, and preserves safety in case an object does get removed by another object.
I’m glad we had this little chat.
Add a default finalize definition to Subscriptions:
class Subscriptions(
...
val finalize: () -> List<ISpaceObject> = { emptyList() }
Change Transaction to use Subscriptions. This:
fun removeAndFinalizeAll(moribund: Set<ISpaceObject>): Boolean{
moribund.forEach { spaceObjects += it.finalize() }
return spaceObjects.removeAll(moribund)
}
Becomes this:
fun removeAndFinalizeAll(moribund: Set<ISpaceObject>): Boolean{
moribund.forEach { spaceObjects += it.subscriptions.finalize() }
return spaceObjects.removeAll(moribund)
}
Remove finalize
from ISpaceObject
, after opening a list of implementors to edit. I use the Find window and the convenient option command arrow to tick through and remove all the ones that just return the empty list. That leaves the three that use the facility, Asteroid, Ship, and Saucer. These need to subscribe to finalize
, and to make it no longer an override. We can make it private and call it in the subscribe.
class Asteroid
override val subscriptions = Subscriptions(
interactWithMissile = { missile, trans -> dieIfColliding(missile, trans) },
interactWithShip = { ship, trans -> if (Collision(ship).hit(this)) trans.remove(this) },
interactWithSaucer = { saucer, trans -> if (Collision(saucer).hit(this)) trans.remove(this) },
draw = this::draw,
finalize = this::finalize
)
All three will be done like that.
I notice from hints in IDEA that there are tests that do finalize. We’ll deal with those as they arise.
I find in compiling that I was mistaken: Missile does need finalize
. No harm done, I update its subscriptions. However … without the tests, I might not have noticed that and very likely would have introduced a defect.
I can now replace all the override
with private
. Tests cannot compile because many of them send finalize
. I’ll run them through subscriptions
, like changing this:
fun `asteroid splits on finalize`() {
val full = Asteroid(
position = Point.ZERO,
velocity = Velocity.ZERO
)
val radius = full.killRadius
val halfSize= full.finalize()
...
To this:
val halfSize= full.subscriptions.finalize()
There are at least nine of these. Those fixed, the tests are green. Nice. Commit: finalize
is now a subscription. function removed from all but four objects.
Let’s sum up, since it’s still only 0630ish.
Summary
We’ve removed an element from our defining interface, ISpaceObject
, bringing it down to requiring just one function, update
. We could, and perhaps should do the same work on that one. In essence what we are doing is specifying “events” that occur during an object’s life cycle and allowing objects to sign up for the ones they care about. This makes each object simpler, in that it contains only lines of code it actually needs, but does make thinking about things a bit difficult, because instead of starting out, doing something, and then stopping, objects spread their activity out over several events, setting up assumptions in one event, verifying them in another, acting on the result in a third.
It’s certainly possible to imagine a simpler setup. It would require some changes that I’m reluctant to make, such as providing some well-known objects and facts, such as the ship, the asteroid count. One thing leads to another and next thing you know you have a god object and elebenty-seben global variables. And that’s not necessarily a bad way: it’s commonly used, and I’ve used it myself in the past and probably will again.
I fell into this scheme by accident, by taking a simple focus on the individual objects, and it has survived this far with the help of my friends. My friends don’t like it but if my friends don’t dance they’re no friends of mine. Well, they are my friends, actually. But as Hill puts it “the code works for you; you don’t work for the code”. This code is the way I want it, because it’s working nicely enough and I want to see how it turns out.
Today’s change is a net reduction in lines of code and reduces complexity in that regard, while adding some increment of complexity due to the addition of another event. But we have 14 of them now, so adding one isn’t a big deal.
That’s my story, and I’m sticking to it. Let’s publish this and see what I do next?