Last night, some topics came up that I’d like to consider a bit this very early morning.
At last night’s Tuesday meeting of the Friday Night Geek’s Night Out Zoom Ensemble, we spoke at length about programming, our love for it, and the many aspects that go into a developer learning the things we consider to be central to how we do our work. Our starting context was some new developers that Hill is working with. He was reviewing the Python section of the company’s course work, and observed that in it, TDD was a module. He felt, and we all felt, that TDD should be the context within which new developers should learn, not just a module.
This morning, I want to touch on a few topics, including these:
- I wanted to say “Learning” in the header, but “Teaching” fit the pattern.
Honestly, I don’t believe I could “teach” anyone much about programming. What I would see myself doing would mostly be providing learning situations where the developers had many small experiences, aided by reflection, and probably by a bit of explaining or description by the “instructor” and the written material, describing what was happening and what the “instructor” experienced while doing the same ind of thing.
We’d want to create many tiny learning experiences. Some of them, we’d know exactly what we intended, but mostly the experiences would be so small and come so fast that there’s no way we could plan each one.
The idea is that programming is about making tens, probably hundreds of decisions per hour, and that learning good programming is about learning to make most of those decisions well, and learning to make poor decisions as rarely as possible, and learning to use rapid feedback to make better decisions next time. By “next time”, I mean in the next minute, not next day, week, project, or job.
- We would use TDD as a context, asking the developers to make and express each new decision with a test, to implement that decision, and then using the test and the code being written to assess that decision.
Sometimes we write the test and in the writing of it, we make a decision about what the code will look like, because our test calls the code, and therefore is the first user of our code. When we write the test first, the test is a tiny act of design, because the code doesn’t do that thing, and often, doesn’t even have the class or method we refer to in the test.
Sometimes we don’t like the look of the test. In the simplest case, we don’t like the syntax or the details of the way we call the object, and we learn a better way to talk with the object. In my personal case (cases, many), I learn that setting up the test is painful. When I’m at my best, I recognize this as calling for improved factoring. I’ll come back to that.
Sometimes we write the test, and have trouble getting it to pass. (As if not passing were the fault of the test! Why do we even say “getting it to pass”? It’s not the test’s fault.) When we’re at our best, we notice within a very few minutes that we’re having difficulty, and we typically interpret that difficulty as having taken too big a bite. We need an easier question, a simpler test, a smaller next step.
Sooner or later, ideally sooner, our test passes. We would like our “student” to reflect at this point, reviewing the test, the code, their feelings, the recent experience, to see what they’ve learned … or maybe not what they’ve learned, but what they can learn from the past few minutes’ work. And it should be a few minutes, not a day, not two hours. Ideally a few minutes.
The RED / GREEN / REFACTOR cycle of TDD, to the extent we have it built in to how we work, provides a very short cycle of trying, doing, learning, and we’d like our “students” to develop that habit. Quite honestly, I personally would like them to develop the habit better than I have, because all too often I do without a test, or accept a test that is, in retrospect, too hard to write or too hard to pass.
- The problem of design comes down to what we want to call “tensions”1. There are many tensions in the code that we need to bring into an acceptable balance. We don’t want too many statements, methods, classes, files: they’re hard to keep track of. We don’t want too much code in a single class: it becomes too difficult to understand and maintain. We don’t want to split a class apart too much: it becomes too connected and that’s hard to understand. We want a fast algorithm and simple code.
It goes on and on. We balance these tensions, the forces that act upon the code and upon our minds, as best we can.
For the learner we’d like to expose them to as many tensions as we can, help them to recognize them, help them to discover and build in the many responses that we can have to try to adjust the overall collection of tensions that our code produces. I’ll discuss an example below, space permitting.
- Hill points out that programming isn’t just technical, it’s “socio-technical”. It’s about technical work done in a context of many connected people. In terms of learning programming, the “socio” part is oneself, one’s fellow students, and one’s “instructors”. (I quote the term “instructors” because we really aren’t there to instruct or to pass information, we are there to provide a rich experience in which to learn. As part of that, we might try to pass on some information. (Well, some of us might.))
We’d have the team work sometimes in ensemble (mob), with everyone contributing what they have to the program. We’d have them work in pairs, learning how to truly mind meld with the partner, making the best use of all the ideas and observations of the pair. We’d try to encourage a confused individual or pair to ask the team, and we’d encourage the team to find ways to unstick whoever is stuck.
N programmers working independently cannot do N times the work of one. Why? Because their code has to work together and independently-written code won’t integrate and the process of integrating it is not only socio-technical, it’s hard socio-technical, because you think your code is obviously right, and my code actually is obviously right, and we have to negotiate how to jam these things together. And when we do, they still won’t fit together as well as code written by N/2 pairs, or one ensemble.
The team, the socio-technical aspects of programming, must be learned by doing, as must all of these things, so our learning experience would be built around the social concepts as well as the technical ones.
The above is just a sketch of what needs to be considered and done, it’s just a brain dump of what has come to me in the half-dozen hours since our Zoom ended. Now I’d like to get down to two cases.
There is a particular flavor of tension that I’ve been too often ignoring in my building of this Python Asteroids / Invaders program. My testing talks to me, and I do not listen.
My tests have often been hard to set up, and they are often quite long, essentially telling some story about setting up the fleets and putting a bunch of objects in there, each one arranged so as to collide, or not collide, with others, and then ticking a few cycles of game and then checking that what’s now in the mix is what should be there.
That’s better than not having tests, and too often I’ve worked without tests, perhaps because I wanted to see something on the screen and couldn’t even imagine how I could test it with code. But a long story kind of test is pretty weak sauce, and a lot of my tests are like that. Better than nothing, and the best I’ve been able to do. Examples, maybe good ones, or half-decent ones, but not great.
I’m not here to do something thirty-eleven times behind the scenes and then write up the perfect process. I’m here to show you what one somewhat capable developer does, what happens to him, what he thinks that means … and then to leave you to draw your own conclusions. I’m trying to share my experience, not my expertise.
And that’s good: I have a lot more experiences than I have expertise.
But tests that can’t be written, tests that are difficult to write, tests that are long and tedious, are telling us something about our code. Tension from the tests tells us that the code can be better. There was a small example of that the other day, where in testing the ShotController, I managed to have at least a few tests of tiny methods that handled counting or toggling states. Small direct tests, which can be made to work by small bits of direct code.
I probably should have made a bigger deal of those little successes at the time. I’m better at noticing the pain of doing something wrong, than the pleasure of doing it well. That may be because I have so much more experience creating the pain and so little creating the pleasure. I think I’d do well to pay more attention to what goes right.
And let me remind you that I’ve been programming for more than six decades, and I’m still learning how to do the job a bit better. I look at myself and laugh, because so often I seem to have to learn the same thing a million times, but the truth is that even Horowitz practices. Continual mindful attention to doing the thing is what makes us sharp and keeps us smart.
In the Asteroids / Invaders game, I have continually struggled with the tensions around the way the decentralized design works. Those tensions include:
- Overall it is incredibly easy to introduce a new object;
- A common mistake is to forget some interaction that a new object should deal with;
- The Interface solution to this requires many objects to implement many methods that do nothing, simply return, to at least somewhat ensure that I’ve considered that interaction;
- The Inheritance solution to this results in simpler code but increases the chances of missing an interaction.
Last night on the Zoom, I mentioned this design issue and said that “on paper” I’d probably draw a two-d matrix with all the objects on both axes, and in the cells, noting what action should be taken by those two kinds of things interacting. I said that I didn’t know of a good way of representing that matrix in code.
Hill, denying any snark, pointed out that months ago he proposed a matrix kind of solution but that I was too locked in on my “every object handles its own interactions” to even listen to him.
I’ll allow that. We talked a bit about how an individual with an idea is like someone with a hammer: everything looks like a nail. And, if you’re over the age of two, things that don’t look like nails you kind of ignore. As I ignored Hill’s idea.2
If you’re on a team doing well, the team has the program’s overall approach and design in mind. That can mean that when a good idea comes along, the whole team may reject the idea, because they have the hammer already in mind. And that led us to think briefly about what social techniques a team or an individual might have to be able to introduce a conflicting but possibly important idea.
But here, I want to ask a different question: not “should it be everyone for himself” versus “the matrix”, but “why not both”?
It seems clear to me that thinking about the matrix of kind vs kind is a valuable and desirable part of designing this program, no matter how we implement it. We need to think about how asteroids interact with ships and how invaders interact with player shots, and all the other combinations. If we don’t think about a combination, it won’t be handled, and with some designs it might be really difficult to handle.
If we draw that matrix on Paper™ or on actual paper, whatever is in that drawing is lost. It’s not in the program. It might be in one of my articles. Good luck finding that.
But if we could express that matrix in readable code, it would be in the program. Would we have to get rid of the “every object handles its own interactions” idea? Heck, no. The two ideas can live together in at least two different ways.
First, the “matrix”, once defined, could be used by tests to inspect the code and ensure that the code does what the matrix says. We already have tests that half-way do that, the ones that look to be sure that methods are implemented in the various classes. A matrix-driven test of that kind would allow for finer-grain testing and could also allow us to remove things that are not required by the matrix but that are required by the abstract interface.
Second, the matrix doesn’t have to be a big class owning all the interaction code. Instead it could be a matrix of flags, or of method names, or of bound method references, and we could drive interactions through the matrix but still do the work down in the classes. The interaction cod, instead of directly making a method call, would look in the matrix and call what it finds there. The call could still go directly to the individual object.
It might be that the matrix would replace
interact_with, while leaving
interact_with_playershot still in the individual objects that implement it.
In other words, why not both? I might actually try it: now that it has gotten into my head, I find it interesting.
And that’s the bottom line: getting things into our heads, whether we are students, seasoned professionals or octogenarian bloggers. And last night, at least, we were thinking that most of that learning comes from mindful experience, thinking about something, slicing off a little bit of it, thinking about that, testing it, doing it, and thinking about what happened.
Someone said that good decisions come from experience, and experience comes from making bad decisions. I think experience comes from making lots of decisions and assessing them as soon as possible. Early on, maybe most of them will be poor decisions. As time passes, more and more of them will be good enough.
Student, “teacher”, young or not so young, we can all improve, if we care to. Me, I enjoy trying.
See you next time!
A number of us on the Ensemble dislike the term “code smell”. Partly that’s because we have a decent sense of smell and the term reminds us too strongly of unhappy scents. Smells are typically bad. Tensions are always present and are useful, they just need to be in balance. ↩
Chet pointed out that to a hammer everything looks like a nail, but to a dragon, everything looks like lunch. ↩