Then the unthinkable happened. Somebody actually came up with a comment to Life without a controller, case 1, said my design was crappy and proposed an alternative based on a centralized, monolithic approach, claiming miraculous properties. I couldn’t just sit here and do nothing.
Besides, I wrote that post in a very busy week, leaving a few issues unexplored, and this is a good chance to get back to them.
I suggest that you go read the whole thing, as it’s quite interesting, and adds the necessary context to the following. Then please come back for the bloodshed. (Note: the original link is broken, as the file was removed; the "whole thing" link now points to a copy hosted on my website).
The short version
If you’re the TL;DR kind and don’t want to read Zibibbo’s post and my answer, here is the short version:
A caveman is talking to an architect.
Caveman: I don’t really like the architecture of your house.
Caveman: you have specialized places I have to go. If I need to take a dump, I have to go to that place called bathroom. If I need to get some food, I need to go to that place called kitchen. What if I want to take a dump while getting some food? I have to move around! Don't you ever get that problem? In my cave, it’s all in one place and I do whatever I want where I want. I really can't see what's wrong with that.
Architect: uh, ok, but please get out of my kitchen.
(Sorry for the caveman language; he’s a caveman).
The long version
My dear Zibibbo, please allow me to say that I’m actually thankful for your critics. You’re giving me a chance to clarify my thoughts better, and to deepen my analysis of the centralized / monolithic approach. That said, I’m sorry, but I’m going to slaughter your design and code for public amusement.
In what follows, text in brown :-)) is always a quote from Zibibbo. Whenever possible, I’ll try to quote him in the same order as it appears in the original text. I have quoted small portions, so it would help to read what follows with a copy of his text in sight.
At the end of this long section, you’ll find a more articulated, less fragmented discussion of some general design issues related to the centralized, monolithic approach. If you want to skip the gruesome argumentation, you can jump down to “Back in reflective mode”.
You yourself mentioned a number of criticisms of your design
Yeap. I consider the ability to see any kind of weakness (especially in our own proposals) and strength (especially in other’s proposals) a necessary condition to call yourself a professional designer. I also consider the honesty to disclose them a sign of intellectual integrity, again a trait of professionalism.
It basically contains only the abstract mapping from inputs to outputs your control engineer was thinking about. It doesn't implement any main loop, nor it does any I/O.
You keep using the term “abstract” all around your text, but in practice the only thing you got is isolation from I/O.
In your perspective, your mapping is “abstract” because it’s defined in terms of “math-like” things like booleans and floats instead of “concrete” things like sensors and pumps. From a methodological perspective, this is very 1980. We used to learn that kind of stuff in CS101 in the past century. Still, the world is moving elsewhere, for a few good reasons:
- Requirements come in domain terms. Someone is going to ask you to add a redundant pump. Nobody will come asking you to add one more hidden variable to your Markov chain.
- Math, in the sense of the number-crunching stuff, is largely commoditized. Nobody is writing an FFT from scratch anymore, except the few who are pioneering new technologies (see cuda), waiting to be commoditized anyway. So, math is the low-level stuff in 2012.
- Domain abstractions, however, are not going to be commoditized. That’s where you should be spending your time, creating meaningful, reusable classes.
What you don’t seem to get is that a Sensor class is an abstraction (Abstraction is a reasonably well-defined concept), and that your process is totally about sensors and pumps, not about booleans and floats. You propose exactly the kind of “bad abstraction” that I’ve been talking about more than a few times now. But enough theory. Let’s see what you get in practice.
Well, in practice there is nothing “abstract” in that mapping. It’s a very concrete, flat mapping between (input + state) to output. Not having to know (inside your logic) if you have two floating sensors or a single probe is an act of abstraction. You don’t do that (until you are forced to); actually, you work against it. As we’ll see, your “abstract logic” is also not decoupled from “low level” concerns, as you call them, like threading. Actually, it’s dictating them (details follow).
That said, technically speaking, your class is not a controller, as it’s not controlling squat, as you said yourself. It’s best defined as a monolithic model, or monolithic process model to be more precise. So, technically speaking again, what follows is not about controllers, but about monolithic software.
Indeed, your approach shares several of the disadvantages of a monolithic controller, adds some of its own (as we’ll see), and is no more testable than what I discussed under “the fake-oo way, step 1”, as you get I/O mocking with that simple step as well. It’s an interesting approach, anyway, which I haven’t seen frequently in practice, probably because it’s less safe than a controller (I’ll show you near the end).
I’ll take this opportunity to highlight the fact that my design was not, as some said, an example of “fat model, light controller”. It’s a no-controller architecture. A fat model, as you perfectly demonstrated, is not a guarantee of proper OO design.
Then I would have all the sensor objects call the appropriate methods on the controller each time they detect a change in the input value, and the objects controlling the output devices (motor and alarms) periodically poll the controller to check if the corresponding devices should be active. The corresponding classes can be decoupled from the controller class by using closures, or, equivalently, an event-driven approach
This requires some machinery to connect things together, that you neglected to show. Yet that portion requires changes when the wiring changes. Just like the builder in the OO version. Remember that when I get back to this in a little while.
The controller class holds all the abstract logic of the system in just one place. If I want to understand at a high level how the system works, I only need to check this one class
This is sort of a tautology, which I hear quite often from the monolithic-minded. If you write 100 KLOC in a single function you have a single place to understand and change. Unfortunately, it’s not a genius strategy, but this is a mindset problem, and I’m not planning to change your mind. I understand your position very well. I just don’t agree with it. No large system can be (safely) built without breaking it into small, cooperating parts. Your approach just does not scale (you show that yourself, and I’ll prove it again and again below).
The problem here is that you’re confusing your personal preference for understanding things by linear reading of intertwined code with a general truth of which you have no proof, while the rest of the world is betting on modularity (not just in software, by the way. Modularity is of key economic value in manufacturing as well, etc.).
I'm particularly suspicious, for example, of your GasAlarm class, which, as I understand it, would mix the abstract logic (triggering an alarm when the gas level is critical) with a very low-level concern like the threading model.
No, it’s just you trying to read my words in the worst possible way, for your own convenience. I said: “GasAlarm is entirely autonomous, with a Watch() responsibility which may actually be run in its own thread”. I didn’t say (a) that GasAlarm owns the thread, (b) that it’s mandatory to do so. I just said that I have the option to do so.
You have that option too: you may have your sensors and actuators polled from a single thread, or you could actually have multiple threads. Oh, wait, you can’t :-). Not on the sensor / setXXX side, at least. You would need some kind of synchronized access to the whole
On my side, I like having options. I like the idea that I can reuse a GasAlarm in a different design and provide it with an independent thread. Oh, but you don’t believe in reusability of domain logic anyway. Bummer.
The controller class is reasonably small, and it's very simple conceptually. It could be made even smaller and simpler by creating, for example, a tiny helper template class that encapsulates the concept of hysteresis.
This is your style again :-), to extract only the mathematical notions. You did that with the bowling too. When I pointed out issues with your procedural solution, you answered by proposing a state-machine framework, totally removed from problem domain. I told you what I thought in the subsequent post. It’s better to accommodate domain-related abstraction through proper form than abstracting out the domain into some form of math. I repeated this in my previous post too, and believe it or not, when I wrote about “another path not taken” I was talking to you. You keep looking for abstractions in the wrong place.
Again, I’ll take this chance to highlight another common mistake in naming classes (no, you didn’t do that; I just forgot to mention it in my previous post). I always try to find meaningful names in the problem domain. Some, for instance, would have called SafePump simply SmartPump. Except Smart means nothing, it’s just another naming antipattern. When someone inherits our code, Smart is telling him nothing; Safe is telling something.
This is also the case when someone inherits a bowling framework modeled as an “abstract” state machine, instead of a framework based on domain concepts with meaningful, domain-related extension points. Faced with the need to implement new requirements, expressed in domain terms, he now has to learn your code first, expressed in some other terminology which makes sense only to you.
The controller class knows absolutely nothing about the rest of the system. A nice consequence is that it can very easily be tested in isolation.
Again, you keep saying that, but it’s just isolation from I/O, like in the first-cut fake OO version I presented. You got nothing more than that. Besides, your “easily tested” completely ignore the state space combinatorial explosion. More on this later.
The rest of the system (with the exception of the builder class that creates the whole object graph) also doesn't know anything about the controller (thanks to the use of closures, or the event-driven approach), which means that it can easily be made reusable.
- There is not much else in the mythical “rest of the system”, except I/O, which anybody can make reusable (I didn’t call it “low hanging fruit” by chance). The juicy part is in your controller, which is not reusable, not extendible, and not adaptable except by tweaking. Great job :-).
- Why is your builder ok while my builder is not? (See below).
The abstract logic can be changed easily, both because it's separated from the rest of the system and because it's all in one place. In particular, all the high-level data (input signals and abstract state) in the application are kept together, so when the mapping between inputs and outputs changes, or when new outputs are added, updating the code tends to be trivial. You never have to find your way around the object graph to reach the information you need, like you do in a "real" OO design. All the information is at your fingertips.
This is really ludicrous. You’re passing your personal opinion as truth, with your argument being that everything is easy to change when it’s all in one place. You’re completely ignoring the explosion of complexity as the state space and logic grow (again, see below). You’re also actually ignoring what the entire world has learnt in the past 40 years or so: that without modular reasoning and encapsulation, things just don’t scale.
Now, youngsters don’t read the classics, and as Santayana said, those who don’t know history are condemned to get a D- :-), so here is a little quote. Back in the 70s, Fred Brooks, father of the IBM OS/360, wrote a critic of Parnas’ concept of Information Hiding in his well-known “The Mythical Man-Month”. 20 years later, in the anniversary edition, Brooks reflected on what was right and what was wrong. Here is Brooks: ”I dismissed Parnas's concept as a "recipe for disaster" in Chapter 7. Parnas was right, and I was wrong. I am now convinced that information hiding, today often embodied in object oriented programming, is the only way of raising the level of software design.”
I’m counting on being still alive 20 years from now. If you happen to have an epiphany, let me know :-).
But the abstract logic is a very different beast. First, it is (by definition) much more application-specific, so I don't think reusability is something worth striving for. It also tends to change a lot, in ways that I find difficult to predict. And, worst of all, I think it's much more difficult to partition into reasonably indipendent subsets, because of all the data dependencies in there.
(Funny how the “abstract logic” is now “application specific”. What happened to abstraction? :-).
I can only hope you can see the self-fulfilling expectations here:
a) You don’t believe in reuse of application logic, so you don’t even try to break it into reusable pieces. Then you claim it can’t be done. Wake up man, in my version every single piece is reusable.
b) You think it’s hard to partition the system, so you don’t learn to see the natural seams in the forcefield. I know it’s hard for some and natural for others. I’ve seen it time and time again mentoring people. But beyond talent, it’s also a matter of application and training. You’re trapping yourself in the monolithic view.
Say that the requirement that the engine must be switched off when the methane level is high is not present in the original specification, but it's only added at a later time. In that case, your initial design would not, I presume, contain a SafeEngine class, and the PumpEngine would be operated directy by the SumpPump. When this new safety requirement comes up, you face a problem; the SumpPump has no way to know whether the methane level is critical because it is not "linked" to the object that holds that particular piece of information. So you not only have to change the SumpPump (and maybe add a new SafeEngine class), but you also have to rewire the object graph. Contrast this with what happens in the controller-based approach, where all you have to do is change one (one!) line of code.
Now, this is an interesting argument, which allows me to explain the wiring thing. Weird enough, in your case you have to wire things together anyway (and I’ll get more specific in a moment) but you choose not to see the problem (very professional :-).
Of course, as I add new abstractions, I occasionally have to re-route things. This is why I left to the well-inclined readers to understand the consequences of attaching the SumpPump to the SafeEngine, instead of polymorphically to the PumpEngine.
But you seem to think that since the SafeEngine is used by the SumpPump, it must be created by the SumpPump, and that to do so, the SumpPump has to know the GasSensor. Well, if you do OO this way, it’s no wonder it does not work for you. The MinePlant is the builder. I also suggested that an IoC could make some things simpler.
In a real-life, large-scale system we use configuration-driven wiring, based on catalogs of I/O, sensors, actuators, etc. That allows us to plug things in at any time, adding stuff without modifying existing code, build product families with ease of extension and contraction (as good ol’ Parnas said, but you weren’t listening; more on this in the second part). Theoretically, I can even rewire a live system, although I never had this requirement, so I can’t claim I did it.
Oh, by the way: I have to bring water to the kitchen and to the bathroom, and electricity as well. I don’t mind the wiring, because I enjoy the separation of functions. Of course, you’re free to use a single place for all functions :-).
It's not just that you have to think harder, it's also that the changes you have to make are way more complex and no longer localized in a single place.
C’mon. You’re ridiculing yourself. My changes are “way more complex” because I have to rewire a builder? While tweaking a larger and larger controller is fine? Sure. Whatever :-).
Now, this is a trivial example, and doing the rewiring here is easy. But in real-world applications, this particular issue drives me nuts all the time (Don't you ever get that problem?).
Let me tell you something that we in the gray hairs club :-) usually know. When you implement a non-trivial system, no matter the initial design, at some point you’ll face some issues.
If you believe in the design vision, you’ll squeeze your brain, come up with a nice solution (that is, an improvement over the initial design), and proceed. If you don’t, you’ll claim the design is a failure, and proceed doing what you always wanted to do.
In your case, you need to create sensors and actuators and connect them to the centralized
Did it ever occur to you that in the OO version you could have a machinery to connect things by name, describing the wiring outside the application logic, so that (e.g.) every sensor has a name, and every part (pump, smart pump, etc) can be attached to any other thing by configuration, using a catalog, and not by hard-wiring things in code?
Can you see how this would completely solve your issues with getting hold of things? Can you see how this will expand reusability, composability, etc? How this could help a system grow over time using “modern” techniques (dynamic libraries) where modules are independently written, tested, and deployed? How this would beautifully link with diagnostics, synoptic, etc, with the ability to drill down everything by name?
Of course, if all you can see is a monolith, you’ll never invest in the necessary infrastructure, because you just don’t believe in modularity. So when you face problems with a modular system, you don’t squeeze your brain to solve it by increasing modularity even more. You fall back to the monolith. Well, it’s your choice :-).
So I generally prefer, when coding the abstract logic, to end up with "fatter" classes, that hold all the data that is likely to be used together. And I think a controller/manager is a very natural place to encode this highly cohesive knowledge.
Please. I’ve actually defined the concept of center in software as a locus of highly cohesive knowledge. And your controller (model) is not. It is not cohesive by any of the known software engineering metrics, and is not cohesive by my (yet to be explained) definition based on entanglement.
But leaving the big theories aside, your fat model fails the pragmatic test for cohesion, which is, can I break it apart so that the communication between parts does not require exposure of internal details? Of course I can. I did. You did it too when forced by a new sensor type. So that knowledge is not cohesive. QED.
You see it as highly cohesive because you choose not to see the natural seam in it; you choose to ignore the natural decomposition as suggested by the forcefield; you actually choose to ignore the forcefield and design a fixed-shape system (the monolith), irrespective of the problem. Sorry, as a designer, I have to say that it is a crappy design strategy.
As another example, I've also implemented the evacuation rule; all I had to do in the controller was to add a tiny new method, is_water_level_critical() (two lines of code in total)
This just ain’t true. Even for this two-line change, you also messed up with set_high_sensor_state, to set last_time_water_level_became_too_high.
This is a perfect case in point. Your coding style will always require you to patch things around, particularly so in the various setXXX, since those are the natural sub-gravitational centers in your design, where stuff happens and state is updated.
So you add feature X not by adding code in a new modular unit, but by making your class larger and tweaking the existing code, at risk of breaking it. And since you have a combinatorial explosion of the state space, we just have to trust you to have enough test cases to cover for the occasional errors. Way to go, man :-). Compare that with the “way more complex” job of just rewiring things to connect new code, leaving existing code alone.
Outside the controller, I would have to create another alarm object, and link it to this new method. But that would be very straightforward too, the architecture would already be able to handle that, as it would already be doing the very same thing for other types of alarms.
I’ll show you in a moment why this is a very biased view, but should be pretty obvious now that you trivialize the wiring problem on your side and make a big fuss out of it on the other.
The invariant is hardly unfathomable, and in fact, I think the controller is an ideal location to place this kind of integrity checks, because it can implement complex invariants on data that in a pure OO architecture would be scattered throughout the entire object graph, and that would be difficult to reach from a single place.
You’re contradicting yourself. If the invariant is simple, how can it be complex? The problem is that you’re again trivializing the issue of state space explosion, and ignoring the fundamental principle of modern [software] design (modularity). We want to check parts independently (simple invariant) and compose them in a safe way. You want to work as a caveman. Your choice.
In a real application it's of course going to be larger that it is in this toy example. But even if it were, say, 4 or 5 times larger, I still would find that perfectly acceptable, given the fact that its structure is so simple. If it got much larger than that, I would of course think about splitting it into several pieces (as few as possible, though). In practice I find that that can usually be done while at the same time avoiding the problems of the pure OO approach. I think one nice way to do it is by using (abusing?) multiple inheritance.
You see, besides the obvious mockery for abusing a design concept for its implementation consequences, you just told everybody that your design strategy does not scale. At some point, despite your effort, you’ll be forced to split your class into more manageable pieces.
Even at that time, you’ll try to resist, ignore the natural seams in the forcefield and keep it as monolithic as possible. Then you want to use multiple inheritance so that everything is still visible in case you wanna mess with it (see the quote by Brooks again man – you’re doing it wrong).
Funny enough, when you had to split your class, you didn’t use MI, so you now have distinct objects (controllers). You didn’t say that, but now you have to change the damn wiring. The input is no longer connected to the MineController: is connected to the LevelSensorController. The LevelSensorController must be connected to the MineController, and it’s not a sensor or an actuator, so you need some machinery in place to connect arbitrary objects.
Why is that not a problem in your design, but it’s supposed to be a problem in the OO design? Because you either did the OO thing wrong, or you can’t see you have the same wiring problem, or don’t want to say that you have the same wiring problem.
So, to wrap up, I really can't see what's wrong with keeping all the high-level logic and data in one place
Well, honestly, just get out of my kitchen :-))
On the other hand it seems to me that the pure OO architecture has a couple of pretty obvious flaws;
the first one is that the high-level application logic is more obscure
To you, because you want to find it all in one place. Modularity requires the ability to understand a system a piece at a time, after which I personally think it’s easier to understand the overall behavior from a diagram.
You may want to try an exercise. What if I draw a portion of my system like this, translating the UML into a more pictorial form (using a bunch of engineering symbols):
The problem is that you’re shape-blind. You’ll never be happy with OO until you develop the ability to see function through shape.
Of course, you don’t have to like OO. Remember what I said at the beginning of my previous post: "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".
You’re just epitomizing that statement. Actually, you’re making things worse, by pretending your software has properties which, in fact, does not possess.
and the second one is that the application is more brittle and difficult to change
You must live in your own reality distortion field, because it’s exactly the opposite. You’re blinded by the wiring thing and your own preference for monolithic logic.
OO software like mine is easily extended and contracted by adding/removing small, domain-inspired, independently testable abstractions (I’ll show you a couple of examples below). Your code requires tweaking an ever-growing class, adding methods and changing existing ones, leading to a combinatorial explosion of states which have to be tested together, etc. etc. It’s error prone and does not prevent common mistakes like using the wrong variable out of a large set (back to the 70s, man). It even fails if you set the sensors in the “wrong” order (out of a gazillion; see below). Talk about brittle. You gotta be kidding.
I would also add that I believe this is a signature problem of "real" OO solutions.
I would add that your stance is the signature problem of those who can’t understand the concept of form, modularity, modern programming practices, yet think that the problem stays with the rest of the world and therefore dig deeper into their cave, leaving a few math symbols behind to look cool. But that would be rude. So I won’t.
Back into reflective mode
Ok, now that I’ve provided some (undoubtedly unsatisfactory :-) answers to most of your points, why don’t we lay back and try to add some perspective to the whole thing.
You don’t want to see reusability
I choose the mine pump instead of making up a something exactly because it is a well-known problem. People have already proposed reasonable requirement changes in existing literature, and therefore I don’t have to invent things purposely now, which is part of the aforementioned intellectual integrity.
One of the proposed requirements I’ve seen in some papers is to have two pumps, and alternate between them after some time to avoid wearing the pump out (makes lot of sense too).
In your programming style, that’s just more stuff inside the controller (and more wiring, since now you have multiple outputs). Except that in your existing controller you don’t have a place for time-driven logic. So you can either make up an artificial “time” input or you can try to put that time-checking logic inside some existing setXXX function which you assume will be called frequently enough. What you get, of course, is just a fatter, less reusable controller.
In my design approach, I see this as a great chance to create yet another reusable, application-level class (abstraction!): the pump rack. A PumpRack will keep track of time and switch pump after a while. Do I need to change wiring? Well, from the outside, the PumpRack is just a PumpEngine, so I do have to create a different object, but the wiring is the same (this is where an IoC will give you everything for free).
Now, before we move to the next change in requirements, let me talk about your style again.
Your design style does not scale up
When I introduced the concept of mass and gravity, a smart reader pointed out that if you set up the right gravitational field in the beginning, gravity will be your friend, attracting things in the right place. I further explored the issue in Notes on Software Design, Chapter 4: Gravity and Architecture.
Of course, once you have a monolith, gravity will do the only natural thing: bring in more stuff. However, at some point you won’t cope with complexity anymore, and, as you admitted, you’ll be forced to spin off a few pieces. You can only delay the inevitable by abusing multiple inheritance. Sooner or later, you won’t have the entire state space “at your fingertips” (of which I’m grateful :-).
So, the next requirement change is that if a pump gets clogged, I need to switch to the next pump even if it’s not its time yet (again, makes a lot of sense). Clogging will be detected by monitoring power.
Once again, you have only one place to put that logic: the mine controller. Two more inputs (one for each pump; what if I have 3, 4 or 10 pumps?), more logic into that poor class.
On the other hand, I have a nice gravitational field set up. That portion of the logic is naturally attracted to the PumpRack (I truly hope you can see why; for the interested readers: it’s entanglement manifesting as attraction). I now have the choice to put it in there, or create a subclass (say, FailSafePumpRack) to preserve a particular form of modularity (and reusability) that people tend to forget.
Your design style does not scale down
In the post above on gravity and architecture, I mentioned how one of Parnas’ best papers on modular design was cleverly called "Designing Software for Ease of Extension and Contraction", and how people tend to forget the “contraction” part.
In fact, your design cannot scale down. Once you get beefed up by new requirements, you have all sort of complexity in your controller. You cannot trim it down except by copy and paste (caveman :-). Sure, you can avoid wiring some I/O (perhaps; see also below on safety), but your stuff is all there.
That's another antipattern: thinking that you cannot have a scaled-down version of your software (not to mention a software product line) because it’s too hard, and that you can only “disable” things and leave them in.
My design has no problems scaling down. I can remove the FailSafePumpRack. I can remove the PumpRack. I can remove the SafeEngine if not needed, thanks to polymorphism. I don’t have to disable things. I can bring in the necessary modules and nothing else. This is the beauty of designing a system with emerging behavior in mind.
About scaling and complexity, let me tell you another story. About 15 years ago, an industrial automation company asked for my help in the design of a new system. They had a few OO “pioneers” inside, but basically, the rest of the company was stuck in “controller mode”. They had troubles when the I/O space became larger, say over 300 I/O, and serious troubles over 500 I/O.
We built a first-generation system which was reasonably OO, and they scavenged a small framework out of it. Five years later they called me back. A good guy said “we now routinely build systems with over 1000 I/O without problems” (I was actually quite proud of him :-).
We did a quite few improvements here and there, using more modern programming techniques and bringing in more domain concepts (abstractions!). I didn’t hear from them for about 10 years, until recently. They are now working in the 10.000 I/O range and think they may need something more sophisticated (which is fine, technology and languages are much better now).
Don’t tell me my approach gets too complex, because I’ve proven over and over in real-life systems that it doesn’t. On the contrary, it makes previously impossible systems possible. Sure, it requires skilled people. I don’t believe in deskilling. I don’t believe in designing systems for cavemen to maintain. That’s a heck of a bias, I know :-).
The mine controller is a simple, yet life-critical system. Now, suppose you have to bet your life on a software system. You can choose between two implementations, coming from people of equivalent IQ, experience, dedication, etc.
Company #1 says: “we believe that we can build a safe system by creating small, safe components which can be tested thoroughly in isolation, with a small state space. The components hide their internal state space to prevent accidental changes. Components are organically assembled in larger components (pump / safe pump / rack / etc), still with minimal behavior, and so on, until the complete safe system is created. We develop highly reusable components and we tend to reuse proven modules whenever is possible”.
Company #2 says: “we believe it’s better to put all your state space in a controller, and don’t break it up until you are forced to. When we are forced to split, we abuse multiple inheritance, so the whole state is still available to the central core routines. That way, we can access whatever we want from wherever we want, and we don’t have routing or visibility problem. We don’t believe in reuse because you can’t have two identical large controllers. We also don’t believe in programming errors and we don’t think we’re going to mess up and write the wrong output or read the wrong input. We’re real men. We don’t believe in state space explosion either, and we think we can easily understand what is going on because all the logic is in one place. We can easily test the whole thing because the sensors/actuators can be mocked; hey, we use booleans and floats! Oh, did I mention we don’t believe in state space explosion, which is why we believe we can actually test the whole thing?”
I don’t know about you, but I know which one I would choose. Oh, since you don’t believe in state space explosion :-), in your implementation you have 5 inputs, 2 internal states, 4 outputs (represented by isXXX functions). Let’s multiply that by 5, as you say that you don’t see a problem with that. You now have 25 inputs, 10 internal states, 20 outputs. Assuming for simplicity that every input, state and output is a Boolean (which is not), you’re mapping 2^35 states into 2^20 states, within a single monolithic model class. If, as you said, you don’t see a problem with that, well, please stay away from safety critical systems :-).
Seriously: go through any decent, modern literature on building safety-critical systems, and you’ll see modularity and state space separation as key components. Some even suggest using hardware separation of the state space. Having adequate hardware support, in life-critical systems I would be glad to physically isolate the internal state of my components from each other, to prevent any accidental change (e.g. through wild pointers). Your “global mess at your fingertips” approach is just antithetical to safety.
Oh, by the way, if you’re thinking that “in practice, you don’t have to test all those combinations”, you know what? You’re right. Because despite what you think, that information is not highly cohesive, so it will move inside a small subset of the state space. Ain’t that funny? The only thing that makes your technique barely acceptable is that your hypothesis are wrong :-). Unfortunately, having “all the information at your fingertips” means you’ll never know if you got it right, so it’s not really a safe bet.
Contrast this with my design, which recognizes the natural partitioning and models a corresponding class structure, where the state space is partitioned as well. Can’t you really see the light?
One of the reasons your style is seldom adopted (not even by cavemen stuck in controller mode) is that it’s much weaker than you seem to believe. The controller style, as ugly as it is, comes with a strong guarantee of sequential execution. The sequence is hard-coded in the controller itself. Your fat model does not come with that guarantee. The order of execution is in the sequence of calls to the various setXXX functions, which lies elsewhere.
In general, however, there is no guarantee whatsoever that you can just swap the order of setXXX calls and still have a correct behavior.
In your code, for instance, if I call set_low after set_high, and I have a failure in the low sensor (being false when high is true), needs_draining will become false. And since you put in a safety check (in another function) only because I said that (and you know it :-), people will just die. Good job :-).
By the way, it’s a crappy implementation: in case of sensor failure, unless there is methane present, it’s better to pump out water. You don’t if I call set_low after set_high, but you do if I call set_high after set_low. This is just a simple example of why, even in a trivial case, your design strategy is too weak for professional programming.
By the way: with 25 inputs, you have 25! possible call sequences to your various input functions (that’s about 10^25). Since only some of them work, make sure you’re using the right one :-). As should the lucky one who inherits your code, too.
There is something good, of course
I could go on and on, but let’s switch to the bright side. Your design has a few good properties, just not the one you’re thinking of.
- Observability: it’s easier to observe the global state with a software probe when state is not scattered in memory. We had this requirement in a project a few years ago, but being in C++ we solved it beautifully at the allocator level.
- Patchability of state. A byproduct of the above, seldom used but extremely useful if you’re building a rover to mars. Suppose you have a bug and your state gets messed up. Thanks to observability, a remote team can potentially devise and send a state patch.
Now, I want to stress the intellectual integrity thing again. I’m pointing out any strength I can see in your design out of honesty. Overall, they don’t balance the negative side, and there are different ways to obtain those properties without crippling the entire design.
Keep it up!
Ok, I’ve been trashing you for a while now, but the truth is, I’m not really interested in proving you’re wrong and I’m right. I don’t think that I can (and I don’t want to) change your mind. What we see here is a belief system in action. You believe some things are true. It is only natural to defend your belief systems against my words, and try to crush my antagonist vision.
I also have a belief system. It’s called freedom :-). Within that belief system, you have any right to explore your own programming style (as ugly as it is :-), as long as you don’t try to push it down my throat. I’d rather not having you around safety-critical systems, but ok, life is dangerous anyway :-)
Maybe one day your ideas will evolve into something different, better than what we both do today. Nature values diversity. Especially in caves :-).
I retweeted this the other day, and it seems so appropriate here: The truth will set you free, but first it will piss you off. ~ Gloria Steinem.
If you liked this post, you should follow me on twitter!