Every developer (not quite every developer, but bear with me), at some point or another, is confronted with a large, open-source, or generally collaborative codebase. Though the feeling of staring at a massive block of ambiguously structured code can be suffocating, the sense of accomplishment from understanding, learning from, and adding to such a codebase can be empowering.
In my case, the feelings of suffocation and eventual empowerment came from navigating and learning from the rather large codebase that is Story Squad.
What is Story Squad?
Story Squad is a modern-day industry-challenging web application that aims to turn “reluctant readers into authors and illustrators” Founded by Graig Peterson, story squad aims to offer children a fun, creativity-focused game with the intention of reducing screen-time.
Something that made our team’s particular feature so interesting was the concept of developing for developers. The game ran on a cronjob, and most of the components were hidden behind several layers of protection, including but not limited to the day of the week, time of day, conditional database queries, global state, and more. Though this was necessary for the game, it made the developer’s life difficult. Since certain components were only accessible under very particular criteria, working on certain bugs and functionality was nearly impossible. Our solution was implemented in the form of a Developer Mode.
What’s Developer Mode, and Why Do I Care?
Developer mode was created to bypass all of the application’s restrictions, all the while keeping our production database safe. We sought to spoof the application by creating a separate database, fleshing out its corresponding state, and shifting all of our API queries to hit the developer database whenever the so-called “developer mode” is active.
Additionally, it was implemented to explain various game states to other open-source developers through a straightforward UI, since the onboarding into the codebase could otherwise be confusing and overwhelming.
Why care, however, is a slightly different story.
To create such a feature, we inherently had to navigate through the depths of the codebase to understand the underlying mechanisms and where everything was happening. Since the primary purpose of this feature was to make developers’ lives easier, that included observing good code practices, noting bad practices, and refactoring/documenting/implementing good practices along the way. (Though I’m no expert on so-called “good practices,” when you’re navigating such a codebase you quickly pick up some preferences).
With the context aside, here are the main lessons from the codebase.
Lessons from Open-Source
Documentation is King (and so is commenting)
Although this one goes without saying, you may be surprised at how quickly documentation “expires” and becomes irrelevant. After one or two development teams ignore documentation, things can become confusing and unmaintainable quite quickly. Throughout our time we came to understand that we should (for the sake of future developers) try our best to keep all of our documentation up-to-date. That aside, I found that comments were my best friend while navigating other developers’ code.
Relevant to this topic, something I was told was…
“Well-written code is understandable and doesn’t require documentation”
I’m sure we’ve heard something similar to this before.
This is something one of my colleagues said to me while discussing the codebase, and I think it’s not an uncommon opinion. Though it sounds great in theory, I tend to disagree. Not to say that you need to comment everything, but despite how straightforward certain functions may be, the time saved by simply equipping some of them with explanatory comments can save future developers some very valuable time. After all, the only thing faster than reading a function is reading the commented sentence above it.
And next up, my second lesson-learned…
Abstraction* is a beautiful thing
Is this abstraction? Is this just a demo of functional programming? I’m not entirely sure (approximately 60% sure). Love it or hate it though, well-done abstraction* not only can simplify testing, but it can also…
- Set a very understandable standard
- Can allow future developers to jump into the process much faster than if they had to create everything on their own.
- Keep files looking clean, tidy, and approachable
For example, look at the following API endpoint. The function is fairly well documented (though Swagger-UI can be a lot), is concise, and appears seemingly simple. Look closer though, as the next couple of images will expand on it.
Though this is just one example, there was an immense amount of abstraction in our codebase, and while things may have been intimidating or confusing initially, it made implementation a breeze after we became more familiarized.
Whether it was making API calls, repeatedly typing out BASEURLs, API endpoints, and CRUD operations, most of the logic was highly abstracted. From my experience, this kept our functions simple to understand, and even easier to implement. It saved our development team the time of having to manually write all of our functions, and not only that, but it set a sort of standard for how certain functions should be written. And when everything has a defined how, everything quickly becomes understandable.
Personally, this abstraction-focused organizational method was one of the biggest takeaways for me (if not the biggest one). Although I’ve sat back and admired some complex functions and code before, never else in my experience have I utilized someone else’s functions and actually enjoyed them so much. Pair proper documentation/commenting with concise functions and everything becomes very quick to understand, and exceptionally fast to recreate.
Now look back up at the previous two photos, and track the function back. I’ll bet you’d never expect such a basic, inviting-looking function to contain so much logic. The really special part, however, is that most of the functions were reusable. By abstracting the standard API logic into our so-called “crudOperationsManager,” we were able to keep our API calls looking clean and friendly. Not only that, but it makes creating new endpoints intuitive. Here’s a look at the actual crudOperationsManager file itself, to give you some insight.
Though the above file might not demonstrate much just by looking at it, it made all of our actual endpoints between one and five lines of code, just look below.
Developing for Developers: Our Contributions and the Future
Concerning our MVP, we managed to ship out a feature that
- Kept the production database safe via implementing a development database in conjunction with dynamic URL selection and environment variables.
- Spoofed the database and frontend
- Allowed developers to select particular game stages, learn about them in a new stage-related view, and route to their components
- Extended general game functionality
Though this was the most obvious and celebrated success in our MVP, the biggest takeaway was shifting our thought process to developing with future developers in mind. Though we have all heard to “be the person your dog thinks you are,” one thing we may not all have heard is:
“Be the developer you want to inherit code from.”
I should send a very large thank-you to a friend of mine who said this in presentation.
By navigating through such a large codebase we were able to pick up on the good practices and bad practices of previous developers. This allowed us to take a shift, really understanding what it means to develop with future developers in mind. It’s obvious when you’ve inherited sloppy code, and since it’s something we all complain about it’s our responsibility to make sure we are forward-thinking with our code and design.
Moving forward, abstractions like these were a demonstration of one of the many lessons I learned through working on open-source. Not only was this a learning experience for our team, but I hope these examples could inspire and give you a learning experience as well.