The Power of Abstraction
Last week as I was talking about clients and servers, I casually mentioned that there existed several libraries for interacting with GPIO pins on a Raspberry Pi. Today I want to show you specifically why this was such a huge benefit as I was doing the initial architectural planning for this system.
GPIO pins are electrical contacts on the Raspberry Pi which can be configured as either inputs or outputs. In the case of inputs, you can read the voltage that is being applied to the pin between 0 and 5 volts. For outputs, you can programmatically set the voltage within that same 0-5V range. In this way you can interact in your programs with electronic sensors and other devices attached to the computer.
My friend Alex and I wrote the majority of the code for The Last Defender in Node.js. Since JavaScript is an event-driven language, it lends itself very, very well to working with these pins. Using the rpi-gpio package, changes to the pins' values are just events to listen and respond to, the same as you would a keypress or mouse click. This is huge. The library saves us from having to deal with the system-level details of changing or reading pin values, and we can push off the electronic considerations of the physical components to be dealt with at a later time. We have a clearly defined interface between the computer and the other components. Namely, a pin that can send or receive 0-5V signals.
The GPIO library is what's known as an abstraction, and it's one of the keys to architecting good software. An effective abstraction presents a consistent cognitive model to the user, and rarely if ever requires them to know the underlying implementation. I will be honest with you and say that I have no idea what happens between my code executing the command gpio.write(7, true)
and the system sending 5 volts to GPIO pin 7. I never needed to dig deeper, because it just worked. Good abstractions save cognitive effort to be applied to higher-level problems.
The code that you write every day is sitting on top of layers upon layers of abstractions. You don't have to write raw SQL queries because your ORM gives you abstractions for the most common operations. You rarely, if ever, have to worry about memory addresses because high-level languages like JavaScript, Python, and Ruby take care of memory management for you. You probably never think about individual packets in an HTTP request. You've probably never written machine code. There are high-level abstractions available to you to interact with all of these things.
As you gain competency, you will learn to see the abstractions you can build into your own applications. For instance, in The Last Defender I built a small Button
class that enhanced the rpi-gpio
library and emitted events for 'press' and 'release'. The class encapsulated the logic of configuring a given pin as an input, debouncing the inputs, and attaching handlers to the events. I didn't need to write out all this logic for every button in the room (there were dozens of them) but could instead use something like this:
import { Button } from './Button'
// 7 refers to the pin that the start button is wired to
const startButton = new Button(7)
startButton.on('press', () => { /* do something */ })
startButton.on('release' () => { /* do something else */ })
What sorts of abstractions might make sense in whatever projects you're working on right now?
Tomorrow, I'll start talking about how we integrated the game server with the lighting, sound, and video systems.
PS: If you want a fantastic practical guide to what a modern computer is really composed of, literally starting from the binary switch and building the abstractions up all the way to writing an operating system, check out Nand to Tetris.
Next Up:
Fun with Abstractions: Multimedia Edition
Previously:
Client and Server are Just Roles