Code Utopia
Some guidelines for developing practical, agile, high quality software. Any of these points or sections could be their own article, but I think it’s a good idea to summarize them here.
I wrote these up as education and inspiration for the mostly-junior coders I was working with at my last gig. And so instead of boring people with another rant I could just say “Utopia!”
Test Everything.
Locally.
Automated.
Isolated.
All the time.
Mock external services. (Or stub, or fake, or otherwise make them work appropriately in development, test, staging, etc.)
Write your code to be used by tests, and it will be more usable in general.
If there’s code that’s hard to test, refactor it until it’s easier. (e.g. if you call a library directly, extract a method, then put that method on an object, then set that object in your constructor, then pass it in to your constructor… Voilà: service injection.)
Test like a pyramid: lots of fast unit tests, fewer slow integration tests.
Zero Defects.
Once burned, never again.
No broken windows.
New bugs automatically go to the top of the backlog.
When a support ticket comes in, fix it, but then immediately fix the underlying bug. (Do we need better validation? Explicit business logic? Faster or earlier error reporting? More detailed user stories and research?)
Objects Everywhere.
Put the behavior with the data.
Encapsulate data behind humane interfaces.
Put the behavior with the data.
Delegate responsibilities, minimize coupling, maximize cohesion.
Turn parameters into instance variables when possible
Prefer “new Foo(bar).run()” over “run(foo, bar)”.
Complete construction: objects should always be in a valid state.
No getters, no setters.
Put the behavior with the data.
Focus on the interface, not the implementation.
Think what, not how.
And remember: Put the behavior with the data.
Continuous Integration.
After any change, run all the tests locally…
Then run them again on a per-branch environment…
Then integrate (merge to master) and run them again on a master staging environment.
Short-lived branches. Merge to and from master frequently.
Feature Flags enable long development cycles without long-lived branches.
Refactor At Will.
Refactor to understand.
Refactor to increase velocity.
Be vigilant: only you can prevent feature envy.
Turn helpers into services; turn functions into methods.
Shorter methods. Shorter lines. Fewer dots.
Never copy code when you could extract and/or parameterize it.
Fail Fast.
Errors should be handled either gracefully or rudely — nothing in between.
Don’t hide errors from developers, or from users. It’s more useful for users to read “Error: Memcache Entity Defrobulated” than to read “Sorry! Something happened, please try later.” since the former allows us to track errors and allows users to at least distinguish between different types of failures instead of just thinking “it’s all broken, again”.
Continuous Deployment.
Every branch gets an instant, transient per-branch staging-esque environment, ready for feature review
When a feature is merged into master, it is automatically deployed to staging
The product owner clicks a button to deploy staging to production…
…OR…
Every push to staging is also immediately deployed to master, and the product owner clicks a button to enable features via feature flags
Definitions
Service — code external to your system or object that performs a vital feature but is not essential to your system itself. There is usually a Real version of each service but if you design to interfaces, you can swap in different implementations for testing or demoing or feature-flagging.
You can (and should) also use Services to hide different parts of your system from each other. Use Dependency Injection to configure objects so they don’t need to know the difference between running in test mode or local mode or staging mode or prod mode, or connect directly to their databases or cloud apps; they delegate those details to the service object you passed in.
Stub — a service that returns canned values, usually “true” or “false” or “none”
Fake — a service that maintains its own state internally but doesn’t do much else; a smart(er) stub.
Mock — a service that expects to be called with certain methods and parameters in a certain order, and fails fast if the expectations are not met
Spy — a service that logs calls and then (often) passes through to the real service underneath; the logs are queried later by tests.
Humane Interface — an interface that’s designed for humans to use, and that provides names and concepts that map to the caller’s needs, not just to the internal design of the implementing system. May provide several ways of accomplishing the same thing, e.g. `list.first` and `list.last`, not just `list.get(0)` and `list.get(list.length - 1)`.
Feature Envy — a “code smell” when a chunk of code repeatedly references another object’s instance data. Indicates that the envying code may want to be extracted and moved to become a method on the envied object.
Feature Flag — switches a feature on or off based on configuration. Useful for A-B testing, or long-lived development paths (usually large refactorings or features), or validating features in staging before rollout to prod, or de/activating services during testing, etc.
Domain Modeling — designing objects and interactions that model business concepts as directly as possible — essentially the code equivalent of user-centered design
Ubiquitious Language — use the same terms with the same, precise definitions, across all layers of the product, from user interface to URLs to app code to database schemas