« Questions about JavaScript-based development | Architecture for a buck » |
I've finally made it through Step 1 of the master plan:
That only took nearly a year, though in fairness, I also got to:
With all of this massive progress achieved (only 67% of the todo list remain!), it's time to journal what I've learned over a handful of posts.
I start most projects by first nailing down terminology. The value of doing this cannot be understated. For one, having to rename stuff partway through a project can be distracting, time-consuming, and merge-conflict-inducing. More importantly, though, I find that if I cannot nail down the terminology, I don't understand the design well enough to build it.
At the beginning of WarmAndFuzzy I didn't have a particularly great grasp on the technologies I was using (and also changed tech partway through) so a bunch of changes were required. That's okay for an experimental project in the beginning but it was worth getting it straightened out because there's nothing worse than coming back to a side project you haven't touched in weeks (or months) and finding yourself confused by misaligned or ambiguous word choices that you're now having to trace back and understand.
As of today, 10% of the 842 commits to the project are "gardening" commits that improve naming, formatting, or tech debt without any (intentional) side effects. It's a major morale boost to get to do that sort of work whenever I feel like it, and to get to come back to a nice codebase whenever I resume work.
The physical | functional antonym'ing below is shamelessly stolen from Windows NT.
Sensors also have a configuration that defines operational traits for sensors (chiefly its display name).
As such, we have the convention that normal people using the system on a day-to-day basis only care about (and see) settings while a system administrator (i.e. me) also sees configuration, and that convention is applied consistently to thermostats and sensors (and whatever else may come in the future).
It matters less that these terms are entirely intuitive (which is hard to achieve) and more that they are consistent and that antonyms are cleanly established and maintained.
As we'll see later, the settings vs. configuration divide projects cleanly into maintaining different tables (e.g. thermostat settings vs. configuration), different GraphQL types, different sections of the UI, and different authorized privileges granted to user accounts to control who gets to change day-to-day system settings (everyone in the house) and who gets to see and make administrative changes to the system's configuration.
With the terminology set up cleanly, you can look at any piece of code and understand right away whether something is targeting normal people or administrators without having to further understand what it is. Neat.
Each device configuration specifies the name of the stream its values should be saved into. Making the stream name configurable independent of the device name and/or ID allows us to:
Every device has an id, a string assigned by whatever manufactured the device (Particle for thermostats, DalSemi/Maxim for temperature sensors).
I built the system as a multi-tenant architecture from the beginning because, plainly, it was easy.
When a device contacts the API, we need to look up what tenant we should route its data into, so there is one device tenancy table with that mapping.
Every term discussed so far is visible in the user interface (at the very least for administrators). Every term is used directly, without change, abbreviation, or change in pluralization in the code.
There are a few additional terms I use consistently in or around the code:
ThermostatConfiguration
model class, a GraphQL.ThermostatConfiguration
graphql class, and (when required) a firmware representation, projected with typed functions named graphqlFromModel
, modelFromGraphql
(for mutations), and firmwareFromModel
.shared
directory or one of the two shared
packages. I consistently use the word shared rather than common.npm
lifecycle scripts are named and matrixed consistently, e.g. npm run start{-mobile}:{local,remote}:{dev,prod}
starts a web (start
) or mobile (start-mobile
) frontend using a locally (local
) or cloud-hosted (remote
) instance of the API layer, targeting the development (dev
) or production (prod
) database.SensorConfigurationTable
, not the SensorConfigurationsTable
.ThermostatSettingssTable
with two s? No, this ain't Gollum guarding their precious settingses.id
and get on with it.id
so invalidating/updating cache contents in face of client-originated mutations works like magic, iff that's how you name your fields. You can of course configure it to treat other property names as identity fields, but if those names get re-used for foreign keys or references rather than primary keys you'll be up a creek. Good times.timezone
and project it into a field named currentTimezoneUTCOffset
, a signed IANA UTC offset (e.g. PST = 480
, i.e. eight hours ahead of UTC). Of course the Particle device OS wants to be configured with an inverse signed fractional offset to UTC (e.g. PST = -8.0
) so currentTimezoneUTCOffset
is projected into particleTimezone
:void applyTimezoneConfiguration()
{
//
// We don't bother telling Particle about DST, we'll just change zones (e.g. PST to PDT)
// so the above is sufficient. We just need the math to work, we don't need nice formatting.
//
// Our configuration tells us the current timezone's offset as well as
// the next one and when to switch over, in case we don't update frequently.
//
bool const inNextTimezone = g_Configuration.rootConfiguration().nextTimezoneChange() <= Time.now();
// {current,next}TimezoneUTCOffset: signed IANA UTC offset, e.g. PST = 480
int16_t const timezoneUTCOffset = inNextTimezone ? g_Configuration.rootConfiguration().nextTimezoneUTCOffset()
: g_Configuration.rootConfiguration().currentTimezoneUTCOffset();
// particleTimezone: signed fractional hours, e.g. PST = -8.0
float const particleTimezone = -1.0f * (timezoneUTCOffset / 60.0f);
// Apply
Time.zone(particleTimezone);
}
Pretty much everything mentioned here is a hard-earned lesson, be it on this project or on previous ones.
« Questions about JavaScript-based development | Architecture for a buck » |