That post was very well received, with a few exceptions. One of the critics said something like “I’ll keep calling my controllers controllers, and my presenters presenters”. This is fine, of course. If you have a controller, why not calling it “controller”? Or, as Shakespeare said, a controller by any other name would smell as bad :-) . The entire idea was to get rid of controllers, not changing their name!
Still, I can’t really sit here and do nothing while innocent objects get slaughtered, so I decided to write a few posts on living without a controller. I’ll start far away from UI, to avoid rocking the boat. I’ll move closer in some future post.
A well-known problem
I could easily come up with some problem, cleverly designed to prove my point, but I won’t. Instead, I’ll adopt a well-known problem, dating back to 1983, which has been used in numerous articles and books about process control, industrial automation, etc: the mine pump problem (a couple of references: A Framework for Building Dependable Systems by Burns and Lister, and Real-time Systems Specification, Verification and Analysis, edited by Mathai Joseph).
The overall problem is represented here:
We want to pump water out of a mine sump. We have two water level sensors (D, E). When D goes on, we pump out water until E goes off (this is to realize a form of hysteresis and avoid “bouncing” around a given level).
There are also a few gas sensors for carbon monoxide, methane and airflow levels (A,B,C). If any of those becomes critical, an alarm must be raised.
Finally, to prevent explosions, the pump must not be operated when methane is above a certain level.
Existing literature is mostly concerned with specification, timing and modeling issues. Software design issues are largely ignored, and Fig.1 already seems to suggest a centralized controller style.
I’m mostly interested in exploring design choices, so I’ll forego timing issues (which ain’t really that hard for this problem, anyway). I’ll try to evolve a design starting from the most primitive style, and highlight what needs to be done to break out of a centralized controller style.
If you got some time, you may want to explore a few design alternatives on your own: there are many sensible (and less sensible :-) alternatives to what I’m going to present here.
The control engineer way
(Apologies to control engineers, I’m mostly thinking of a friend of mine here).
Control engineers will follow a simple, 4-step, repeatable process for system design:
- write 10 pages of differential equations to prove that if water is coming in faster than you can get it out, you’ll drown.
- assume that once you got your equations right, it’s just a matter of mapping some inputs (bits) to outputs (bits).
- start with a simple MainLoop function, reading bits, going through a sequence of conditionals and switch cases, adding state variables as needed (hysteresis requires a simple state machine), until you can write bits.
- add a Kalman filter at some point. If you ain’t gonna use a Kalman filter, you can’t call yourself a control engineer.
More seriously, what’s really common about control engineers is to spend a lot of time modeling the function and then proceed in total disregard of shape*. Therefore, we usually get a monolithic function. Sometimes, a few functional abstractions are indeed identified and extracted, but more often than not, you simply get a long sequence of statements (sometimes justified by “performance”, usually without any data to back up that claim).
[*I’m moving from using “form” to using “shape” because “form” is often confused with “ceremony”, which is definitely not what I mean.]
Don’t get me wrong. There is nothing intrinsically wrong with the monolithic function. It works. We just get into troubles when we expect some non-functional properties which are at odd with that shape.
Indeed, many cultural antipatterns I’ve observed in the industrial automation field can be mapped directly to the mismatch between monolithic software and some desirable non-functional properties.
Consider testability: if your entire process is implemented as one large function, it will be hard to test thoroughly. Antipattern: claim that faults can only be found in the field and neglect testing. You may recognize this as a self-fulfilling expectation: if you don’t test because faults can only be found in the field, you’ll only find them on the field, thereby reinforcing your belief.
The fake-OO way, step 1
There is a wide spectrum from the monolith to what I’m discussing here, including some entirely functional or procedural solutions, but my destination is a true OO design, so let’s take a first step toward objects.
At some point, people get usually tempted by a few low-hanging fruits and introduce some abstractions:
- the DigitalInput, to read from the water sensors
- the AnalogInput, to read from the gas sensors
- the DigitalOutput, to control the pump and the alarm
In hubris, they may even “hide” those abstractions behind an interface, because (and that’s right) it may help to abstract away a few hw/sw details.
In the end, you get something like this (ah, the beauty of a picture, conveying the entire structure with a few strokes; how far is the day when the agile police will allow picture-based conversations again?)
Of course, we still have the dreaded controller: all the logic is sitting there. Still, there has been a slight fluctuation in the non-functional properties: now we don’t really care about the details of (e.g.) a digital input. Is it being mapped into a register? Is it coming from some kind of serial bus? Who cares (to some extent; the devil is in the details).
If we take the extra step and introduce interfaces, we actually gain something on the testability side as well: we can then mock I/O (depending on where/how creation is actually implemented).
Those of you who are [still] looking at design from the “principles” perspective will see this tiny step as perhaps inspired by the Single Responsibility Principle and by the Open/Closed Principle. Maintainability, however, is still pretty low. We have one, big gravitational center, and that’s where we’re gonna spend most of our time adding, removing, and tweaking functionality. Reusability is also limited to the I/O. Everything else is copy / paste / tweak.
The fake-OO way, step 2
At this stage, people realize that OO is about “modeling the world”, and perhaps parroting the Domain Driven Design guys, start to babble about the ubiquitous language and introduce a few more abstractions:
- various type of sensors
- the pump
- the alarm
This shape is not wrong, of course. Actually, it’s ok, sort of. What is wrong, at this stage, is usually how responsibilities are distributed (or not, meaning: centralized). When you have a controller in your mind, it’s all too easy to end up with stupid classes, because stupid are easier to control.
As you can see, the responsibilities in layer 2 (sensors and actuators) have a 1:1 mapping with responsibilities in layer 1 (I/O). Again, this is not necessarily wrong: layer 2 is closer to problem domain. However, layer2 does precious nothing as far as the controller is concerned.
The entire logic is still in the controller. Writing pump.On() instead of pumpDigitalOutput.Write(1)is an improvement, but it’s not changing the overall design (architecture).
The path not taken
It is perhaps tempting, given the similarities among concrete Sensors (and between Pump and Alarm), to introduce yet another abstraction layer (the Sensor/Actuator as hinted above). Again, this is not wrong, but you shouldn’t take this path too early.
Object oriented design is not about building a deep chain of domain-inspired “abstractions” with little or no behavior. It’s about decomposing the problem so that the intended function emerges from the interaction between intelligent, specialized mini-machines (objects, as seen from Alan Kay).
Adding layers for mere classification is not a terribly useful step, although I understand the intellectual satisfaction :-) in doing that.
Toward "real" OO
A key realization is that to move toward a different set of non-functional properties we must find better abstractions. Those abstractions will eat away the controller, until in the end there will be no controller left.
There is not a single, deterministic process to find those abstractions. We may end up there through domain knowledge, through experience in asking the right questions, through commonality analysis of past implementations, through sheer brightness, and so on. I’m a bit skeptical about the possibility of finding all those abstractions through test-driven design, but some may definitely emerge that way as well. I don’t want to suggest that a specific design process is superior to another, so I’d rather move on and introduce the first abstraction.
The sump probe
We can blame the specification, so to speak. As it usually happens, it does not describe the real problem. It’s more about a specific incarnation of the problem, or about the invariably biased perception of whoever is describing the problem (which is why we used to consider analysis important ;-). The presence in Fig.1 of two stylized sensors was enough to bring into the software counterpart the digital input, and then a level sensor (which is actually a level switch, hence digital).
What if we change sensor technology, and we bring in a submersible level probe, which has an analog output and can tell the actual depth? Well, what we need is to:
- drop the level sensor (ok)
- add a LevelProbe, connected to an AnalogInput (ok)
- tweak the Pump Controller; remove the switch-based level detection logic, replace it to deal with analog measurement. Maybe tweak the state machine too. (not ok).
An alternative would be to introduce, from the very beginning, the concept of SumpProbe. Its sole responsibility is to know when it’s time to drain the sump.
In an early implementation, the SumpProbe could be based on Level Sensors. We don’t need an interface yet; we’re just moving some logic outside the controller and into SumpProbe: generalization can wait. In a LevelSensor-based implementation, SumpProbe will sample two sensors and implement the hysteresis state machine.
Now, this change may seem like nothing, but it’s actually a pivotal moment. For the first time so far, we have a class that is actually doing something. The SumpProbe is not exposing data. It’s exposing a service.
If we want to use a LevelProbe in place of a LevelSensor, we have to change the SumpProbe, but the PumpController won’t need to change, because that logic is no longer there. Of course, now polymorphism can easily kick in, and we can turn SumpProbe into an interface with two concrete implementations, one based on LevelSensor and another on LevelProbe. In a LevelProbe-based implementation, SumpProbe will poll just one sensor. It may or may not be worth sharing the state machine between the two implementations. That’s truly a minor point. I won’t show the polymorphic version in the diagram, keeping only the main structure in place (as my goal is to get rid of the controller, and I want to focus on that).
Interestingly, the SumpProbe, in its LevelSensor-based implementation, would also be the perfect place to add some fault-detection code. If the higher sensor is triggered, the lower sensor must be triggered. That’s a nice invariant for the LevelSensor-based SumpProbe class, which reminds me I seldom talk about Design by Contract, so here is another hint that you’re on the right path: you can actually think of an invariant for your class.
Stupid classes have empty invariant; controller classes have unfathomable invariant (which adds to the difficulty of testing).
A useful Gas Sensor
I warned against introducing a Sensor abstraction before. I want to stress that point a little: the wrong abstraction can actually lead you the wrong way. Now, what is a meaningful, little responsibility for a gas sensor (as opposed to a generic sensor)?
Of course (?!), it’s telling you when a critical level has been reached. The beauty of this responsibility is that, for instance, a Methane or CO Sensor will be triggered by an upper bound, while AirFlow by a lower bound, but that's invisible from the outside. A different type of sensor may be triggered by a window, or by any given law.
Note: the actual beauty is not really in respecting the Open/Closed Principle. It’s about having found a natural place for knowledge, more specifically, knowledge about gas type. GasSensor is an excellent gravitational center for highly cohesive knowledge.
This is a first-cut diagram: again, we’re eating a small portion out of PumpController (checking thresholds). It’s ok to eat a small bit: it means we’re creating small abstractions.
As simple as it is, this change may generate some discomfort. I am making the sensors “less reusable”, because some domain (hence, application) knowledge has got into it. For instance, a CO sensor may need to operate on a lower bound, instead of an upper bound, in an entirely different problem.
Here is where the designer needs to understand what he’s doing, and avoid being a blind follower of some principles or prescriptions:
- Reusability is just one of the many forces we have to consider.
- The new sensors are perhaps less reusable, but they’re more useful. An empty class is absolutely reusable, but also completely useless.
- Before you discard the smarter sensor, consider making it even smarter, and maybe get a smaller codebase in exchange. If we implement just one super-policy (the high/low window) everything else is just a matter of configuration (a threshold being a special case of a window).
Ok, so those were still pretty low hanging fruits. The pump controller is still sitting there, asking sensors, deciding whether or not it’s safe to operate the pump, and whether or not to trigger the alarm.
To the experienced designer, these two responsibilities will eventually suggest two new abstractions.
The Safe Engine
I’ll rename Pump into PumpEngine, because that’s what we’re really operating. Now, the problem with the “standard” engine is that it’s dumb: it doesn’t know whether or not it’s safe to turn on. Well, that’s a SafeEngine job:
I’ve brought in multiplicities (which I’ve left at the implicit level so far) just to point out how easy it would be to make the SafeEngine watch more than one sensor. Just use a list, or perhaps a composite. Once you have the right structure in place, things get simpler, not harder.
We may also want to consider the option to make the PumpController working polymorphically on PumpEngine. I’ll ignore this issue, as this is already a long post.
The Gas Alarm
It may follow with ease now that we also want to trigger an alarm whenever a Gas Sensor triggers a critical condition.
You can see similarities with PumpEngine becoming smarter, except that:
- Here I’m going for 0..n multiplicity from the very beginning. The composite would still be an alternative.
- GasAlarm has-a, not is-a, Alarm. That’s because a GasAlarm is entirely autonomous, with a Watch() responsibility which may actually be run in its own thread (I’m ignoring all the threading and timing issues here, much to the scorn of the existing Mine Pump literature :-).
The End (sort of)
What is left in the controller now? Three distinct responsibilities:
- Creation and configuration of all the other objects
- Watching the SumpProbe and activating the SafePump when MustDrain (now literally a one liner :-)
- Defining the threading model (I’ll ignore this; trust me, it’s a rather marginal issue now).
The second responsibility can be easily factored out into a SumpPump class. After all, it’s all about draining the sump.
I would leave the first to a MinePlant class, which is no longer a controller, but just a builder (in the pattern sense). If you’re in the IoC camp, the MinePlant is particularly trivial to write.
Here is a final diagram. I’m still neglecting a few interfaces which could be useful for mocking and extendibility (like SumpProbe, still a concrete class here), and I’m also leaving some multiplicities implicit, both for sake of space and terseness.
Another path not taken
Some of you may recognize the SumpPump / SafeEngine chain as an "and" chain: we pump when we must drain and it’s safe to do so. As usual, if you work with this kind of systems on a regular basis, it could be tempting to generalize on that and build some kind of rule-based infrastructure.
Again, it is not wrong to do so, but always remember the difference between a domain-based abstraction and a math-based abstraction. In my experience (and as I told before, I used to love math-based abstractions) the former is more easily understood and adopted, the latter is often misunderstood and abused. It’s not a general rule or “principle”. Just experience.
The inevitable extension
Ok, so, I didn’t tell you the whole story. If you read the literature on the mine pump problem, you’ll find one extra requirement: “if, due to a failure of the pump, the water cannot be pumped out, the mine must be evacuated” (which I read as: if the water level does not fall after X minutes, the alarm must be triggered).
In a centralized controller style, we know what to do: we add more stuff to the controller. Is the shape above suggesting a natural place for this responsibility? Is it ok to put it there? What about coupling and reuse? Where would you encode that knowledge? Is it better to change a few dependencies?
Note: I’ve ignored the logging requirement as well. Logging is a cross-cutting concern in the structure above, which is kinda obvious, since I’ve been breaking apart a large class. There is little to be said/added here, if not that this is exactly why AOP should be taken more seriously.
This is just one among many shapes. I took a few design choices, and this is the final result. You may take different choices, still leading to a controller-free shape. For instance, everything above is based on a polling loop (easier to deal with in safety critical stuff), but an event-based approach is ok too, and may lead you to a different structure.
No shape is perfect. So, while I know I shouldn't be ending my post with a critic of my work, I'll ignore the good advice and do it anyway:
- Structure is more complex. I have many classes, and quite a few dependencies. It is worth noting that in practice, most of those classes would be 5 to 10 lines long. Easy to write (bug-free), easy to test, and easy to understand. But the overall shape may not be easy. Here is where I see the value of a diagram as a communication tool.
- The centralized controller is easier for the junior guy. Which is another way to say Worse is Better. Yeah, well, it’s true. Beginners are usually better at understanding complex logic than complex structure and interactions. There is also some evidence that a diagram can help there. But overall, it’s a non-trivial choice about the kind of software you want to write (for beginners or for expert programmers).
- The process is no longer “visible”. This is true, as there is no longer a central piece of software encoding the entire process. It has been scattered among cooperating objects. This is another facet of the same problem: for those who don’t get OO, this shape is harder to understand. For those who do, it provides a number of non-functional benefits.
- It’s easier to add stuff in the centralized controller. Again, this is a facet of the same problem, with a different slant. Every once in a while, someone tells me that when he’s working with a sophisticated structure, he needs to actually think before adding stuff. Where do I put this logic without breaking the conceptual integrity of the whole? When you have little or no structure to begin with, you just don’t care, and you can take the path of least resistance.
Of course we know where this leads: to the big ball of mud, the natural destiny of those who surrender to gravity. Depending on your business model and professionalism, you may still not care.
I mentioned that most (all) classes above would be 5 to 10 LOC. This is not what happens in most software, where there is a power law distribution for LOCs. Today, I got why. I’ll cover that in my next blog post.
If you liked this post, you should follow me on twitter!
There is a follow-up to this post in "Episode 2: the Controller Strikes Back"
... And another in "No Controller, Episode 3: Inglorious Objekts"