There's a constant tension in programming between "what do you want to have happen"—an abstract design—and "how do you get that to happen", a concrete expression of rules that achieve some result. If we had a Computer Oracle (not to be confused with Oracle-the-company), we might ask "What is the meaning of life?" and get the answer (42, of course). But we don't: we have to tell a computer to do stuff in fiddly little steps.
As soon as you break a problem down into an executable algorithm, with component pieces, you start to come up against issues with how the pieces interact. Programmers get things wrong, and cause pieces to interact incorrectly. Requirements change: a program made up of lots of pieces that do A, now has to do B instead (or in addition). For cost and time reasons, we want to re-purpose old code to do new things. All of these lead to bugs. If only we could have comprehensible, well-behaved big steps that we just compose out of comprehensible, well-behaved little steps....
Many early languages were "verb oriented": create a simple machine-level variable (like INTEGER
or REAL
), modify it, ADD X AND Y GIVING Z
, and so on. If you needed to build a large amount of state to describe something, such as a Mars Lander, you had a large number of variables holding all the parts. It then becomes far too easy to apply the wrong verb to the wrong part, like attempting to read the temperature of the parachute, or rotate the orbital thrusters with a function that only works on the wheels.
"Typed" languages offer one approach to curbing bad interactions. If the rotate
function/procedure only operates on a wheel
type, you can't accidentally call it on the RCS system. But then you end up with a profusion of action verbs, thruster_rotate
and wheel_rotate
(although some typed languages offer namespaces so that you can do thruster.rotate(thruster_id)
or similar, which won't interfere with wheel.rotate(wheel_number)
).
Object Oriented Programming offers a different approach: the program's state can be kept in terms of individual "object states", and accessors operate on those objects. Now thruster.rotate()
can work on an RCS thrusters while wheel.rotate()
works on a wheel. It is, or at least can be, clearer what's working on what. (Note that OOPLs can and usually do have fancy namespaces too.)
Alas, people still make mistakes. Accessors give you (as a programmer) a way to allow other parts of the system—often written by other people—to interact with "your object(s)" in a well-controlled way, and maybe one more useful to them. If you have a temperature sensor, you can offer readings in both degrees_F
and degrees_C
even though internally, the actual reading is in microvolts. Protection (however loosely enforced—it's been noted that C++ must be from the Free Love era, in that it gives friends access to your private parts) at least allows you to state, in code form, your intentions as to who should fiddle with what.
In the end, programming is often much about abstraction: exposing that which needs to be exposed, but at the same time, hiding anything that should not be exposed. Accessors and protection give you direct control over what is exposed, and what is hidden. Even if they are mostly advisory (as in Python), they are still a direct statement: "If you're not working on this module, you're not supposed to use or modify this thing. It's just an implementation detail, not part of the interface."