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?
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?
- Environmental complexity, where harmful
races
are possible?
- High-scale
requirements?
- 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.)
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, 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. Much more primitive, right out of Wirth's
Algorithms
+ Data Structures = Programs. But... it works, it
works well.)
Pesky little rules such as (for example):
- You cannot use malloc/free once the startup 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 for inter-entity communication, you must
use (atomic)
datagrams.
(That 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. (Completion being
defined as waiting for the next independent event, in the same
exact place in the code as the first one.)
- Any configuration change (for example) must not affect
anything but what is changed. (E.g. if I tear down an
existing connection, or create a new connection,
any other connections must remain unaffected by this.)
- 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.) 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.
- 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...)
- 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."
This is just a start. Sound fun? Sound like there's 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
example.
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.
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' 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.