The Pragmatic Programmer

July 04, 2020 · 20 mins read By David Thomas, Andrew Hunt

Introduction

  • There is room for individuality, even in large software teams. Craftsmanship matters.
  • Continuous process: Kaizen. A Japanese word meaning “many small improvements over time”.

1. A Pragmatic Philosophy

1. It’s your life

  • You have agency. Software Engineering is one of the most flexible industries out there.
  • If you don’t like something, change it.

2. The cat ate my source code: taking responsibility

  • Team trust: your team needs to be able to trust and rely on you, and vice versa. Teams without trust are broken.
  • Take responsibility: taking responsibility is something you actively agree to. You commit to ensure that something is done right, even if you don’t have full control over it. You have the right to refuse to take responsibility.
  • When a bad outcome happens, admit your mistakes and provide options. Not excuses.

3. Software entropy (or rot)

  • Fix broken windows. They accelerate rot fast.
  • Don’t break windows, even when a crisis arises. Broken windows multiply fast.

4. Stone soup and boiled frogs

  • Stone soup story: be the catalyst for change. Start working on something then ask for more. People find it easier to join an ongoing success.
  • Keep an eye on the big picture. Projects fail one small problem at a time. Frog in boiling water analogy.

5. Good enough software

  • Good enough means: meeting basic standards of quality, and letting users be involved in the decision of what is good enough.
  • Involve users in the tradeoff. Make quality part of the requirements discussion.
  • Know when to stop: over-quality is not a good thing. The program can never be perfect.

6. Your knowledge portfolio

Think of your knowledge as an investment portfolio. Good investors:

  • Invest regularly: never stop learning. This is the most important one.
  • Diversify: learn many unrelated things, including non-technical subjects.
  • Manage risk: learn low-risk, low-reward tech (classical things) as well as high risk, high reward areas.
  • Buy low, sell high: learning an emerging tech can payoff handsomely.
  • Review and rebalance: regularly check what the state of the market is and adjust your learnings accordingly.

Pass everything you learn through a [[critical lens]]. Ask why, in what context, etc… this particular solution makes sense.

7. Communicate!

Note: this is a good section about [[communication]] in the workplace, a recommended read and re-read.

  • Know what you want to say. Structure before you communicate.
  • Know your audience and adapt the message.
  • Chose your moment.
  • Chose a style. Adapt it to your audience.
  • Involve your audience. E.g. with early drafts for feedback.
  • Be a listener: turn meetings into dialogs for example.
  • Get back to people.
  • Keep code and documentation together.
  • Looks matter.

2. A Pragmatic approach

8. The essence of good design

Good design is easier to change (ETC). All the rules, patterns etc are just a manifestation of this.

9. DRY: The evils of duplication

  • DRY - the long version:

    Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

  • DRY is not about code but knowledge. A change in code should only have to happen in 1 place.
  • Not all code duplication is knowledge duplication. There is such thing as accidental duplication.
  • Duplication in large systems: same code created more than once. Good communication between developers solves it.
  • Make things easy to reuse. If it’s not easy, people won’t do it.

10. Orthogonality

  • Term borrowed from geometry. 2 things are orthogonal if they can change independently.
  • Making orthogonal components: well defined and isolated. Change in one place doesn’t affect other places.
  • Orthogonal systems are less error-prone, easier to test. But it’s easy to introduce a lot of orthogonality in subtle ways.

11. Reversibility

  • Some decisions entail changes to the system that are hard to change or reverse.
  • Avoid creating such systems. Abstract away the components related to those decisions so they are easy to swap out later. Changes will come.
  • Also, don’t follow fads.

12. Tracer bullets

  • Tracer bullet: live bullet that leaves a trail to aid soliders aim in battle. Provides real time feedback under actual conditions.
  • In software: at the start of a complex project, a tracer bullet is a mini-feature that sets a skeleton for all parts of the system. It’s a live feature that:
    • shows users that work is going somewhere.
    • builds a skeleton for other devs to work on.
    • is integrated between all pieces of the system, end to end.
    • can be demoed for external stakeholders.
    • gives a feeling of progress: each individual development is smaller.
  • If the bullet doesn’t hit (missed requirements), it’s easier to correct course.
  • Different from prototyping. Prototyping is exploratory and gets discarded. Tracer bullets are meant to be kept onward.

13. Prototyping and post-it notes

  • A prototype is a way to try out anything uncertain: design, code, architecture…
  • You can ignore: correctness, completeness, robustness, style.
  • The value of the prototype is in the learning.
  • Prototypes are not meant to be kept around. Be very clear about that, because they tend to do just that. If the goal is to keep the result, go for a tracer bullet instead.

14. Domain languages

  • In some cases, it makes sense to program using the domain language: DSL.
  • DSLs can be internal (part of a programming language) or external (have to be parsed and executed by another piece of code)

15. Estimating

  • Communicate accuracy (“130 days” and “6 months” are felt differently).
  • Steps for a good estimate:
    • Understand what’s being asked.
    • Build a mental model of the system.
    • Break the model into components that have parameters.
    • Give parameters a value. Focus on the parameters that most affect the outcome.
    • Calculate the answer.
  • Keep track of your estimating performance for self feedback.
  • Estimating project schedules:
    • “Painting the missile”: give an optimistic, most likely and pessimistic estimate for each task. Then sum them up for a range of estimates for a the project.
    • Break down the project and gain experience to produce better estimates. The estimate is a living number evolving with the project.
  • When asked for an estimate, don’t give an answer without first taking the time to do a proper model. “I’ll get back to you “ is a good answer.

3. The basic tools

16. The power of plain text

Plain text is to prefer to binary formats when possible because:

  • Insurance against obsolescence.
  • Leverages existing tools.
  • Easier to test.

17. Shell games

Shells are more powerful than GUIs, be familiar with them and use them.

18. Power editing

Achieve editor fluency.

Note: the “challenge” part is a good guide for this.

19. Version control

Use it.

20. Debugging

  • Fix the problem, not the blame.
  • Don’t panic. Debugging is hard and you need a clean head.
  • Step 1 in debugging is reproducing the bug.
  • Write a failing test if you can. Going through 15 clicks for a reproduction is not good.
  • Read the error message!
  • Test it with the same data that crashed.
  • Do a binary search when the input is large.
  • Logging helps when you need to trace the steps that led to the bug.
  • Rubber ducking. (Note: this is the original rubber duck)
  • Assume the simplest hypothesis first (the OS is probably not broken).
  • A bug is an assumption gone wrong. Question and validate all your assumptions.
  • Is the bug a symptom? Look for the root cause. Are there other instances of this around?

21. Text manipulation

  • Text manipulation tools are versatile and should be a part of your toolbox.
  • Know one (perl, ruby, python, sed, awk…)

22. Engineering daybooks

Keeping track of what you’re doing helps with:

  • recall
  • giving a place to store ideas not immediately relevant
  • kind of rubber ducking
  • nostalgia when you re-read them

4. Pragmatic paranoia

You can’t write perfect software.

23. Design by contract

DBC is a design technique where any software system (method, app…) has a defined contract with preconditions, postconditions and invariants.

Contract:

If the preconditions are met, I guarantee the postconditions and the invariants

  • Powerful design technique that forces you to think.
  • Encourages failing early (checking preconditions) and not accepting invalid input.
  • Implementation:
    • Some languages have first-class support for DBC.
    • Other don’t: you can still design using DBC.
    • Assertions are a form of DBC.
  • “Semantic invariants” are invariants core to the business. They can strongly guide the software behavior.

24. Dead programs tell no lies

  • Crash early. Crashing is better than risking an invalid/incorrect output.
  • Don’t forget to clean up after yourself.

25. Assertive programming

In languages that provide assertions, use them to check for “impossible” things. Let them live in production too.

26. How to balance resources

When dealing with resource allocation in code, rules:

  • Finish what you start: the same code responsible for allocating a resource should be responsible for deallocating it. More tips here on how to allocate multiple independent resources, handle exceptions etc.. Good reference.

27. Don’t outrun your headlights

  • Take small steps. Don’t try to guess too long into the future.
  • Your feedback loop is your speed limit.

5. Bend, or Break

How to write code that is easy to change and make reversible decisions.

28. Decoupling

Decoupled code is easier to change. Examples of coupling in code (classic):

  • long method chains (fix: “Tell, don’t ask”)
    Note: don’t apply it blindly, know when to stop.
  • Avoid global data (including singletons)

29. Juggling the real world

Using events to react to the real world gives you flexibility. Different ways to handle events:

  • Finite state machine.
  • Observer pattern:
    • Pros: simple, battle tested
    • Cons: synchronous, introduces coupling
  • Pub/sub: add a pipe passing messages in the observer pattern. More decoupled.
  • Reactive programming: events as a collection of data. Manipulate streams like infinite collections (e.g. infinite arrays). Synchronous and asynchronous data has a common API.

30. Transforming programming

  • Thinking of programs as a series of transformations on data.
  • Data becomes a first class citizen.
  • Fits well in Functional Programming (examples here are in elixir)

31. The inheritance tax

  • Inheritance creates coupling. Avoid it.
  • Prefer interfaces, delegation or mixins.

32. Configuration

  • Configuration should be external to application code.
  • Static configuration vs configuration-as-a-service.

6. Concurrency

Concurrency: architecture. Parallelism: running at the same time.

33. Breaking temporal coupling

  • We are used to thinking linearly. 2 things are important when decoupling for time: concurrency and ordering.
  • Analyze your workflows with activity diagrams to find sources for optimization using concurrency. Then opportunities for parallelism emerge depending on resources.

34. Shared state is incorrect state

  • Intro to shared resource (memory, db, threads…).
  • Race conditions and solutions (semaphore/mutex).
    • warning: those abstractions are hard. Avoid using them if you can.
    • Other models for concurrency exist.

35. Actors and processes

  • Actor model uses concurrency without shared state.
  • Concurrency in the actor model is implicit. Erland is a good example.

36. Blackboards

  • Blackboard analogy for a highly async system architecture.
  • They resolve concurrency issues but have tradeoffs: the system is hard to design and reason about in its entirety.

7. While you are coding

37. Listen to your lizard brain

  • Your instincts are your brain subconsciously noticing patterns.
  • They are worth listening to. Take the time to listen and understand them.
  • See also [[tacit knowledge]]

38. Programming by coincidence

Programs that “accidentally work” are not reliable. Be sure to understand why things work and what assumptions are made, and test those assumptions.

39. Algorithm speed

  • Introduction to Big-O notation.
  • Estimate the order of your algorithm, test your estimates and know when it matters.

40. Refactoring

  • Metaphor for software: building construction -> [[gardening]].
  • Refactoring: small steps, don’t change external behavior. Like weeding and raking.
  • Refactor early and often, using a good test suite for support.

41. Test-to-code

  • On TDD and the values of unit tests to help during coding, not after.
  • Easy-to-test classes are less coupled.
  • TDD pros: guaranteed tests, always be thinking about tests, good to start.
  • TDD cons: you can overdo it (focusing on test coverage, redundant tests for example).
  • With TDD: You need to know where you ‘re going.
  • Unit testing / test against contract / test in production (aka diagnostics).

Test your software, or your users will!

42. Property based testing

  • Testing properties without manually specifying input.
  • Can test for edge cases the programmer didn’t think about, the failures are often surprising.
  • Makes you think of your code as invariants and contracts.

43. Stay safe out there

On software security. Basic principles:

  • Minimize attack surface area
  • Principle of least privilege
  • Secure defaults
  • Encrypt sensitive data
  • Maintain security updates

44. Naming things

  • Naming signifies intent. Things should be named accordingly to the role and motivation behind them.
  • Rename when needed.
  • Name things the same way your organization does.

8. Before the project

45. The requirements pit

No one knows exactly what they want.

  • Historical reason of waterfall approach: computers were more expensive than programmers. To use them efficiently: we designed everything first.
  • “Programmers help people understand what they want.”
  • Requirements are learned in a [[feedback loop]].
  • Work with a user to think like a user. “Internship” concept.
  • “Policy is metadata”. Everything that is policy should be easy to change, implement it as a general system. Giving the client a large technical document is like giving the average developer a copy of the Iliad in greek and ask them to code the video game for it.

46. Solving impossible puzzles

  • For hard tasks, it’s important to distinguish between hard constraints and self imposed ones.

    Don’t think outside the box, find the box.

  • Enumerate all possibilities, then dismiss the invalid ones from [[first principles]].
  • When stuck: do something else for a while, or rubber duck.

47. Working together

Pair and mob programming as ways of collaboration.

48. The essence of agility

  • Agile is not a noun, it’s how you do things.
  • Agility is about responding to change.
  • Where are you -> smallest meaningful step to where you want to be -> evaluate result and repeat.
  • At all levels: from projects to code.

9. Pragmatic projects

49. Pragmatic teams

  • Pragmatic team: small and stable. People know, trust and depend on each other.
  • Schedule it to make it happen:
    • Maintenance
    • Process reflection
    • Learning
  • Communicate as a team to the exterior world. Tip: have a brand.
  • Organize fully functional teams to avoid gates and handoffs.
  • Know when to stop adding features.

50. Coconuts don’t cut it

  • Cargo cult: “do what works, not what’s fashionable”.
  • Try it, keep the good parts and discard the bad ones.
  • The real goal is instant delivery when users need it.
  • Try various methodologies and keep experimenting.

51. Pragmatic starter kit

3 pillars:

  • Version control drives the project: builds, CI, deploys etc. are driven by version control.
  • Regression testing at all levels: unit, integration, performance etc..
  • Full automation: don’t rely on manual procedures. Automate every task that’s important to the project.

52. Delight your users

Our goal is to delight our users by solving their problem. Getting to the root of their issues is crucial here. A question to guide the reflection:

How will you know that we’ve all been successful a month (or a year, or whatever) after this project is done?

The goal is not to ship software, but to solve problems.

53. Pride and prejudice

You should be proud of what you ship. Sign your work.

Postface

On software and ethics. 2 rules:

  • First, Do No Harm
  • Don’t Enable Scumbags