Book by Kent Beck
- 1. Guard Classes
- 2. Dead code
- 3. Normalize symmetries
- 4. New Interface, Old implementation
- 5. Reading Order
- 6. Cohesion Order
- 7. Move Declaration and Initialization Together
- 8. Explaining variables
- 9. Explaining constants
- 10. Explicit parameters
- 11. Chunk statements
- 12. Extract helper
- 13. One pile
- 14. Explaining comments
- 15. Delete redundant comments
- 16. Separate Tidying
- 17. Chaining
- 17. Chaining
- 18. Batch Sizes
- 19. Rhythm
- 20. Getting Untangled
- 21. First, After, Later, Never
- 22. Beneficially Relating Elements
- 23. Structure and behavior
If you see code like:
if condition: ...
or
if condition:
if another condition: ...
tidy the above to:
if not condition: return
if not another condition: return
...
Exit immediately, it is easier to read -- before we get into the details, there are some preconditions we need to bear in mind.
Delete it. If you need it later, use version control. Delete only a little code in each tidying diff. Just in case, if it turns out that you were wrong, it will be easy to rever the change.
Tidy forms of unnecessary variations. Use common style for your functions. Things get confusing when two or more patterns are used interchangeably.
If some interface you need to use is very difficult to use, implement the interface you wish you could call and call it. Implement the interface by simply calling the old one.
Reorder the code in the file in the order in which a reader would prefer to encounter it.
If 2 functions are coupled, put them next to each other, if 2 files are coupled, put them in the same directory, ...
If you know how to eliminate coupling, go for it.
It is easier to understand the code if each of the variables is declared and initialized just before it's used. It is hard to read when declaration is separated from initialization.
When you understand a part of a big, hairy expression, extract the subexpression into a variable named after the intention of the expression.
Always separate the tidying commit from the behaviour change commit.
Create a symbolic constant. Replace uses of the literal constant with the symbol.
It's common to see blocks of parameters passed in a map. This makes it hard to read and understand what data it required. Make the parameters explicit:
foo(params) -> foo(a, b)
The simplest tidying. Put a blank line between 2 parts doing different things. After you've chunked statements, you have many paths forward: Explaining Variables, Extract Helper or Explaining Comments.
A block of code that has an obvious purpose and limited interaction with the rest of the code can be extracted into a helper function. Using the helper can be taken care of in another tidying.
Sometimes you read the code that has been split into many tine pieces, which makes it hard to understand. The biggest cost of code is the cost of reading it, not the cost of writing it.
Sometimes in order to regain the clarity, the code must be merged together, so new, easier-to-understand parts can be extracted.
Write down only what wasn't obvious from the code. Put yourself in the place of the future reader, or yourself 15 minutes ago.
Immediately upon finding a defect is a good time to comment. It is much better to add the comment that points out the issue, rather than leaving it buried in the sand.
When you see a comment that says exactly what the code says, remove it.
Tidying should go into their own separate PRs, with as few tidyings per PR as possible. Behavior and structure changes should be in separate PRs.
Tidying can set up another tidyings. You will begin to flow tidyings together to achieve larger changes to the structure of your code. Be wary of changing too much, too fast. A failed tidying is expensive relative to the cost of a series of successful tidyings.
The more tidyings per batch, the longer the delay before integrating, and the greater the chance that a tidying collides with someone else is doing.
The change of a batch accidentally changing behavior rises with the number of tidyings in the batch.
The more tidyings per batch, the more we are prone to tidying just because, with all the additional costs that creates.
In many orgs, the fixed cost of getting a single change through review and deployment is substantial. Programmers feel this cost, so they move right in the trade-off space (despite collisions, interactions, ...).
More than an hour of tidying at a time before making a behavioral change likely means you've lost track of the minimum set of structure changes needed to enable your desired behavior change.
Tidying is a minutes-to-an-hour kind of activity. Sometimes it may take longer, but not for long.
Tidying leads to more and more tidying. What to do? 3 options:
- Ship as it is [very impolite, prone to errors, but quick]
- Untangle the tidyings into separate PRs [more polite, but may require a lot of work]
- Start over, tidying first [more work, but leaves a coherent chain of commits]
Re-implementation raises the possibility that you will see something new as you re-implement, letting you squeeze more value out of the same set of behavioral changes.
Never
- you are never changing this code again
- there is nothing to learn by improving the design
Later
- you have a big batch of tidying to do without immediate payoff
- there is eventual payoff for completing the tidying
- you can tidy in little batches
After
- waiting until next time to tidy first will be more expensive
- you won't feel a sense of completion if you don't tidy after
First
- it will pay off immediately, either in improved comprehension or in cheaper behavior changes
- you know what to tidy and how
Software design is beneficially relating elements.
Elements: Tokens -> Expressions -> Statements -> FUnctions -> Objects/modules -> Systems. Elements have boundaries.
Relating: In software design we have a handful of relations like:
- invokes
- publishes
- listens
- refers
Beneficially relating elements. Software designers can only:
- Create and delete elements
- Create and delete relationships
- Increase the benefit of a relationship
caller()
return box.width() + box.height()
This function has 2 relationships with the box. This relationship can be adjusted. we can have box.area()
.
caller()
return box.area()
The benefit is that it is simpler and the cost is that box
has additional method.
Software creates value in two ways:
- what it does today
- the possibility of new things we can make it do tomorrow
Behavior creates value. Rather than having to calculate a bunch of numbers by hand, the computer can calculate millions of the every second. If running software costs $1, you can charge folks $10 to run it on their behalf, then you have a business.
The structure creates options. The structure could make it easy to add new features to our system, or it could make it hard.
- A dollar today is worth more than a dollar tomorrow, so earn sooner and spend later
- you can't spend it so it's worthless
- you can't invest it
- there's some chance that you won't get the dollar
- in the scope of this book: the time value of money encourages tidy after over tidy first
- In a chaotic situation, options are better than things, so create options in the face of uncertainty
Software design has to reconcile the imperatives of "earn sooner/spend later" abd "create options, not things".