I've got to jump in here and talk about my experience having to help a former boss understand why the project he wanted to do wasn't possible in the time available. It came down to incompatible data types and he just couldn't see why I couldn't convert from one to the other (he wanted to do real-time motion capture using an XBox Kinect to animate a 3D mesh of the circulatory system and thought this was easy).
Ben I.'s answer touches on this too, but I can add to it. He says this:
The objects keeps track of some kind of information (instance variables), can be interacted with only in specified ways (interface), have setters (switch/button) and a getters (lights, your eyes).
There's more to it than that. In this electrical engineering paradigm (which should be familiar to most), a light switch that has two states is the most basic example.
- It has output:
bool isOn()
- It has a method of supplying input:
toggle()
- Underneath it has an implementation of what
toggle()
does and how that affects the return value of isOn()
Buttons can also be described in this manner:
- It has output:
bool isOn()
- It has a method of supplying input:
push()
- Underneath it has an implementation which differs from
Switch
slightly: after push()
is called the output is true
for a short duration, then automatically returns to false
. That is, the user must continue to interact with the button in order for the output to be true.
A knob also has similar features, although different types are involved:
- It has output:
float getValue()
- It has input:
twistTo(float v)
- Underneath it has an implementation
But here's where it gets interesting:
What happens if we define an object that implements both Knob
and Button
? We should expect the following description:
- It has output:
float getValue()
,bool isOn()
- It has input:
twistTo(float v)
- Underneath it has an implementation
- After input is supplied,
isOn()
becomes true, then after a duration becomes false again. In order to keep isOn()
returning true forever, we must continue interacting with the object.
What do we end up with?
We can twist it to set the value, which turns the output on and displays the value. Then after a duration defined by that value it turns itself off again. As the homeowner we don't care how it was implemented only that they can interact with it in a certain way (defined by those interfaces) to get the desired behavior.
And yes, I'd present that to students in that manner. Describe the simple items (button, toggle, knob, timer) and then ask the students to figure out what happens when two of them are combined. The epiphany moment when it clicks will do wonders. That's the difference between being told and being taught.
You can expand on this analogy quite far, as well: Three-way switches, time-locks, missile launch confirmation (you need to press two buttons at the same time), sliders, gauges...
And then...you can get to events. Instead of treating the wires in the analogy as hard links in code (while(switch.isOn()) { ... }
), you can treat them as events: when the switch changes state, that update propagates to all objects listening for it and they handle the state change as their implementation dictates (switch.addListener(this.doWhenToggled)
).
The classic "cats are felines, felines are mammals, mammals are animals" hierarchy only gets you so far and provides a very poor and rigid understanding of Object Oriented Programming that causes new programmers to tend towards Do-Everything Objects (where do float planes fit in the "boat extend vehicle, plane extends vehicle" hierarchy? Clearly the only solution is to make all vehicles
drive, fly, and float and just disable the ones we don't need!) rather than through a composition of interfaces (say, FloatPlane extends BaseAircraft implements IWaterCraft
).
I know that sort of thinking messed me up for a number of years. Heck, I'm still recovering from it: I still can't read inline lambda expressions natively, I have to decompose them, and it often takes me several minutes. Same thing goes for creating interfaces, I often have to go the "cats are felines" road until I hit a spot where I realize "I want to use this code on both cats AND cars" because they both share some property that doesn't exist in the common class (and shouldn't) and I have to refactor.
Of course, refactoring is a good thing. Give your students a powerful IDE and teach them how to use the refactoring tools. Eclipse is the god of these , offering an astounding fourteen different options, including "Extract Interface" and "Use base type where possible." Even if they make a mistake, now they know how to fix it in moments rather than hours. No one wants to (and no one will) spend hours fixing an inheritance problem that can be solved by a quick copy and paste; "code duplication, meh, never hurt me."