Design Thinking
It feels as if my objects aren’t helping enough. It’s important to recognize that feeling and see what can be done about it. Think, speculate, sleep on it, try something.1
Sometimes when things are going well, the code we need for some new capability is easy to write, mostly concerns itself with just one object, and is easy to understand without digging down in the call tree. Other times … not so much. One of the advantages to aggressively refactoring toward simple clear code is that we come to be better able to recognize when we’re over in the “other times” realm.
Unfortunately, some developers have little chance to develop this sense that things could be better, for various reasons, in my view mostly due to being under too much pressure to code code code. I believe that even a little work toward better code will allow a developer to build up that sense, but as it is the practice that builds the sense, it takes longer if they’re unable to do much of that kind of thing.
Fortunately for me, I’ve had ages to work with shaping code, and while I’m not as good at it as I’d like to be, in fact far from the capability of some people I know, I’ve got a fairly decent sense of when things could be—and therefore should be—better. And I have that feeling now.
Let’s see if I can express a bit about what feels like not enough help.
Searches
It feels to me as if I too often have to code up a little loop to find out something that I’d like to know. Here’s one example:
class Room:
def _build(self, bank, length, start_cell: Cell):
self.origin = start_cell
new_cell = start_cell
for _ in range(length):
bank.take(new_cell)
self.cells.append(new_cell)
new_cell = self.find_adjacent_cell(bank)
if new_cell is None:
return
def find_adjacent_cell(self, bank: CellBank):
for cell in sample(self.cells, len(self.cells)):
neighbors = cell.neighbors
for neighbor in sample(neighbors, len(neighbors)):
if neighbor in bank:
return neighbor
# cell is not on periphery?
return None
I’m not sure quite what I’d like either of those to be, but they feel messy to me. For the second I have at least part of an idea:
The method wants to find a cell that is currently unused (in bank) and a neighbor of one of the room’s current cells. It seems to me … at least tentatively … that we should be able to ask a cell whether it has a neighbor in the bank. Additionally, there are cells in the room that cannot possibly have a neighbor in the bank: all the internal cells, which as the room grows, becomes most of them. So we’re searching all the cells, randomly, to find a peripheral cell such that there is an available cell adjacent to it.
Does the code say that to you? It doesn’t say that to me. What might be better? Well, let’s fantasize a bit.
- Idea
- What if the room knew which of its cells were peripheral, where peripheral means on the outside of the room? And what if each cell knew its neighbors directly, and cells knew if they were in a room or in the bank (which is arguably just a very large disconnected room)? Maybe we could say something like this …
No, I can’t write any code that feels right with this idea in mind, but I do like the notion.
- Idea
- OK what if the room knew, in addition to the cells it contains, all the open cells surrounding it? We could grow that knowledge incrementally. We start the room with one cell, so when we add that cell to the room, we check its neighbors right then to see which ones are in the bank, and we add those to our surroundings. Every time we add a new cell, we “just” pick a random cell from the open cell collection and add it. Then we check its neighbors and if they are open, add them to our open collection.
- Issue?
-
What about other cells pointing to the same one we added? I think there’s no problem, because now they still point to the same cell, it’s just that it’s inside now.
Reflection
This seems like a pretty good idea, and in fact we could implement it now. Maybe we will, but first I want to look back at the notion of cells knowing where they are. Presently there is a CellBank, which is not part of the Dungeon, and probably should be (we have no use for changing the cell bank or having more than one at a time). And the Dungeon has a collection of room, Room instances, and a collection of paths, which is a list of lists of cells, I think.
When we’re creating a path, we ask each cell whether we can add it to the path. We can add it if it is in the cell bank or in one of the rooms. We find that out with this code:
def can_use(self, neighbor) -> bool:
if neighbor in self.bank:
return True
for room in self.room_list:
if neighbor in room:
return True
return False
So we invariably look in one dictionary (the bank) and one list (the first room) and sometimes in the second. We could improve that somewhat if the room’s cells were not in a list but in a set.
However … it would be better, if it were possible, to ask the cell neighbor directly whether it is in the bank or in room 1 or room 2. This suggests that a cell should know what object owns it.
However^2, collections whose members know their collection raise my hackles a bit. It can get very fiddly to maintain the right connection when things move around. I don’t know a rule about this: I’ve just come to be nervous about it.
However^3, we have essentially the same problem now, since a cell that was in more than one room or in a room and the bank could be a problem. (Cells can be in a room and in a path, or available and in a path. But paths are weird and not finished by a wide margin, so I’m not so worried about that.)
So therefore, it starts to appeal to me to give the Cell a bit more knowledge, such has, but perhaps not limited to knowing what object owns it. It will know its owner.
However^4, we presently create Cells out of the air and they are only checked against collections that hold them, never for any information in themselves other than their x and y, which is all they know. If we give them knowledge, then when we create a new c1=Cell(3,5), and then create a cell with the same coordinates c2=Cell(3,5), those cells need to be identical, because if we tell c1 that it’s in room_a, we want c2 to be in room_a as well.
We could avoid that concern by never creating Cells in the open (or only for detailed testing), only accessing cells initially from the CellBank, and later from whatever collections we put them in. Then the only creation of cells could be in the initialization of CellBank.
Where are we?
I’m envisioning an expanded Cell object, something like this:
class Cell:
def __init__(self, bank, x, y):
self._bank = bank
self._owner = self._bank
self.x = x
self.y = y
self._n = None
self._e = None
self._s = None
self._w = None
@property
def owner(self):
return self._owner
@owner.setter
def owner(self, new_owner):
self._owner.forget(self) # ??
self._owner = new_owner
@property
def n(self):
if self._n is None:
self._n = self._bank.at(self.x, self.y + 1)
return self._n
@property
def e(self):
if self._e is None:
self._e = self._bank.at(self.x + 1, self.y)
return self._e
@property
def s(self):
if self._s is None:
self._s = self._bank.at(self.x, self.y - 1)
return self._s
@property
def w(self):
if self._w is None:
self._w = self._bank.at(self.x - 1, self.y)
return self._w
@property
def neighbors(self):
return [self.n, self.e, self.s, self.w]
def open_neighbors(self):
return [n for n in self.neighbors if n.owner == self._bank]
This is just a sketch, though I was careful with it. I was thinking it might be nice to have properties n (for north) and so on. In any case, the cell needs to know its four neighbors by the nature of the idea. (I’m not sure what happens at the edges. Maybe a MissingCell object. Maybe None would be OK.)
Building the structure would be tricky. In the sketch above it’s done on the fly, as you ask for neighbors the first time, they are looked up in the bank. (After that, the bank is basically useless, I think.) I’m just guessing that when we change the owner of a cell, we should inform the former owner to forget it. However, it is possible, probable even, that we never change the owner more than once. We grow rooms, removing cells from the bank and telling them they are in the room and storing them in the room’s internal structures. They never move again. So that could lead to a simplification if we can just build a big net of cells and work with it.
- Graph
- People who write about games and game operations often speak of a graph instead of an array of cells. If we do wire all our cells together north east south west, the cells are nodes in a large rectangular graph. Unless, of course, we made it round or any old shape.
- Danger, Will Robinson
- That’s getting a bit too speculative for my current understanding, but it is interesting. What if there were no coordinates in the code, just some number (variable number?) of connections from a given cell to its adjacent cells.
-
What if: a Cell in Graph A could contain another graph B? Graph A might be thought of as connecting rooms together, and the individual rooms would be sub-graphs, internal to the Graph A nodes, not really visible to Graph A at all, which would just represent travel between the contained rooms. I’m not sure how we would draw such a thing, but it’s … interesting.
Time for a break, a bit of thinking, and then maybe an experiment.
Reflection
A couple of ideas that I’d like to muddle around with:
-
The notion of a growing periphery in the Room, which is perhaps simple enough that we could do it now, gaining experience and perhaps speeding things up.
-
Replace or expand the notion of Cell to an object that knows the object that contains it, and knows its neighbor Cells directly. Perhaps give it an arbitrary number of neighbors that just happens to be four right now. (Hex cells, popular in gaming, have six neighbors. That might just drop out.)
-
Consider shifting from focus on Cells to graphs containing connected nodes, and perhaps even to nodes containing new graphs.
Those are roughly in order of my confidence that they are sensible, with #3 a very much lower level than #1 and #2.
I think my next step will be to draw some pictures, which I’ll share next time.
Summary
From time to time, when the code seems not to be helping, or the future looks fuzzy, there can be value to sitting back and thinking about alternative ideas, large or small. We don’t want to stay stuck in speculation for a long time, but just as it is good for our eyes to look away from the monitor for a while, it is good for our brain. And good things in our brain tend to result in better things in our code.
The pattern is something like this:
- Notice that things feel sticky
- Make time to think about it
- Think about what feels sticky
- Think about what would be better if only we could
- Think about how we could
- Let thoughts ramble
- Rest, reflect
- Experiment if some of the ideas seem decent after reflection
We’ll see what happens. See you then!
-
I think it was Kent Beck who first used the notion of the objects helping within my hearing. Just one of the many ideas I’ve gained from him. Any inadequacies in my use of his ideas or anyone else’s are mine, much as I’d rather blame someone else. ↩