Code That Fits in Your Head Heuristics for Software Engineering
Summary
The book focusses on two areas, that are mixed up in the content:
- How to keep code simple and easy to comprehend?
Starting from the things that we know about how our brain works, what does simple mean and how can we see and
improve on that?
- How to get away from the shifting sands of individual experience?
We software developers are dominated by our own individual experiences heavily. That hinders team work. So what
practices can we lend and modify from engineering to achieve more conformity?
Chapter 1: Art or Science?
The author lists different analogies that are usually used when trying to explain what software development is,
like: Building a house, growing a garden, crafting and more.
He shows that, while all of them contain some truth,
none fits right. But for the moment software engineering seems right for him and he wants to
contribute something
in this direction.
Chapter 2: Checklists
- Use checklists
They are easy to create and can help everybody on the team. In a lot of different complex jobs they
already help a lot.
Chapter 3: Tackling complexity
Software engineering is about tackling complexity. The brain works in certain ways, e.g. we can only keep track of
7 things in our short term memory. Our code needs to respect that.
Chapter 4: Vertical Slice
Instead of working your way from the UI down to the database or the other way round, consider creating a vertical slice.
A vertical slice contains everything from the ui down to the database. It means that you only implement one feature, but through your complete architecture.
This has advantages:
- You create working software
You can already get user feedback on it. Maybe the users can even already use it for a certain part of their work?
- You can already test drive your CI/CD pipelines
... and they start small, so it should be easier to create them early.
This vertical slice can also be described by the name of "walking skeleton" (external explanation).
Then use outside-in test-driven development:
- start with a characterization test
This is a high level test that describes a functionality.
- From there on write more tests and code to complete the functionality
... , working your way inward on the details.
Other recommendations are:
- Test code should be in balance
When you turn the code 90 degrees and then point to the middle you should
point to the act section (this essentially means that the arrange and assert blocks should have about equal
length).
- Documentation should prioritise explaining why a descision was made
..., rather than what was decided.
- Use value objects
This makes the Domain Model easier and easier to test (Example given on Page 71). Since they are not complicated and often also fully implemented by a tool, the error probability is very low. Thus you do not need to write tests for them.
- Commit database schema to the Git repository.
- Remove internal logic from classes that interact with subsystems to the utmost extend so you do not have much that could fail.
Classes that interact with subsystems are not easily tested.
Chapter 5: Encapsulation
- Encapsulation
is about being able to trust an object to behave in a certain way. So it is good to reduce complexity.
- Write production code as answer to drivers
like tests or analyzers.
- Keep the time your code is invalid as short as possible.
Concept: Transformation Priority Premise
- Postel's law
"Be conservative in what you send, be liberal in what you accept."
- "The most important notion is that an object should guarantee that it'll never be in an invalid state."
(p.108)
- "The less you ask of the caller, the easier it is for the caller to interact with the object. The better
guaranties you give, the less defensive code the caller has to write."
(p.109)
Chapter 6: Triangulation
- Do not count on your long term memory
Our long term memory it is not very valuable in a software development
organization, neither for the long term value of developers nor for the company.
The developers collect information on legacy code. While this makes them
hard to replace for one company it also renders them not valuable for any other
organisation and ultimatly restricts their personal development.
The company has increased bus factor problems, because the developers on a project
are not easily replaced. This also restricts scaling, keeping the software operational,
and might be a kill switch, if the company is to dependent on the software.
- Short development bursts
While coding, when you see things that you want to change, make a list and keep to the change that you are
doing at the moment. Do not get distracted.
- Use the devil's advocate for writing unit tests
It is a technique where you continue to try to implement a clearly wrong solution that still makes all tests
pass. Then you try to be more specific in your tests and continue this loop until your tests are so good
that they force you to implement the right thing.
Chapter 7: Decomposition
- Continuously watch your code complexity
Code increases in complexity over time and it is a bad habit to leave it unchecked.
- Cyclometic complexity
could be monitored by a build system. (Counts the number of branching and looping
instructions). You should strive for less than 7.
- 80x24 rule
based on the constraints of early terminals, the code of a method should fit these constraints
- Parse, don't validate
Using the TryParse-Methods in the .Net library reduces the complexity
- Fractal architecture
At every level your architecture should not exceed a maximum of 7 elements.
This will span a tree and still be within some bounds to be comprehensible by your brains constraint.
-
Variable count
The number of variables and parameters your method uses should be kept below or equal to 7.
Chapter 8: API design
- Dot-Driven Development
This means supporting Intellisense so that users can quickly find what they are looking for.
- Poka-Yoke
Fool-proof your api by making mistakes impossible by design.
Compiler-errors are preferable to runtime errors.
- Remember to write for readers
- Favor well-named code over comments
- Can you create self-explaining signatures?
Try to x-out names of methods and then try to infer the name from the signature.
You may be able to shorten the name since you do not have to say what the types already state.
- Use Command-Query-Separation
Methods that return data should not have side effects.
Methods with side effects should not return data.
- Use the Hierarchy of Communication
- give APIs distinct types
- give methods helpful names
- write good comments
- provide illustrative examples as automated tests
- write helpful commit messages in git
- write good documentation
Prefer the methods with the lower number, when having a choice.
Chapter 9: Teamwork
Teamwork is one of the most engineering-like practices you can adopt.
- Use the 50/72-rule for git commit messages
- write a summary in the imperative no wider than 50 characters
- if you add more text, leave the second line blank
- you can add as much text as you'd like, but format it so that it's no wider than 72 characters
You can then use git log --oneline
to get a summary of the log while preserving detail.
- Use continuous integration
(merge at average every 4 hours, if your change
requires more time, hide it behind
a feature flag and merge anyway)
- small commits enhance manouverability
- collective code ownership
A stable team does not exist, how many people can exit your team before the
operation stops?
- pair programming
comes with a real time code review and
an informal approval process. It removes the time between
the development and the review effectivly countering the negative
effects of late or no reviews.
- mob programming
Is effective in small groups, e.g. for coaching scenarios.
Remember: Productivity is unrelated to typing speed.
Code Reviews
- Review early
The longer the time between the coding and the code review the more time it will take because the
developer forgets.
- Review
Without reviews the probability of bugs in production increases, which are sometimes only found a long time
after publishing,
and then the dev will not remember his own code so fixing the bug will take even longer.
- Reviews should be performed in at least a daily rhythm
- Reject big changesets
- Ask yourself: "Will I be ok maintaining this?"
- Driven by the reviewer
code reviews should be driven by the reviewer, the developer is biased and is not in the position to tell
if the code is readable.
- Good questions for a code review
- Does the code work as intended?
- Is the intent clear?
- Is there needless duplication?
- Could existing code have solved this?
- Could this be simpler?
- Are the tests comprehensive and clear?
- Absent author
The author should be absent during the code reading,
the reviewer takes notes about what the people go
into dialog later.
- No privilege
Code reviews should be conducted by all team members,
it is not a privilege for a selected few.
Pull Requests
- Make each pull request as small as possible.
That's smaller than you think.
- Do only one thing in each pull request.
If you want to do multiple things, put them in separate pull
requests.
- Avoid reformatting
unless it is the only thing that you do in your pull request.
- Make sure the code builds
- Make sure all tests pass
- Add tests of new behaviour
- Write proper commit messages
Part 2 Sustainability
Chapter 10 - Augmenting Code
- Split up big changes into little changes
For any significant change, don't make it in-place, make it side-by-side.
- Use feature flags
Feature flags can easily be used when you introduce a new functionality
- Strangler-pattern on method level
- do not start by modifying the interface rather by implementing & testing on a concrete object
- or do change the interface but only implement the new method in one concrete class - as long as you
have no other consumers than tests
- Strangler-pattern on class level
create a new class and move the implementation over step
by step
- Versioning
- take advice from the semantic versioning specification to learn about breaking and nonbreaking changes
- warn users of your api when you have breaking changes
Chapter 11 - Editing Unit Tests
-
Only edit either test or production code
When you edit the test code you have the risk that your test code breaks. This improves the chances to get it right.
- Deleting assertions and tests weakens the integrity of the system.
- Automated refactorings are safe most of the time.
- Use the stash to get around merge problems at inappropriate times
git stash
: put all your dirty files in a hidden commit
git stash pop
: reapply the stashed changes to HEAD as if they are new changes
Chapter 12 - Troubleshooting
What to do when your code does not work?
- The lowest level is programming by coincidence
Throw code at a problem, look what sticks...
This is not recommended.
- The Scientific Method
- make a hypothesis
- perform the experiment
- compare outcome to prediciton repeat until you understand what is going on.
- Simplify code
- Rubber Ducking
explain the problem to a collegue or a rubber duck.
You can do this in writing to, e.g. as an email. At the end you can decide if you still need to send it.
- Address defects early
Do not schedule solving known defects for later, as they probably will cause secondary and
tertiary effects that will create additional work
- Reproduce defects as tests
If a defect occurs once it has already proven that it can occur. Creating an automated test
that reproduces the problem will help you not only fixing it but also protect you against a second occurence.
- Do not be afraid of slow tests
Use slow tests, but when they take more than 10 seconds let them be run by a second system, maybe your build system.
- Reproduce nondeterministic defects with loops
Nondeterministic defects e.g. race conditions cannot be 100% tested. But you can
create tests that make the problem probable and then loop over them for a few minutes.
If you get away with a green flag you then can be relativly certain that you addressed the
problem.
- Use Bisection
- Find a way to detect or reproduce the problem
- remove half of the code
- If the problem is still present, repeat from step 2. If the problem goes away, restore the code you removed, and remove the other half. Again repeat from step 2.
- Keep going until you've whittled down the code that reproduces the problem to a size so small that you
understand what is going on.
- Use Bisection using GIT
Walk back in the history to identify the commit that introduced the bug; git has a git bisect
command to explicitly help with this interactivly using a search algorithm.
Chapter 13 - Separation of Concerns
- Nesting is bad
Hidden side effects can easily make your code less comprehensible.
- Sequencial combination is good
- Pure functions reduce the complexity
Pure functions are only dependent on their parameters, not external side-effects.
This also makes them very testable.
- Keep side effects close to the edges of the system
Keep nondeterministric queries and behaviour with the side effects close to the edges of the system and write
complex logic as pure functions.
- Use decorators for cross cutting concerns
(e.g. Logging, Performance Monitoring, Auditing, Metering, Instrumentation, Caching, Fault Tolerance
and Security)
- Log all impure actions, but not more
You log the right amount of information when you can repeat the execution.
- Try "functional core, imperative shell" as coding style
Chapter 14 - Rhythm
Personal rhythm:
- Use boxes of time
- Do breaks
- Break out of the flow
In the state of flow you can easily waste hours, because you do not have enough capacity left to
know if you are working on the right things. Remember that intellectual work is not about the hours
you put in. Use time deliberatly!
- Learn touch typing
so you can look at the screen while typing and can use all the features modern IDEs give you
Team Rhythm:
- Update dependencies regularily
... so you do not get stuck on old library versions (recommended once per month)
- Schedule maintenance tasks
Chapter 15 - The Usual Suspects
- Correctness is more important than performance
If the program does not need to work correctly, you can always make it run faster.
Correctness should be the center of your attention.
- If you need to work on performance, then measure it.
You cannot work on performance without measuring it!
- Use the STRIDE acronym to think about security
Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege
- Try property-based testing
Mark a test as [Property] and running it using FsCheck. FsCheck auto-generates the parameters for the test method
It is like [TestCase], just random.
- Use Behavioural Code Analysis
reasoning over your git log, which files seem to be involved in most changes, which
"cause the most trouble"? (This is an own research topic).
Chapter 16
- Put all files in one folder
"When you put a file in one folder you cannot put it into another folder at the same time."
- Use IDE features to navigate
Instead of a folder structure
- Avoid cyclic dependencies in your code