Change is inevitable. It is the one constant in software development.
I find optimising to be confident in the face of this inevitable change valuable. I want to be confident that what I’ve done – what I’m releasing – really works.
When changing software I’ve come to ask several questions that help me to be confident in the changes that I make.
- How will I know when something breaks? Does it matter if some given functionality breaks?
- How easy is it to understand?
- How localised would changing this be in the future in any new direction?
- How confident am I? How can I be more confident?
This leads me to a list of principles that I currently value when writing software.
- Ease of Understanding
- Deliberate Intent
These principles lead me towards certain practices that I currently find useful.
In order to know when something breaks a feedback mechanism is needed. Waiting for feedback from an end user that you messed up is far, far too late. The best time for that feedback is the instant the change was made. That is the time when the knowledge is known about what the breaking change is and hence how to fix it. This means testing. Testing at the right levels. Robust testing to ensure that the feedback is as clear, direct and immediate as possible.
Emergent design and Test Drive Development combine to provide feedback. The tests confirm that the implemented requirements are still working.
In order to build something that I’m unsure of, I want to do the smallest thing possible to validate whether what I’m trying to do will work. I cannot write tests when I’m unsure of how it will be called or of how it works. I don’t want to build something to discover that what I thought I was being called with isn’t actually want I’m being called with.
Unexpected things can happen. If I don’t expect them, I may not plan for them. But often I will implement ways to log or notify the unexpected – for when the unexpected happens. For instance using logs to log unexpected errors, or tools like Airbrake (for error catching and logging) or New Relic (for real time stats).
Systems become complex. Large balls of mud become very complex. If one assumes entropy in software is inevitable, any system will get messy. Instead of one large mess I try to focus on creating lots of small messes. Patterns I use to help towards small, contained messes include the Aggregate pattern; valuing Composition over Inheritance; separation of IO and CPU operations; Pipes and Filters; and Anti-Corruption layers.
Emergent design helps me keep a solution simple by only implemented the required functionality and allowing the design to emerge effectively behind the specific interface.
I try to isolate the same domain concepts using DRY. But I try not to isolate accidentally similar concepts. As Sandy Metz puts it – prefer duplication over the wrong abstraction.
Test! Tests provide me feedback on the change that is being made. The expected tests should break. Tests that break unexpectedly should be looked at to understand the unexpected coupling in the system. I try to focus on increasing cohesion over reducing coupling. Increased cohesion provides a better-defined system – while also reducing coupling.
I aim to isolate change with design practices and test practices. The goal is to test at the right level. I try to test the interface so that everything below the interface can be allowed to emerge in whatever sensible design is needed for the requirements now. I encapsulate object creation in tests to allow the creation of the object to change over time without having to change a large number of tests. The goal is that any change in any direction is as painless as possible. This doesn’t mean predicting the change, but rather isolating the obvious changes and hence limiting their impact when they do change.
There should be no area that is too scary or painful to change. When there is the goal becomes to find ways to reduce the fear or pain – assuming it still needs to change.
Ease of understanding
The ability of someone else (or yourself in 6 months time) to understand the existing code is highly underrated. Code is write once, read many. Every time an issue occurs near a given part of code, the code may need to be understood. If it is clear and obvious and is able to be trusted, it will be quick. If not, it could add 30 minutes or more to understand the code even if the code is not relevant to the problem. The cost of not being able to read and understand the code quickly is high over the lifespan of the system when considering the number of people who will read the code over time in their quest to make changes and solve problems.
I use Behaviour Drive Design to drive what tests to write. If I can’t write the test or the code cannot be built from the test, then I do not understand what I’m doing yet. The tests describe the intent of the code.
Discoverability – how will someone else discover that the code works like this? I try to ensure that breadcrumbs exist for a future developer to follow. If the knowledge exists for a developer to follow, then they can ask the question – do I need this.
I value explicit over implicit. If developers are joining your team, how will they know how the system works? Code is obvious. Implicit rules are not. Frameworks such as Rails come with a lot of implicit rules. These rules can be very opaque no matter how experienced the new developer is. You don’t know what you don’t know. The power of Rails is the weakness of Rails. But good Rails developers can move between Rails projects and know the implicit rules. But what if there are implicit rules that are specific to the code base? Those are hard to know. Again – you don’t know what you don’t know… until you realise that you didn’t know it and someone tells you – or you go really deep. Either way a lot of time can be unnecessarily lost grappling with the unknown.
I try to make conscious, well-reasoned design decisions. My goal is to keep the exposed interface tight and deliberately defined. For any solution I try to think of at least three ways to solve the problem and then make a choice based on the pros and cons. This ensures that I’m thinking deeply about the problem instead of simply implementing the first design thought that arose in my mind.
A continued experiment
These are my principles and practices that I’m experimenting with. Over time I imagine they may change or modify as I continue to experiment and learn.
In order to experiment I generally try applying a constraint and see what it does to the code base. Do we like it? Does it make things better? How do we feel about the result? To paraphrase Kent Beck – If it doesn’t help, stop doing it. If it does, do more!
I continue to look for the experiments that I can do to make things better along with the underlying principles and values that they represent. I hope to blog about some of those experiments in the future.
There is a lot more that could be said about each of these principles. I hope to follow up with related posts digging into different implementations and designs that I have derived from these principles.
Credit goes to Kent Beck’s Extreme Programing Explained that focuses on Values, Principles and Practices. I find this an incredibly useful way in which to view software development.