Bookish Development Diary, episode 1.
So currently, Bookish is sort of on the floor in pieces.
I was getting fairly close to something I thought that I could start sharing with a few select friends, then I decided that a feature that I thought I could leave until version 2.0 really needed to be in version 1.0.
Or more precisely, the underlying technology for the feature needed to be in version 1.0, even if the feature itself didn’t get exposed until version 2.0.
The feature in question was the ability for the user to add custom fields to the database. Sounds innocent doesn’t it…
People use different classification systems for books, and have their own weird taxonomies, and maybe want to associate unexpected extra bits of data with the books. They want to track when they last read something, who they lent it to, where they were when they read it. What colour the cover is. What sort of binding it has. Having a single fixed schema for the database doesn’t really accomodate this usage pattern. Something flexible (more like the UI for Apple’s Contacts app, where you just add things when you need them) probably makes more sense.
The classic MVP1 philosophy would say that this is the sort of feature that isn’t essential, and could be pushed back a version or two. Make the schema fixed for now, with a decent set of defaults, maybe add a single “custom” field that the user can use for their own purposes, and refine this into something more flexible later.
After a fair bit of agonising, I talked myself into this argument, and went for a fixed schema. I have a tendency to go too far with flexibility, so I thought that it would be good to resist that urge. Discipline, Deane, discipline!
So I started building out the UI with this fixed schema. Fairly soon, I realised that whatever the database end of things looked like, I was going to need a dynamic UI on top of it. Pretty much every field that existed in my schema could actually be absent in practice, and it made no sense to show a bunch of empty rows with labels that the user might never need or use.
I could have hard-coded each line of a table to a particular attribute in the database, and have them hidden or shown as necessary. It didn’t take long to realise though that I needed much the same code to show the entries for people and publishers and other entity types as I did to show the entries for books - but with different fields of course. Hard-wiring the UI in Interface Builder for one bunch of records was tedious enough, but doing it for four or five bunches… meh. MVP is all very well, but what about DRY2?
Then there was the cross-platform question. At this time I was building Bookish as two separate apps: macOS and iOS. I got a basic version of both up & running, but was concentrating mostly on working on the macOS one. Hard-coding the UI in Interface Builder, in this scenario, really felt like it was a bit of a foolish thing to do when I knew I’d have to repeat the process again later with UIKit rather than AppKit.
So after a bit more work I ended up with some relatively abstract code which could take one of my database records and display in a sensible way just the stuff that was filled in. All very standard - the kind of code most of us have probably done a ton of times. It used property names, and key paths, and an enum of possible fields and custom table row types, and a little hard-coded table which mapped them all together.
Much of the view-model of this code could be shared between both platforms, minimising the amount of repetition between macOS and iOS. Still a hard-coded mapping though, note. Not fully dynamic and generic. We’re going for MVP here remember. Generic would be cool, but YAGNI3 baby. Or, at least, YAADGTNIEBNYH4.
As an aside:, this was probably around the time when Catalyst finally went from “Apple are clear that this is not ready for prime time”, to “Apple say this is ready for prime time, which we of course know means that it’s not ready for prime time, but it’s probably going to be ready by the time Bookish ships, so maybe I should use it”. So that was nice. On the plus side, I only had one UI to maintain in IB. On the minus side, it was on the platform I’d not been concentrating on so much. On the plus side, the cross platform effort I’d put in meant that it was pretty easy to catch-up on the iOS side. On the minus side, maybe I now had more flexibility than I needed in a singe-platform-from-the-ui-point-of-view world? Ah well, no harm done.
So this is sort of where I was at, except a few things were bugging me.
I was really sure that I would want the flexibility. YAGNI be damned. How hard would it be to add in later to what I had? Might I be causing more problems for myself in the long run?
I was also finding that the UI was falling in to that area where you just know that it makes a lot of sense to write it generically, and you are in danger of endlessly turning objects into dictionaries, and dictionaries into objects. Was I making things unnecessarily complicated by defining rigid model classes and then having to define translations to/from something looser?
Unrelated to the model per-se, another of my early design decisions was to try to adopt a proper action abstraction. Rather than mutating model objects directly in UI code, I wanted the UI to issue actions which mutated the model; so that the same actions could be issued by scripts, or macros, etc (more about this in another post, at some point). But how did this square with having fixed model classes? Wasn’t I ending up having to define a lot of very similar actions (“make new book”, “makle new person”, etc) which did essentially the same thing to different entities?
Finally, what about things coming down the line like SwiftUI. Ok, I’m maybe not going to adopt that right now (or am I?), but…
All of this was pushing me towards a solution that was more asynchronous, more generic, more transactional, and a bit less tied to manipulating actual model objects. I probably wanted to be able to request information on one or more model objects and then build the UI when the information arrived. I probably wanted to be able to modify the properties and relationships in the UI without really changing the model, and then submit everything atomically when the edit was done, to update the database. If I did all this right, the database schema would be a much looser sort of thing, and I’d have my flexible user-defined fields almost by default.
I wouldn’t be being honest here if I didn’t also admit that at the time, I was getting a bit bored.
There’s a kind of anxiety and imposter-syndrome that sets in for me when you’re building something new that you’re not sure anyone else will care about or want. Whilst I’m in the early stages of development, I can rationalise that away because I’m getting a ton of other benefits out of the process: learning new things, and catching up with technologies that had been on my to-do list for years. Once all the big decisions have been made and you’re just trying to version 1.0 out of the door, it’s harder.
Having the chance to play around with new technologies is great. When you’re working on a mature application like Sketch, it’s a lot harder to do. Once you have a lot of users, and a substantial codebase, and a ton of features on the list, your options are reduced and your horizon narrows.
It’s a problem most of us would kill to have, and I don’t for a minute imagine that Bookish would ever be a tenth of that size, but having customers and a released product isn’t without some loss of flexibility… it’s nice to be able to change whilst you still can, before any of that sets in.
So there we are. All of this stuff was bouncing around in my head, leading to a kind of pivot point.
Maybe I should chuck away my existing model and start again with something more flexible. How hard could it be?
Well… quite hard it turns out - but that’s ok.
Bookish is currently in bits on the floor. There’s a hole where the database used to be, and I’m in the process of plugging in something else instead.
It’s only when you do something like this that you realise how coupled you’ve accidentally made parts of your code to classes and concepts that perhaps they didn’t need to be coupled to. This in itself is a valuable lesson.
My decision to do this is probably crazy at this point.
If I’d been a manager, working to a deadline and spending someone else’s money, and one of my coders had come to me asking to make this change, I would have said “let’s ship what we’ve got first”.
But hey - if you can’t follow crazy ideas when you’re your own boss, and turn everything upside down because you’re getting bored - when exactly can you?
Maybe it’ll prove to be a big mistake and set me back by months, but… it’s fun. Which at the end of the day, is what I’m doing this for in the first place :)