Essay on Inheritance - Popular, Attractive, and Not Very Good
Despite being one of the most popular code-reuse pattern, inheritance often leads to messy, less maintainable codebase in the long term. Why is it popular if it's bad? What can you do instead?
Inheritance is still a widely used tool in software design. I think there is something fundamental to inheritance that makes it attractive to software developers. But it’s not often the right tool to use.
FirefoxWidget, each inheriting from
The subclasses overrode relevant functions to adapt to different browser behaviours. This worked well for a while until a new popular browser called Chrome entered the market. The natural reaction to this was to create a new subclass for Chrome, except this couldn’t be done without duplicating a lot of code from the other classes. Clearly, the class design wasn’t working very well.
It became much worse as we moved into the wild world of mobile browsers where there were more than a handful of browsers with different quirks. It was obvious to me this design didn’t really scale but I didn’t know what to do back then.
Over time as I worked with more code, "composition over inheritance" suddenly clicked. I've read about it before, but it wasn’t obvious to me how to apply it well. I’ve worked at a few more companies since then, but I still see it misused all the time.
Popularity of Inheritance
In 2001, Joshua Bloch wrote in Effective Java why we should favour composition over inheritance. It’s been almost two decades since then. Why do we keep seeing this? I can think of a few reasons for this.
First, there is a whole generation of developers who were taught inheritance as the primary method of OOP design. Many earlier popular libraries have been written using inheritance. The famous unit test framework JUnit introduced the concept of unit test classes where you inherit from the base TestCase class. JUnit moved on to the new annotation-based style but it didn’t stop developers from structuring tests the old way. Newer frameworks like pytest use composition via dependency injection as the primary method for organizing test dependencies. My experience with pytest has been very positive. The framework naturally leads to more modular tests.
Fortunately, this seems to be going away. When I took a software design course in 2011, they didn’t teach composition over inheritance. Now it seems to be part of the curriculum.
Second, inheritance offers the path of the least resistance for implementation reuse. Mainstream languages like Java do not offer a convenient way to do composition, at least compared to inheritance. Inheritance gives you a special syntax to construct your parent class, for example. Compare that to composition where you have to pass the object via a constructor, add a member variable, and then every method call over that object has to be qualified with the name of the object. It feels wrong to have to go through so many steps! Combined with the tendency for Java developers to have verbose variable names, no wonder many people default to inheritance (just imagine having to write
abstractCollection.x(), instead of
x() for every delegated function
Another reason is that it takes a lot of experience and deliberate thinking about software design to understand and experience issues with inheritance. Let’s go back to the WebWidget example I mentioned earlier. The design worked fine for many years until the requirements changed (i.e., numerous new browsers). Once the requirements outgrew the design, the signs of a design breakdown like downcasting appeared in the codebase (i.e.,
instanceof in Java &
dynamic_cast in C++). Unfortunately, by the time this happens, the original designers may not even be around to learn the consequences of their design. Even if they were around, they would have to know about the alternative design choices (like composition) to realize how it could have been done differently. Put it another way, you have to be at the right place at the right time to learn the lesson.
Deadly Attraction of Inheritance
Unlike the long-term downsides, there is an immediate upside to using inheritance. It gives developers nice, warm feelings right away. Seriously, developers have an occupational disease—genericitis—for both reusing code and categorizing objects, sometimes to their demise. Inheritance as a tool does both, so it’s insanely attractive for developers. It’s a dangerous trap, really. It feels good now, but it hurts you later. Many leaders in software design have been warning us about this particular trap through rules like composition over inheritance, the rule of three and YAGNI (you aren’t gonna need it). Unfortunately, they are not as well-known as principles like DRY (don’t repeat yourself). My explanation for this is that principles like the rule of three embody the next level of understanding above principles like DRY. This is something worth exploring more deeply.
Underneath the rule of three is the learning that we are not so good at predicting the future. This is well-known in the project management circles as the cone of uncertainty. Software design at its core is about making bets about the future. We predict what data belongs where, and how objects will interact with each other. When we get the design right, it pays off by making the code easier to modify and extend. On the other hand, when you make a wrong design decision, it bites us back with higher maintenance costs later. The stronger your prediction is, the more expensive it gets when you get it wrong. There is value in making a weaker prediction because it will cost you less when you get it wrong.
Let’s connect this back to inheritance. Inheritance is a very narrow prediction about the future. It defines strict subtyping relationships. Implicit in the relationship is the assumption that child classes do not overlap, and that they fit in a hierarchical category sharing implementation in a certain way. However, unlike mathematical objects, real-world entities can rarely be categorized hierarchically. The world of web browsers looked hierarchical until they weren’t. File systems were categorized into the "Unix-type" (
/ as path separator, and case sensitive), the "Windows-type" (
\ as path separator, and case insensitive), until they couldn’t be—HFS+ on MacOS uses
/ as path separator but it is case-insensitive. Evolution looked like a mathematical tree until we found out about the horizontal gene transfers. Hierarchical categorization is a good aid to understand the world, but it is often not the right tool to describe the truth. Inheritance is a narrow bet that is unlikely to pay off later.
One important thing to note is that the issues of the hierarchical categorization don’t apply when we use them to model an artificial world like mathematics. Every natural number is an integer. The set of all natural numbers don’t overlap with negative integers. We can be assured of this relationship not changing because we defined them that way. The troubles occur when you conflate prescriptive concepts like integers with real-world descriptive concepts like web browsers.
Difficulty of Advocating for Simpler Design
Advocating simpler designs at work could be challenging. It takes a lot of courage and conviction to say, “I don’t know what the future looks like”. Convincing others of this is a fundamentally asymmetric battle. Fancy designs, however wrong they may be in the future, sounds a lot cooler. On the other hand, enumerating all the ways the design could go wrong is much harder. The irony is that it’s harder because future prediction is hard.
This do something vs. do nothing asymmetry can be found in other fields. In medicine, iatrogenesis refers to the harms caused by medical professionals attempting to heal, rather than not doing anything. For example, the misuse of antibiotics brought us the lethal superbugs. Even though antibiotics don’t help with the common cold, many people still believe that it’s effective and demand their doctors give them the drugs. It’s much harder to explain to patients why antibiotics don’t work for colds than to write them a prescription and keep the patients happy. Never mind those micro bacterial superbugs kill thousands every year, unlike the common cold.
It’s human nature to do something than nothing even when it’s harmful to do things. Taleb talks about this problem in his book Antifragile.
What can we do about it
Here’s a very practical set of ideas for all of us to fight this problem.
As an individual developer, you can start building your modules using composition and show your co-workers that there are different, better ways to organize code. The maintenance benefit may not show immediately. One thing that will show immediately, though, is the ease of unit testing, as it is significantly easier to test delegation than different branches of inheritance hierarchy.
If you are using Python and are using the unittest module to write tests, consider switching to pytest. pytest supports the legacy unittest style tests as well so you can have an easy transition.
If you are using C++, you can make use of private inheritance over public when the subtyping relationship is not warranted.
As for Java, I think developers should consider using super-short single-letter member names for trivial compositions that would have been inheritances (e.g.,
a instead of
abstractCollection). Some code reviewers may flinch when they see a single letter variable name, but I don’t think such a reaction is warranted. It’s still clearer than the inherited methods where method names are imported completely unqualified, and possibly from multiple ancestors. Such composition is supported by the same principle that recommends against
import *, that it’s bad to import unspecified symbols into the scope. On the other hand, making variable names verbose when its meaning is unambiguous is not supported by any reason.
Finally, you can spread the word by sending your coworkers a link to this blog post.
Why I did not blog for two years
It's 2020 and I realized I haven't written a blog entry for a full two years since the end of 2017. It's not like I don't want to write. On the contrary, I do want to write, which is why I started the blog. I think it is cool to write and I have good ideas that some people and I will find interesting. So I figured I should break the silence this year by why I should write more often by writing about why I didn't write and what I am going to do about it.
The first reason I didn't write is that I feared that my writing will be judged by others. I would think about an interesting topic, lay out the essay in my head, then proceed to criticize it. Thinking too much about WWHNRS-What Would HackerNews Readers Say. Of course, by not writing anything at all, I don't get to find out what people would have said and worst of all, remain unhappy since I wanted to write and get better at it. I realized I have the same pattern with music. I like music and the thought of making new music but as soon as I try making something, I think to my self, this is cheesy, not good enough and so on.
Second, rather than writing something and edit it after, I try to perfect the sentence I am writing. This really hurt me at work last year because I had to write a lot (Amazon is famous for its writing culture). The primary reason for me to write at work was to convey my ideas in an understandable way. Impressing others was perhaps the least important thing to do in that context. Instead, I found myself stuck writing a single paragraph for an hour. It was tiring and was definitely not fun. Most importantly, it did not help me achieve my goal: conveying my ideas to others. In the end, I had to spend extra time trying to rush the rest of the document.
Here's a strange analogy: writing is like vacuuming my home. Hard to get started, but once I start the process, it's fun and it feels good when it's done.
What am I going to do about it? I'll write more this year, at least once every month. Well, that's a bit tautological. More specifically, I will write smaller posts like this one. Many of my past posts have been incredibly long, which are hard to write and even harder to read. I'll be more charitable to myself. It's okay to write something possibly wrong, or just completely wrong, it's not the end of the world. After all, it's not like what's in my head is always right or consistent for that matter.
Raspberry Pi thermostat - Python Controller
This is a continuation of the Raspberry Pi thermostat series. The hardware part can be found here.
With the hardware built, now we need software to control the thermostat.
I decided to use Python for its simplicity. At first, I put up a simple website where I can control the thermostat using my phone. Soon, I realized it's actually easy to integrate with Apple Home via Homebridge so I implemented the interfaces required to get that working as well. Doing that let me do things like "hey siri, set the thermostat to 26 degrees".
The following is the overview of the solution:
The code is here, but it has lots of features that are not necessarily used today.
- RPi.GPIO for controlling GPIO pins. This comes with the Raspbian OS already.
- Flask to put up a simple HTTP interface for homebridge.
- DHT11 for interfacing with DHT11 sensor.
- Adafruit_Python_CharLCD to control the 1602 display.
The server just spins up a bunch of servers (implemented as threads) that polls sensors and carry out actions. Whether Python performs well with multi-threading is irrelevant here since the CPU is mostly idle.
There are 5 parts:
pconfig - for persistent configuration
Since Raspberry Pi can lose power or need to restart for updates, you need to save the configuration on the main disk.
The code is dead-simplee. It just reads from and writes to a JSON file every time you ask. Because the call volume is so low, there is no performance impact to worry about.
Stuff that is saved: * Target temperature day & night - I find that I always want the temperature to be 2 degrees C higher than during the day, so I have a separate profile for that. * Target humidity * Current Governor (see below)
temphumids - temperature & humidity sensor
temphumids records the temperature & humidity every second.
You can also query for the latest sampled temperature & humidity. In reality, I take an average of all the samples collected in the last 30 seconds because DHT11 measurements fluctuate a bit.
display - displays two lines
Display literally accepts two lines to display and just forwards it to the LCD.
oracle - tells controller what to do based on your preference
is simply to run what I call a 'governor' periodically (30s)
carry out actions. Definitely not the best design but the program is small
enough that it does not really matter much.
I have three governors:
|What they do
|This governor just leaves everything off.
This governor makes sure that your home is cool and dry. The interesting thing
I learned is that leaving the fan ON makes your home very humid even with the
cooling coil on. Apparently the reason is that if the fan is on, the water has
no chance to condense on the coil.
|This is pretty simple, it just turns heat on whenever it's cold. It doesn't really care about humidity because there is nothing you can do in winter to improve the situation.
server - interface for homebridge-thermostat
Homebridge is an open-source NodeJS server that interfaces with Apple Home via HomeKit API.
Using the homebridge plugin homebridge-thermostat, you can just provide the HTTP interface for the thermostat and let your iOS devices control your thermostat. The plugin is poorly documented but I was able to read the source code to find out what APIs you need to implement.
Interfaces you have to implement: * /status return the governor, temperature and humidity information * /targetTemperature/t - set the target temperature * /targetRelativeHumidity/rh - set the target humidity * /off - set governor to off * /comfort - set govenor to heat * /no-frost - set governor to cool
Make the server run on boot
Of course, we want this service to be running all the time. The best way to achieve this is to make it into a systemd service. Making a simple systemd service is very easy. First, write a service definition file like this:
Description=Raspberry Pi Theromostat
ExecStart=/bin/bash -c "FLASK_APP=rpithermostat.server ./venv/bin/flask run --with-threads -h 0.0.0.0"
This works great because all the standard out and error just gets redirected to syslog, which is what you want normally anyway.
To install this, just copy the file into
/etc/systemd/system/. Then run
systemd enable servicename
to make it run when booted up. Run
systemd start servicename to start the service right away.
The homebridge would randomly stop working. I never bothered to figure out why, but I "solved"
the issue by just creating a cron job that restarts every hour (
0 * * * * systemctl reboot).
It has been working well for many months now without any issues.
I could improve the heat governor by making it control the power outlet attached to a humidifer in winter. That way I can make the humidity just right.
Raspberry Pi thermostat - Building the hardware
This blog is about building my own thermostat with Raspberry Pi. This is part 1 where I explain the hardware. Part 2 talks about how I build the software that controls it.
What did I not like about the mercury thermostat?
I didn't like my old mercury-based thermostat for a couple reasons. First, the temperature fluctuation was pretty significant, up to 3 degrees C because mercury takes a while to react to the temperature change, Also I didn't like having to go to the living room to adjust the thermostat all the time.
Why did I not just use Ecobee or Nest? This was for fun & to learn how to build basic electronics using RPi ;)
The interface to the HVAC is a simple single stage 4-wire control.
heatresistive heat strips - white - not used
- fan - green
coolingheat pump - orange
- power - red
Basically, you just need to connect the power wire to what you want to turn on.
Connecting power to
cooling will heat/cool your coil.
Since I live in an apartment equipped with a heat pump, connecting power to heat pump will cool in summer and heat in winter.
Then you also need to run the fan for the air to circulate.
- Raspberry Pi - it can be any model really, but you want wifi for remote control.
- You need 3v, 5v, GND, and 4 GPIO pins minimum. 7 more for a 1602 display.
- Soldering equipments (example)
- Lots (10~20) of female to male jumper cables (example)
- Wires - I just used a 22 gauge wire
- Prototyping board (example)
- 3 x 2.2k and 3 x 10k Resistors
- 3 x 2n2222 NPN transistors
- DHT11 digital temperature & humidity sensor
- Minimum 3 channel relay (this is what I used)
- A 1602 display, if you want to display status also. It's named 1602 because it displays 2 rows of 16 characters.
Here's the schematic for the core parts:
Solid lines denote where I had to use a wire. Dotted lines denote where I didn't have to use a separate wire thanks to either the board or existing wires.
P4 denote any free GPIO pins.
- 3.3v to power DHT11.
- 5v to power the relay.
P1communicates with DHT11 (both read/write).
P4controls the three relay outputs.
Communicating with DHT11
DHT11 needs only one data pin because it both handles input and output through the same pin.
Controlling the relay
This was the only non-straightforward part that required a bit of thinking. When the relay is powered, the switches are simply disconnected. In order to 'close' (or, connect) the switch, you need to drain the current from the relay pins.
This is where the NPN transistor helps. It has 3 parts:
current (C) and
emitter (E). Electricity flows from
E, only if voltage is
In this case,
C accepts current from the relay, but it doesn't let it go through
B has voltage. And we control the voltage by setting the line high
from the Rpi.
So in my circuit, asserting
P1 high connects power to
controls fan and cooling
Here's a ghetto looking finished thermostat in action:
Due to my lack of any real hardware skills, I could not put together in a more polished way.
Check out the part 2 for the software that runs this thermostat.
tmux.conf with clipboard integration & extra features
I have been using the terminal multiplexer tmux for almost two years now, but I never used its mouse support. The big reason was that it messed up the native terminal copy-paste support.
I spent some time reading the tmux(1) man page, and finally got something working that's comparable to gui terminal emulators like gnome-terminal.
Nice things I added here:
- Copy mode selection with keyboard/mouse will copy into the clipboard, rather than just the tmux buffer.
- Right click to paste from the clipboard.
- Middle click on the window label to close a window.
- Double click on any window label to open a new window.
- Drag to re-order windows.
For those who are not familiar with tmux terminology, a tmux "window" is comparable to a browser "tab".
Here's the relevant tmux.conf parts annotated (my actual tmux.conf is here):
# Enable mouse support.
set -g mouse on
# Middle click on the window label to kill it
# "=" is apparently a macro for the "selected window number" but only for
# a certain set of commands.
bind-key -n MouseUp2Status kill-window -t=
# Drag to re-order windows
bind-key -n MouseDrag1Status swap-window -t=
# Double click on the window list to open a new window
bind-key -n DoubleClick1Status new-window
## Clipboard integration (only applicable when you have an X server running)
# Selection with mouse should copy to clipboard right away, in addition to the default action.
# Unbind the default action first.
unbind -n -Tcopy-mode-vi MouseDragEnd1Pane
bind -Tcopy-mode-vi MouseDragEnd1Pane send -X copy-selection-and-cancel\; run "tmux save-buffer - | xclip -i -sel clipboard > /dev/null"
# Copy mode copy should also copy it to the clipboard as well.
unbind -Tcopy-mode-vi Enter
bind -Tcopy-mode-vi Enter send -X copy-selection-and-cancel\; run "tmux save-buffer - | xclip -i -sel clipboard > /dev/null"
# Right click to paste from the clipboard
# If you like middle click better, change MouseDown3Pane to MouseDown2Pane for middle click.
bind-key -n MouseDown3Pane run "tmux set-buffer \"$(xclip -o -sel clipboard)\"; tmux paste-buffer"
A few interesting things to note:
- The equals sign (
=) in kill-window is a macro for the selected window number. But, it only works with specific commands, in an unescaped form. You can't do
confirm-before "kill-window -t=". Don't waste your time trying to get it working.
- You must redirect xclip output into
/dev/null, or tmux will hang (Relevant SO link).
- Also, If you are not using the vi key binding, all the un/binds with
copy-mode-viin it, needs to be replaced with copy-mode and their respective key binding. The default tmux key binding is actually hard-coded in their source code.
- Notably, this seems to work pretty well on the latest release of Windows 10 Bash on Ubuntu on Windows / WSL (Wow, what a mouthful name!). Just run an X server like Xming and your clipboard will integrate with the Windows clipboard. Now I can use tmux full-time on Windows.
What can be improved:
- Disable the clipboard integration if X is not available. Since I always work with X, I didn't think this was useful.
- It does not select a word like on double click.
- Right after selection, it exits the copy-mode. It's kind of jarring.
Screenshot (well.. it doesn't look any different with mouse support)