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:
- Difficult-to-meet performance objectives, including
real-time
responsiveness?
- (Code) Environmental complexity, where harmful
races
are possible?
- High-scale
requirements? (Including memory footprint issues.)
- High-availability
(reliability) requirements?
(Fault
tolerance,
five
nines,
hot-swapping,
etc.)
- Legal/contractual obligations, like restricted algorithms? (Thou
shalt/shalt-not...) Or
failure
penalties?
- 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):
- You cannot use malloc/free once the startup (not yet in operation)
phase of the system is complete, either directly or indirectly.
- You cannot start any new threads or processes once the startup of
the system is complete, either directly or indirectly.
- You cannot use any resource that is garbage-collected, either
directly or indirectly.
- You cannot use algorithms that have
O(N) (or
worse) complexity, either directly or indirectly.
- You cannot call any subroutine that itself
has any blocking point (e.g. printf(3), connect(2))
anywhere down its call tree.
- 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.)
- 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!)
- 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.)
- 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.
- 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.)
- 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.)
- 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.)
- 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.
- 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.
|