Object Orientation

I'm not a fan.

This (OO) has been claimed to be the greatest thing since sliced bread. I think it... sucks. OO claims to provide encapsulation and isolation, so that separate pieces of code don't meddle with others' internal implementation, theoretically reducing bugs. This it accomplishes, though often escape mechanisms are necessary in order to actually get the job done, puncturing the veil. So much for elegance...

OO claims to foster code re-use, but does it? Is code getting re-used, or do object libraries become so ossified that separate re-implementations are necessary anyway?

Regardless, let us wave our hands and state that OO has fulfilled its objectives. Great, use it and you're done. You've written a bunch of code, all of it responsible for only the published I/O specifications, and don't you dare look under the covers to see how it's getting the job done, assuming you even can, because that's officially None Of Your Business.

If an acceptable answer to any failure is The IT Crowd 'solution' ("Have you tried turning it off and on again?") then go ahead and use OO, if you want to.

But what if you do care how the implementation operates? Significant bodies of code often have collective responsibilities, which OO can (and often does) actively impede. What if the 'IT Solution' is no solution at all? What if the end product has:

  1. Difficult-to-meet performance objectives, including real-time responsiveness?
  2. (Code) Environmental complexity, where harmful races are possible?
  3. High-scale requirements? (Including memory footprint issues.)
  4. High-availability (reliability) requirements? (Fault tolerance, five nines, hot-swapping, etc.)
  5. Legal/contractual obligations, like restricted algorithms? (Thou shalt/shalt-not...) Or failure penalties?
  6. Death or injury as a result of failure?
Any of the above? All of the above? All of a sudden opaque encapsulation is not your friend, and you must end up with your nose deep in everybody's business. If it isn't, you simply can't meet the product's objectives, much less guarantee the product's objectives. Is your OO approach helping you to reach the final objectives, or is it encouraging you to go down paths that could never reach the end successfully? (By the time you recognize the architectural mistakes you're making it might be too late to correct them. In other words, a quick prototype is quite possibly inimical to a successful outcome, if the task is non-trivial. The landscape is littered with the remains of companies whose 'successful' prototype failed utterly under real-world loads and conditions.)

In projects where timing, state, and scale are critical, the kind that interest me most, the abstraction and encapsulation offered/required by OO can be fatal.

If I use garden-variety OO for most projects, how do I become adept enough using non-OO tools and techniques to successfully tackle the more difficult projects? Instead, if I always use tools and techniques that can handle the most difficult tasks, even when I don't need to, I become more proficient in them, and more likely to succeed at the most difficult tasks. So, no OO for me is the inescapable conclusion.

Example

Consider something like a network core switch/router, something that must operate 24×7×365, without fail, for years at a time. Something that must perform at a high level, indefinitely. (No time off.) Something supporting thousands, millions of connections. Something that must get software upgrades, while operating, without noticeably disrupting traffic. Something that can experience a hardware failure, and seamlessly switch to an alternate resource within a very small window of time. Something that if it fails to handle the situation correctly it exposes you to legal and financial repercussions. (Service-level agreements, etc.)

I've worked on these. It's not easy. It cannot be done using OO languages. It can barely be done using a restricted C subset, fraught with architectural peculiarities. In this particular class of product an event-driven state-machine orientation is the only possible path to success, and even then only if the basic design is good and you stick to a long list of rules. (The state-holding structures are objects, in a sense, but nothing to do with modern OO-ness. All are exposed, there can be no opaque encapsulation or data-hiding, those little scamps cannot be trusted to stay out of trouble off on their own. This is much more primitive, right out of Wirth's Algorithms + Data Structures = Programs. But... that primitivity works, and works well.)

Pesky little rules such as (for example):

  1. You cannot use malloc/free once the startup (not yet in operation) phase of the system is complete, either directly or indirectly.
  2. You cannot start any new threads or processes once the startup of the system is complete, either directly or indirectly.
  3. You cannot use any resource that is garbage-collected, either directly or indirectly.
  4. You cannot use algorithms that have O(N) (or worse) complexity, either directly or indirectly.
  5. You cannot call any subroutine that itself has any blocking point (e.g. printf(3), connect(2)) anywhere down its call tree.
  6. You cannot use streams (which block) for inter-entity communication, you must use (atomic) datagrams. (They either arrive, intact, as events, or do not, and you must handle either case.)
  7. In the service of one event you cannot yourself require any event service, directly or indirectly. (Such service must be handled by introducing intermediate states and additional events to progress between them, and all that this implies.) Servicing an event must run to completion, without pause or fail, else you've introduced opportunities for deadlocks, livelocks, inconsistent state, and races.

    (Completion being defined as waiting for the next independent event, in the same exact place in the code as the first one. A decent analogy is the escapement wheel of a mechanical clock. The bulk of the clock mechanism sits idle until the escapement releases [Event!] the wheel, whereupon the rest of the clock spins at full speed until it can do no more until the next Event. Then, everything stops until the next 'tick' [Event] occurs. All code that is run in response to an Event should be accomplished in 'zero time', virtually [logically] of course, but also as close to that in reality as you can reasonably make it if scaling up is important. Multiple event loops in code would be analogous to multiple escapement wheels in clocks, and that is not how reliable clocks are made!)

  8. Any configuration change (for example) must not affect anything but what is changed, even transiently. (E.g. if I tear down or modify an existing connection, or create a new connection, any other connections must remain, at all times, unaffected by this. You cannot use a naive configuration model where the response to a change is to tear the whole thing down and rebuild according to the new spec.)
  9. Any/every state change in an event handler must induce the same state change in a backup event handler, more-or-less synchronously (modulo Schrödinger's Heisenbug), if said backup handler is part of a system fail-over strategy. (Usually on different hardware. Ideally they essentially both get all the same events.) If I, as a backup, must remain ready to take over from a failed primary at any and every point, I must have an exact and up-to-date copy of all the primary's state, at all times. True failures are, by their very definition, unpredictable; you have to be able to handle anything, at any time.
  10. At point of failure a backup event handler will either have gotten any given notice (Event) from its primary, or it will not. (Scheissenbug.) Both cases are legitimate, and both cases must be handled. If there is any other state anywhere in the system that correlates with this uncertain information, state auditing (and correction) will be required before the former backup can assume primacy—all of this state must be reachable, and not hidden in the program counter inside a code loop. (Loops are not inherently a problem, but their presence should inspire a greater level of scrutiny. Loops cannot require any additional information that is not already in evidence. No waiting, in other words, or you've just broken the rules [#5 here] and introduced a failure point.) An audit failure (mismatch) is not itself an offense, it's just an unavoidable side-effect of uncertainty, and must be reconciled. (A failure to successfully audit [and reconcile] transforms a fail-over into a fail, making your fault-tolerant system a fault-prone system.)
  11. If an event handler is fault-tolerant and has a backup event handler, your handler cannot itself utilize state-holding services that are not themselves fault-tolerant, unless that internal state can be completely recreated, usually immediately, during the audit. (Else there is important hidden state that is not propagated to the backup, and you are actually fault-prone, not fault-tolerant. Kernel and library services are particularly pernicious hidden traps for the unwary.)
  12. You cannot log anything routinely. (Because logging potentially violates one or more of the preceding rules, and using non-volatile media introduces long and highly-variable delays. Fun Fact: modern networks are faster than non-volatile storage, and so that means... «expectant Socratic pause here for Student to restate the Rule under discussion» Gathering statistics, however, is cheap and easy, and can be a reasonable substitute for logging. These statistics are themselves, of course, additional State and may need propagation to a backup event handler.)
  13. Any event being waited for (i.e. all of them) must also have a timeout, representing some kind of failure, which must also be correctly handled.
  14. Every state change, including timeouts all along the way, must be tested, else the system is not actually functional, fault-tolerant, etc. Your mantra is: "If you didn't test it, it doesn't work."
That is just a start. Sound fun? Sound like there are a lot of code libraries out there that you can exploit that follow all the necessary rules? How do you ensure (and prove) that all the new code you're writing (or importing) does follow all the rules? Don't forget that real systems are much more complex than this little thought experiment.

Opacity and information hiding are not your friend in a restrictive environment like this. You must know all about everything! (But Wild West programming cannot be tolerated, either. Discipline and conventions are required to prevent the kinds of problems that OO was intended to protect against 'for free'; strict code reviews by the architects, diligent adherence to the design, etc. cannot be avoided. TANSTAAFL.)

On the plus side, most of these restrictions are not required of administration session programs, although the actions of said sessions cannot ever cause any of the rest of the system to violate these rules, directly or indirectly, even transiently. Beware of indirect influence, such as memory pressure or caching effects. Administration sessions still do not have carte blanche, but they do have a lot more freedom and can usually run with (somewhat) relaxed rules.

For success the architects must have a thorough and correct design, and be guiding and supervising all the code that goes into the product. Nothing that violates any of the necessary rules can be allowed in, lest it cause failure in the field. 'Glue-gun' or vibe programming must be prohibited. Exceptions cannot be made for political or scheduling reasons. Corporate reputations, once lost, are difficult to regain, and the more expensive and/or spectacular the failure the worse the fallout.

A portmanteau of Schrödinger, Heisenberg, and bug to indicate a problematic inscrutable duality, with a touch of German scatology thrown in to give this altogether unpleasant triad an appropriate flavor.

Return to Site Home