What maintainable software means? Part 1.
Over the last few months as the wave of feature requests slowly trickled to a halt for the first time in two years. We've suddenly been able to think about something important. How much did we hack onto our code in order to 'make it work'?
The answer. A lot. This isn't uncommon, and it's entirely symptematic of dealing with post-MVP growth stages in your application. You go through cycles of growth and refactor, growth and refactor. And we entered a time where our need to refactor grew greatly.
I was spearheading a lot of the conversations around this and what it means. Because, when you get to this level, the stasis between the two spots, you need to start questioning every single one of your design choices from before, especially in cases of rapid growth. Why did we build our data model like this, and why did we structure our routing like this, and what's the real best workflow like? After spending a bunch of time developing the front end and the data requirements at breakneck speed, we're forced to look at what we did wrong, why we did it, and what was the best course of action to take in the future, and how can we can prevent it?
Meetings aren't always a joyeous part of the job (although I tend to like these high value ones), but, we scheduled many meetings to clarify what we're trying to do in the context of our application. Our main goal was 'we want to build this as to open source it without any skeletons or black magic as to how it works', even if we never released it as open source, it was a goal to create transparency in the architecture and simplify and standardize how we handled certain situations. We spent nearly a month of our time documenting, transpiling the concepts, and creating stories in our backlog of how we wanted to handle these things. It's not an easy task by any stretch of imagination, but a clearly important one when you're making the shift from an MVP to a full fledged application that's maintainable.
We realized we had a maintanance problem, well... basically the entire time. Originally we had 3 microapps that were built around each other (developed independently) and a single SSO application to handle syncing between the them, and that poor design choice made it very difficult to maintain, so, we took the first step and merged all of the code together, and DRY'ed it all up. We still had significant debt. Like, the national debt sized debt for our team. When I say it was 'that large', I really do mean it. When your team is small, which ours is, it's imperative that you make sure you don't grow outside of what's capable to maintain. In the 'olden days' we used to use lines of code measurement as a rule of thumb of how many developers you need, but a lot of it has to do with having the right mix of developers across many levels to effectively manage and delegate application issues. Our team had a glut of senior level developers, but a lack of mid-level and junior-level meant a lot of the high level application development got blocked by tasks that were best suited for mid and junior level developers. It's typically a problem in smaller teams that that happens to roll up and accumulate over a period of time and significantly hurts team velocity and quality. In a perfect world, senior developers wouldn't just be coding, but actively taking time to ensure standards are enforced, to cross-check code, to read through and comment on pull requests and mentoring. Because when you're looking at maintainability, what you're really looking at is building consistency, where you don't have to explore the wild wild west every single time you step out into a new story. Where any developer can step in without arcane knowledge and do their job is the biggest key to maintainability.
So, after talking about how we've developed a strategy on what's wrong, the answer then becomes: how do we fix it. And for a lot of it, it was mostly reorganizing data, about following standards. As a Rails shop, that meant going by the book with some things, realizing that we had disorganized models and controllers that did too much, that we were stuffing too much logic into views. But we also had significant application design issues, such as how we handled our copious amounts of user data through duplication, normalization and denormalization. We didn't have a specific strategy for it. One of our strategies was to use some polymorphism, and carry similar, meaningful data around between our data models. We shared user scores and action events, progress notes and conversationable items as a flexible, yet restricted type of data in a consistant way. This means: when we looked at how our users were progressing in their action logs, we weren't having to check multiple models, we asked a single model to handle all of this. That clearly leads to consistancy, because when you look at how workflow is driven, you're not often handling a single model. It's the base flaw of using something like RailsAdmin right out of the box and expecting it to grow with your application. What's best for your data isn't best for your administrators, and not all of the time best for your application. It requires a gentle, invisible hand to start staying: what if we do this? What's the implications of that?
As we defined it, maintainablity meant we were able to minimize time spent on exisiting code. For us, that meant we needed to get a more robust test suite. I can't ever suggest this is absolutely critical in the quest to create maintainable code. People may quibble on if you need to run unit tests on static compiled languages like C# and Java, and every single one of those people are wrong. Someone you may know, someone you may like, someone who may be you, can write side effects in an application that have inadvertant consequences. You may end up spending more time validating that your code doesn't affect other code's results than actually writing the code. You may be spread out amongst a half dozen files, hunting things down. And yet, this is exactly why I say you need to build tests for everything. Being able to say: "tell me if this change anything turned red" is a massive tool in maintainability. You're not building code that breaks things unexpectedly. But again, you're only as good as your test code, and your test code leads to more maintainability. BUT THAT'S OKAY. When you're making the change, like we were, to more maintianable, more stable code, you're actually spending less hours in the end on each story. You can say: i'm spending X hours here, and there's then going to be Y hours saved in the end because this touches so many parts of the application, that we're going to be able to test quickly without having to spend a massive amount of time doing high touch testing. It's a waste of time, honestly, to do it any other way.
That, though, obviously requires us to build methods and objects that respect testing, that are small, don't rely on side effects, and return states. It's not a hard thing, but something that we found helps create better maintainable code as well. We took a hard look at documentation, and one of the things that we were lacking were clear directives of what each function returned, and what it was supposed to do. We prefered Yardoc in Ruby and Codo in Javascript in order to get a better handle of what each function was. To go back in time and say: well, I don't know exactly how this works, or why we did it like this, also is essentially an audit of your system. It's a way to say: if I don't understand this, how it's written, how will anyone else? And those are big maintainability smells.
Next week: Part 2 on Maintainability. What we actually did, what we didn't do, and how did it end up?











