Long ago, Kent Beck described ‘simple code’ with four rules. One of them was about expressing ideas in code.

Beck’s rules were that a simple design produces code that:

  1. Runs all the tests correctly;
  2. Expresses all our ideas about the design;
  3. Contains no duplication;
  4. Minimizes the number of entities (classes, methods,etc.).

The rules are in priority order, and sometimes Beck would reverse #3 and #4. I prefer the order shown: in general I’d add a function, method, even a class, to avoid duplication. That said, if we see duplication and intend to remove it, there is surely a concept missing that we’ll have to express with our change, so we can expect that where there is duplication, not all the design ideas are expressed.

The rules are rather obviously not entirely independent. Tests express ideas, as does the tested code. Duplication hints that there are ideas waiting to be expressed. Too many programming entities suggests that there is duplication to be found, expressed, and removed.

It can work the other way as well: too few classes or methods is almost always a sign that there are multiple ideas hiding in that entity, and they need to be expressed by extracting methods, or classes, or functions. The easiest way to spot that there are too few entities is the existence of long classes or long methods. How long is long?

Let’s look at an example method that happens to be on my screen just now:

class ConRod;
    def init_draw(self, canvas, start, finish):
        self._cached_line = canvas.create_line(
            start.x, start.y,
            finish.x, finish.y,
            width=3, fill="red")
        wc = self._cached_wheel_center
        tg = vector(2*self._length, 0, 0).rotate_xy(self._piston_radians)
        canvas.create_line(wc.x, wc.y, wc.x + tg.x, wc.y + tg.y, fill="blue")
        one = vector(1,0).rotate_xy(self._piston_radians)
        half = wc + one + tg * 0.5
        s = 0.1
        canvas.create_oval(half.x-s, half.y-s, half.x+s, half.y+s, fill='red')

The purpose of this method is to create whatever Canvas objects are needed to draw the ConRod component. There are eight statements here, and twelve lines. When my standards are high, this method is too long. I tend to accept long methods in code that sets up a lot of things, or creates some highly-formatted thing. I wouldn’t usually improve this unless I found myself changing it frequently. Once it’s done, with any luck it’s done.

However, the method is doing four separate things, which could be expressed separately, and, according to Beck’s rules, the code would be simpler and better designed if they were expressed. Let’s do some refactoring. We’ll try to go too far.

The last four lines of init_draw are creating the small dot on the diagonal path of the rod, showing the point of the rod’s minimal extension:

conrod

Maybe we should call that a limit marker. PyCharm Extract Method should do what I want. But when I try it, here’s what it offers:

extract

Those parameter names wc and tg are weak.: they don’t communicate much of anything. So let’s do some renaming in the method first. That’s Shift+F6 in PyCharm.

    def init_draw(self, canvas, start, finish):
        self._cached_line = canvas.create_line(
            start.x, start.y,
            finish.x, finish.y,
            width=3, fill="red")
        wheel_center = self._cached_wheel_center
        point_on_rod_line = vector(2*self._length, 0, 0).rotate_xy(self._piston_radians)
        canvas.create_line(wheel_center.x, wheel_center.y, wheel_center.x + point_on_rod_line.x, wheel_center.y + point_on_rod_line.y, fill="blue")
        one = vector(1,0).rotate_xy(self._piston_radians)
        half = wheel_center + one + point_on_rod_line * 0.5
        s = 0.1
        canvas.create_oval(half.x-s, half.y-s, half.x+s, half.y+s, fill='red')

Now the extract method of the last four lines:

    def init_draw(self, canvas, start, finish):
        self._cached_line = canvas.create_line(
            start.x, start.y,
            finish.x, finish.y,
            width=3, fill="red")
        wheel_center = self._cached_wheel_center
        point_on_rod_line = vector(2*self._length, 0, 0).rotate_xy(self._piston_radians)
        canvas.create_line(wheel_center.x, wheel_center.y, wheel_center.x + point_on_rod_line.x, wheel_center.y + point_on_rod_line.y, fill="blue")
        self.minimum_travel_point(canvas, wheel_center, point_on_rod_line)

    def minimum_travel_point(self, canvas, wheel_center, point_on_rod_line):
        one = vector(1, 0).rotate_xy(self._piston_radians)
        half = wheel_center + one + point_on_rod_line * 0.5
        s = 0.1
        canvas.create_oval(half.x - s, half.y - s, half.x + s, half.y + s, fill='red')

We’ll continue a bit before we decide how well we like this. Extract the second-last line to provide a better(?) name. Then extract the first line with a better name.

    def init_draw(self, canvas, start, finish):
        self.moving_rod_line(canvas, start, finish)
        wheel_center = self._cached_wheel_center
        point_on_rod_line = vector(2*self._length, 0, 0).rotate_xy(self._piston_radians)
        self.guide_line(canvas, wheel_center, point_on_rod_line)
        self.minimum_travel_point(canvas, wheel_center, point_on_rod_line)

    def moving_rod_line(self, canvas, start, finish):
        self._cached_line = canvas.create_line(
            start.x, start.y,
            finish.x, finish.y,
            width=3, fill="red")

    def guide_line(self, canvas, wheel_center, point_on_rod_line):
        canvas.create_line(wheel_center.x, wheel_center.y, wheel_center.x + point_on_rod_line.x,
                           wheel_center.y + point_on_rod_line.y, fill="blue")

I don’t quite like the moving_rod_line name. It’s really the rod picture, which happens to be a line. And perhaps all of these should include init as part of their names. But first, the last four lines are different from the first line. The last four are drawing background guide markers. They’re not part of the linkage. Extract again and rename the first one. Rename the methods to include init_. And we have:

    def init_draw(self, canvas, start, finish):
        self.init_rod_picture(canvas, start, finish)
        self.init_rod_background(canvas)

    def init_rod_picture(self, canvas, start, finish):
        self._cached_line = canvas.create_line(
            start.x, start.y,
            finish.x, finish.y,
            width=3, fill="red")

    def init_rod_background(self, canvas):
        wheel_center = self._cached_wheel_center
        point_on_rod_line = vector(2 * self._length, 0, 0).rotate_xy(self._piston_radians)
        self.init_guide_line(canvas, wheel_center, point_on_rod_line)
        self.init_minimum_travel_point(canvas, wheel_center, point_on_rod_line)

    def init_guide_line(self, canvas, wheel_center, point_on_rod_line):
        canvas.create_line(wheel_center.x, wheel_center.y, wheel_center.x + point_on_rod_line.x,
                           wheel_center.y + point_on_rod_line.y, fill="blue")

    def init_minimum_travel_point(self, canvas, wheel_center, point_on_rod_line):
        one = vector(1, 0).rotate_xy(self._piston_radians)
        half = wheel_center + one + point_on_rod_line * 0.5
        s = 0.1
        canvas.create_oval(half.x - s, half.y - s, half.x + s, half.y + s, fill='red')

This is 25 lines, as opposed to the initial twelve. Is it better? Well, in some ways, it seems to me, it clearly is:

  1. The top method, init_draw clearly expresses what’s going on, the rod picture and the background;
  2. Similarly init_rod_background clearly expresses that it draws a guide line and a minimum travel point;

However, the class used to have about six methods, including one init_draw and one draw. Now we have ten methods. That is not so good: it makes browsing the class more difficult.

When we look at the method list now, we see something interesting:

def __init__
def crank_radians
def crank_radius
def draw
def init_draw
def init_guide_line
def init_minimum_travel_point
def init_rod_background
def init_rod_picture
def update

Six of these methods are about drawing, and only four are about creating and updating the actual component. There are two ideas in here, computing the component’s start and finish, and drawing the picture. The rules of simple design ask us to express these two ideas in the code. We could do something like give them an additional word in the method, but that seems weak.

There is a better idea: we just don’t happen to know what it is. But there are clues.

The drawing methods only reference a few values from the component, the _piston_radians, used to position the minimum travel point, the _cached_wheel_center, which we save during update and read in init_background, and the _cached_line, which we create and save in the drawing init and read in the draw method, updating its coordinates, “moving” the line:

    def draw(self, canvas, start, finish):
        canvas.coords(self._cached_line,
                      start.x, start.y,
                      finish.x, finish.y)

Both those values, _cached_wheel_center and _cached_line are hacks to support drawing the background and the moving line. Every component that moves will need to cache one or more canvas objects, so as to update them in the draw method. And since I wanted the ConRod to show some “background” guide information, so that we can eyeball the drawing and see whether it’s correct. I needed to cache the wheel center, which we don’t need for positioning the rod itself. Even the _piston_radians is only used to create the background.

There is a pattern called “Method Object”. It suggests that when a method gets sufficiently gnarly, one might create an object for the sole purpose of doing whatever that method does. Possibly we could do something like that here. That’s out of scope for this morning: we’ve done enough for now.

The question is whether to keep this code or not. I’ve not been committing it, although I could have made several commits along the way, one for each micro change I made.

I think we’ll keep it. Commit: extreme refactoring of init_draw.

Summary

When I started this article 90 minutes ago, I had in mind some general discourse on expressing ideas, and a kind of explanation of why I spend so much time tweaking code. Let’s do that last bit right now:

Tweaking Code
I spend so much time tweaking code for at least these reasons:

First and foremost, I enjoy it. I enjoy expressing my programming ideas, and I enjoy trying to express them all, as Beck asked us to do. It’s fun for me, in the same way that noodling on one’s guitar might be, or scribbling in one’s sketchbook. Not doing anything very serious, just exercising gently.

Second, I believe that those four rules are nearly sufficient to drive out good design. They might not get there as quickly as we might like, but if there is a better design to be had, one or more of those rules will very likely be quivering a bit, going “look at me”.

Third, I believe that any large change we might need can always be done in very small steps, and all small steps are pretty much limited to a very few: renaming, extracting, inlining. Sometimes creating a new thing in small steps and then wiring it in with another small step, calling the new thing instead of the old. According to this belief, all programming comes down to a series of small tweaks, so practicing tweaks is of great value.

Fourth, and certainly very important to me, I want to show my readers—all six of you— how I work and what happens, in the fond hope that they’ll find value and get good ideas of their own. At worst, I figure I can serve as a bad example, though of course I’m doing my best to be a good example.

Today’s surprise example impacted the four rules like this:

  1. Tests: All good. No Change.
  2. Expression: Improved with variable and method names.
  3. Duplication: Very slight improvement if any.
  4. Entities: Added more, reducing simplicity somewhat.

Almost always, when we remove duplication, we add one or more entities. New methods, new functions, new classes. The rules interact. In today’s class the additional methods highlight something we’ve noticed before: these component classes have two purposes, not just one, and that’s a matter of at least some concern.

Tomorrow, if the day goes as I anticipate, we’ll see about moving the drawing away from the component, so that the component becomes much more simple, while the drawing gets no worse, and perhaps, with luck, better.

See you then!