The Pragmatic Programmer (TPP) Notes
About#
These are my notes on The Pragmatic Programmer by David Thomas and Andrew Hunt. The book’s website can be found here.
Preface#
We who cut mere stones must always be envisioning cathedrals.
TPP
-
What makes a pragmatic programmer?
- Early adopter/fast adopter: try out new technologies & techniques, learn them quickly
- Inquisitive: ask lots of questions, be continuously learning
- Critical thinker: think from first principles
- Realistic: understand the underlying nature of each problem you work on
- Jack of all trades: be familiar with a broad range of technologies & environments
- Care about your craft: care about developing software well
- Think! About your work: never run on auto-pilot, always be critiquing your work in real time
-
Always be making continuous, small improvements (kaizen)
- Every day, work to refine the skills you have and to add new tools to your repertoire
Table of Contents
Chapter 1: A Pragmatic Philosophy#
It’s Your Life#
- You Have Agency
- It’s your life
- You have the ability to change or improve your work environment & your skills
The Cat Ate My Source Code#
-
Your team needs to be able to trust and rely upon you
-
Be eager to take on responsibility
-
Before approaching someone with a problem, stop and listen to yourself
- Talk to your rubber duck
- Run through the conversation in your mind. What is the other person likely to say?
- “Have you tried this…”
- “Didn’t you consider that…”
- Provide solutions or options, not excuses
-
When you say “I don’t know”, follow it up with “- but I’ll find out”
Software Entropy#
-
Software rot / technical debt can infiltrate even the most well-planned and well-staffed projects
-
A single broken window, left unrepaired for any substantial length of time, instills in the inhabitants of the building a sense of abandonment
- So more windows get broken, graffiti appears, serious structural damage occurs
- Clean, functional systems can deteriorate quickly once windows start breaking
-
Don’t leave “broken windows” (bad designs, wrong designs, or poor code) unrepaired
- Fix each one as soon as it is discovered
-
Help strengthen your team by surveying your projects
- Find broken windows and discuss with your teammates what the problems are and how to fix them
- Take responsibility for fixing the technical debt problems that no one else wants to take care of
Stone Soup and Boiled Frogs#
-
When you are at the onset of a large project, presenting the whole thing to others will be met with delays and blank stares
- Everyone will guard their own resources - “start-up fatigue”
- Work out what you can reasonably ask for & develop it well
- Once you’ve got it, show people and let them marvel
- People find it easier to join an ongoing success
-
Don’t be a frog in slowly-heating water
- Keep an eye on the big picture
- Constantly review what’s happening around you, not just what you personally are doing
-
Get in the habit of really looking at and noticing your surroundings
- Then do the same for your project
Good-Enough Software#
- Most users would rather use software with some rough edges today rather than wait a year for the shiny, bells-and-whistles version
- Great software today is preferable to the fantasy of perfect software tomorrow
- Plus, if you give your users something to play with early, their feedback will often lead you to a better eventual solution
Your Knowledge Portfolio#
-
Your knowledge and experience are your most important day-to-day professional assets
- Your ability to learn new things is your most important strategic asset
-
Building your portfolio
- Invest regularly: build a consistent habit of always learning, even if it’s a small amount at a time
- Diversify: the more different things you know, the more valuable you are + the better you will adjust to changing technologies
- Manage risk: don’t put all your technical eggs in one basket
- Buy low, sell high: learning an emerging technology before it becomes popular can be risky, but can also have a high payoff
- Review and rebalance: the industry changes fast, so you need to constantly be re-evaluating priorities
Goals for Learning#
- Learn at least one new language every year
- Read a technical book each month
- Read nontechnical books, too
- Take classes
- Participate in local user groups, meetups, and conferences
- Experiment with different environments
- Stay current with technology news
Communicate!#
-
Know your audience
- Understand the needs, interests, and capabilities of your audience
-
Know what you want to say
- Plan what you want to say, often by writing an outline
- Then ask yourself, “Does this communicate what I want to express to my audience in a way that works for them?”
-
Choose your moment
- Work out what your audience’s priorities are at any given moment
- Make what you’re saying relevant in time, as well as in content
-
Choose your style
- Adjust the style of your delivery to suit your audience, e.g. “just the facts” or a long wide-ranging chat
-
Involve your audience
- Involve your readers with early drafts/ideas - get their feedback and pick their brains
-
Get back to people
- Always respond to emails or questions, even if the response is simply “I’ll get back to you later” or “I don’t know, but I’ll find out”
-
When writing documentation, focus on why something is done - its purpose and its goal
- The code already shows how it is done, so commenting on this is redundant (and a violation of the DRY principle)
Chapter 2: A Pragmatic Approach#
The Essence of Good Design#
-
Good design is easier to change than bad design
- A thing is well designed if it adapts to the people who use it
- Easier To Change (ETC) is good
- Decoupling is good because by isolating concerns we make each one easier to change
- The single responsibility principle is useful because a change in requirements is mirrored by a change in just one module
- Naming is important because good names make code easier to read and therefore change
-
When you save a file, write a test, fix a bug, etc. – ask yourself “did the thing I just did make the overall system easier or harder to change?”
- When you’re not sure of the answer, try to make what you write easy to understand and replaceable in the future
DRY - The Evils of Duplication#
-
The DRY Principle: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system
-
Common DRY violations include:
- Duplication in code (when the knowledge/intent is the same)
- Duplication in documentation (don’t add comments which just restate exactly what the code does)
- Data definitions with redundant fields or fields that can be calculated dynamically based on the values of the other fields
- In this case, you may choose the violate the DRY Principle for performance reasons
-
Meyer’s Uniform Access Principle
All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation.
— Bertrand Meyer
Orthogonality#
-
Two or more things are orthogonal if changes in one do not affect any of the others
- For example, an application’s database code should be orthogonal to the user interface – you should be able to swap one out without affecting the other
-
We want to design components that are self-contained, independent, and with a single, well-defined purpose
- When components are isolated from one another, you know that you can change one without having to worry about the rest (as long as you don’t change that component’s external interfaces)
-
During coding, there are several techniques you can use to maintain orthogonality:
- Keep your code decoupled: modules shouldn’t reveal anything unnecessary to other modules and shouldn’t rely on other modules’ implementations, only interfaces
- Avoid global data: every time your code references global data, it ties itself into the other components that share that data
- Avoid similar functions: abstract code when possible
Reversibility#
- We should avoid making critical, irreversible decisions as much as possible, since we often don’t make the best decisions the first time around
- For example, hide third-party APIs behind your own abstraction layers so they can be swapped out as needed
Tracer Bullets#
-
When building a new system with lots of unknowns, the classic response is to specify the system to death and produce reams of paper itemizing every requirement
- This equates to: one big calculation up front, then shoot and hope
-
Instead, we should use tracer bullets
- Look for something that gets us from a requirement to some aspect of the final system quickly, visibly, and repeatably
- You don’t have to write fully-functional code to test assumptions and make sure a system design will work
- You can gather feedback and change your approach early
-
Prototyping generates disposable code
-
Tracer code is lean but complete, and forms part of the skeleton of the final system
Prototypes and Post-it Notes#
- What sorts of things might you choose to investigate with a prototype? Anything that carries risk, hasn’t been tried before, or that is absolutely critical to the final system
- Architecture
- New functionality in an existing system
- Structure or contents of external data
- Third-party tools or components
- Performance issues
- User interface design
Domain Languages#
- Ansible is a tool that configures software, typically on a bunch of remote servers, by reading a YAML specification that you provide
- name: install nginx
apt: name=nginx state=latest
- name: ensure nginx is running (and enable it at boot)
service: name=nginx state=started enabled=yes
- name: write the nginx config file
template: src=template/nginx.conf.j2 dest=/etc/nginx/nginx.conf
notify:
- restart nginx
Estimating#
-
Tips to creating estimates of project complexity/time needed
- Ask others who’ve already done similar things and draw on their experiences
- Have a clear grasp on what’s being asked/the scope of the project
- Build a mental model of the system, looking for any details that have to be taken into account
- Break the model into components (sub-estimates)
- Work out which parameters are critical for each component
-
Keep track of your estimates so you can evaluate their quality ex-post
-
What to say when asked for an estimate: “I’ll get back to you”
- Slow the process down and think through the steps carefully
Chapter 3: The Basic Tools#
- Expect to add to your toolbox regularly
- Always be on the lookout for better ways of doing things
The Power of Plain Text#
- Unix is famous for being designed around the philosophy of small, sharp tools, each intended to do one thing well
Shell Games#
- If you do all your work using GUIs, you are missing out on the full capabilities of your environment
- Become familiar with your shell to help automate common tasks or use the full power of the tools available to you
Power Editing#
-
Text is the basic raw material of programming, so it follows that you need to be able to manipulate text as efficiently as possible
-
Every time you find yourself doing something repetitive, find the better way to do it and burn it into your muscle memory via repetition
Version Control#
- Store everything that defines the configuration and usage of your machine (SSH keys, user preferences and dotfiles, editor configuration, shell setup, installed applications, list of software installed using Homebrew, Ansible script used to configure apps, all current projects, etc.) in version control
Debugging#
-
Beware of myopia when debugging
- Resist the urge to fix just the symptoms you see
- Always try to discover the root cause of a problem, not just this particular appearance of it
-
Your own manual testing doesn’t exercise enough of an application
- You must brutally test both boundary conditions and realistic end-user usage patterns - you need to do this systematically
- You may need to actually observe users to get all the context you need
Debugging Strategies#
-
The best way to start fixing a bug is to reproduce it, ideally through a deterministic series of known commands/actions
-
Read error messages and think carefully about them
-
When your application is sensitive to particular input values (and you’re not sure which) or you’re seeing regression across releases (but you’re not sure which release is the cause), use binary search and systematically narrow down the source of the bug(s)
-
Use logging and/or tracing to drill down into the code
-
“Rubber ducking” can be useful to find causes of problems - just explain your problem to someone else
Text Manipulation#
- Keeping things in plain text can have a lot of benefits when you use tools that allow you to manipulate text easily
Engineering Daybooks#
- Keeping thorough notes on ideas or TODOs throughout the day can be helpful
- When you stop to write something down, your brain switches gears and gives you a chance to reflect (rubber duck effect)
Chapter 4: Pragmatic Paranoia#
- We are constantly interfacing with other people’s code - code that might not live up to our high standards - and dealing with inputs that may or may not be valid
- So we are taught to code defensively: validate all information we’re given, use assertions to detect bad data, distrust data from potential attackers
- Pragmatic Programmers take this a step further: they don’t trust themselves, since no one writes perfect code
Design by Contract#
-
We should create contracts documenting and agreeing to the rights and responsibilities of software modules, in order to ensure program correctness
-
Expectations and claims in a contract include:
- Preconditions: what must be true in order for the routine to be called
- Postconditions: what the routine is guaranteed to do; the state of the world when the routine is done
- Class Invariants: any invariants that must be true by the time control returns to the caller
If all the routine’s preconditions are met by the caller, the routine shall guarantee that all postconditions and invariants will be true when it completes.
— TPP
Dead Programs Tell No Lies#
-
One of the benefits of detecting problems as soon as you can is that you can crash earlier, instead of continue with a corrupted program
-
A dead program normally does a lot less damage than a crippled one
Assertive Programming#
- Assertions can be useful checks for “things that could never happen” or on an algorithm’s operation, but they shouldn’t be used in place of real error handling
How to Balance Resources#
-
When working with resources with limited availability (memory, transactions, threads, network connections, files, timers), finish what you start
- The function or object that allocates a resources should be responsible for deallocating it
-
Deallocate resources in the opposite order to that in which you allocate them
- That way, you won’t orphan resources if one resource contains references to another
-
When allocating the same set of resources in different places in your code, always allocate them in the same order
- This will reduce the possibility of deadlock
Don’t Outrun Your Headlights#
-
Always take small, deliberate steps, checking for feedback and adjusting before proceeding
-
Feedback is anything that independently confirms or disproves your action. For example:
- Results in a REPL provide feedback on your understanding of APIs and algorithms
- Unit tests provide feedback on your last code change
- User demo and conversation provide feedback on features and usability
-
Instead of wasting effort designing for an uncertain future, you can always fall back on designing your code to be replaceable
Chapter 5: Bend, or Break#
Decoupling#
-
Coupling is the enemy of change, because it links together things that must change in parallel
-
Some of the symptoms of coupling:
- Wacky dependencies between unrelated modules or libraries
- “Simple” changes to one module that propagate through unrelated modules or break stuff elsewhere in the system
-
Chaining method calls can couple components if the code traverses multiple levels of abstraction, resulting in a lot of implicit knowledge
-
The Law of Demeter (LoD)
- A method defined in a class C should only call:
- Other instance methods in C
- Its parameters
- Methods in objects that it creates, both on the stack and in the heap
- Global variables (probably shouldn’t exist)
- A method defined in a class C should only call:
-
Don’t chain method calls when possible - don’t access field-of-field (thanks Ben Lerner!)
-
Globally accessible data creates coupling by impacting every component in the system that relies on those globals
-
Keep your code shy; have it only deal with things it directly knows about
Juggling the Real World#
-
An event represents the availability of information: a user clicks a button, the result of a calculation is ready, etc.
- If we write applications that respond to events, and adjust what they do based on those events, those applications will work better in the real world
-
Finite State Machines (FSMs)
- A state machine is just a specification of how to handle events. It consists of a set of states, one of which is the current state.
- For each state, we list the events that are significant to that state. For each of those events, we define the new state of the system.
- A pure FSM is an event stream parser - its only output is the final state. We can also add actions that are triggered on certain transitions.
-
In the observer pattern, we have a source of events, called the observable, and a list of clients, the observers, who are interested in those events
- When an event occurs, the observable iterates down its list of observers and calls the function that each one passed it
- The event is given as a parameter to that call
-
The observer pattern has a problem: because each of the observers has to register with the observable, it introduces coupling
- And because in the typical implementation the callbacks are handled inline/synchronously by the observable, it can introduce performance bottlenecks
-
Publish/Subscribe (pubsub) generalizes the observer pattern, solving the problems of coupling and performance
- We have publishers and subscribers which are connected via channels
- Every channel has a name. Subscribers register interest in one or more of these named channels, and publishers write events to them
- The communication between publisher and subscriber will likely be asynchronous
-
Reactive Programming, Streams, and Events
- Frontend/browser frameworks such as React and Vue.js allow for data-level reactivity where events or changes in values are used to trigger other events or changes in other values
- Streams let us treat events as if they were a collection of data
Transforming Programming#
-
All programs transform data, converting an input into an output
-
For example, let’s say we wanted to write a program that lists the five longest (by number of lines) files in a directory free
- We could use the below command, which is just a series of transformations
find . -type f | xargs wc -l | sort -n | tail -6 | head -5
-
Sometimes the easiest way to find the transformations you need is with a top-down approach: start with the requirement, determine its inputs and outputs, then find steps that lead you from input to output
- The pipe operator, like
| >
in OCaml, is useful for thinking in terms of transforming data
- The pipe operator, like
-
A common reflex in object-oriented programming is to hide data by encapsulating it inside objects
- These objects then chatter back and forth, changing each other’s state
- This introduces a lot of coupling, and it is a big reason that OO systems can be hard to change
-
Don’t Hoard State; Pass it Around
- In the transformational model, data becomes a peer to functionality - a pipeline is a sequence of
code -> data -> code -> data ...
- The data represents the entire, unfolding progress of our application as it transforms its inputs into its outputs
- In the transformational model, data becomes a peer to functionality - a pipeline is a sequence of
Inheritance Tax#
-
When inheritance first appeared in Simula 67 in 1969, the idea was to prepend the instance data and implementation of the parent class to the subclass
- The parent class was viewed as being a container that carried around its subclasses
- This introduced polymorphism as a way of combining types
- This pattern continued in languages like C++ and Java
-
In Smalltalk, inheritance was a dynamic organization of behaviors
- This pattern continued in languages like Ruby and JavaScript
-
Inheritance is coupling, since a class is coupled to its ancestors, its descendants, and all of the code that uses it
-
The alternatives to inheritance are much better:
- Interfaces and Protocols
- Delegation
- Mixins and Traits
-
Interfaces and Protocols
- In an OO language, you can specify that a class implements one or more sets of behaviors (interfaces)
- Any class that implements the appropriate interface will be compatible with that type, so we get polymorphism without inheritance
-
Delegation
- With inheritance, classes can have large numbers of methods
- If a parent class has 20 methods and the subclass wants to make use of just two of them, its objects will still have the other 18 just lying around and callable even if they don’t make sense to call
- Instead, we can use delegation (often via encapsulation) to break this coupling and create the precise API we want
- “Has-a” is better than “Is-a” (again, thanks Ben Lerner!)
-
Mixins and Traits
- The idea of mixins is that we want to be able to extend classes and objects with new functionality without using inheritance
- With mixins, we can create specialized classes that each have a single responsibility
- These mixins can then be added to our classes, combining functionality exactly how we want to
Configuration#
-
When code relies on values that may change after the application has gone live, keep those values external to the app
-
When your application will run in different environments, and potentially for different customers, keep the environment- and customer-specific values outside the app
-
Parameterize your app using external configurations
- Credentials for external services (databases, third-party APIs, etc.)
- Logging levels and destinations
- Port, IP address, machine, and cluster names the app uses
- And more
Chapter 6: Concurrency#
-
Concurrency is when the execution of two or more pieces of code act as if they run at the same time
- Need to run code in an environment that can switch execution between different parts of your code when it is running
- Often implemented using fibers, threads, and processes
-
Parallelism is when they actually do run at the same time
- Need hardware that can do two things at once
- Often requires multiple cores in a CPU, multiple CPUs in a computer, or multiple computers connected together
-
Concurrency is a software mechanism, while parallelism is a hardware concern
-
Most decent-sized systems require concurrency
- If you force this process to be serial, your system feels sluggish and you’re probably not taking full advantage of the power of the hardware on which it runs
-
Temporal Coupling happens when your code imposes a sequence of things that is not required to solve the problem at hand
Breaking Temporal Coupling#
-
For many projects, it’s useful to model and analyze the application workflows to find out what can happen at the same time and what must happen in a strict order
- We’re hoping to find activities that take time, but not time in our code
- Querying a database, accessing an external service, waiting for user input: all these things would normally stall our program until they complete, but in a concurrent architecture they are opportunities to do something more productive than the CPU equivalent of twiddling our thumbs
-
When we have pieces of work that are relatively independent, we can process each in parallel and then combine the results
- For example, the Elixir compiler splits the project it is building into modules and compiles each in parallel
- When a module depends on another, its compilation pauses until the results of the other module’s build become available
- When the top-level module completes, it means that all dependencies have been compiled
- The result is a speedy compilation that takes advantage of all the cores available
Shared State Is Incorrect State#
-
When we have nonatomic updates, the problem is that neither process can guarantee that its view of that memory is consistent
-
A semaphore is a thing that only one process can own at a time
- You can create a semaphore and then use it to control access to some other resource
- The operations involved here are lock / unlock or claim / release
- Note that this only works if everyone who accesses the resource agrees on the convention of using the semaphore; if some developer writes code that doesn’t follow the convention, then we’re back in chaos
-
Make the resource transactional
- Instead of delegating responsibility for protecting access to the resource to the processes that use it, we can centralize that control
- We also need to be careful of unhandled exceptions, since they could cause semaphores to never get unlocked
-
Concurrency problems can pop up not only when we have shared memory but anywhere where your application code shares mutable resources: files, databases, external services, etc.
- Most languages have library support for some kind of exclusive access to shared resources
- They may call it mutexes (for mutual exclusion), monitors, or semaphores
- Random failures are often concurrency issues, and they’re probably random because of race conditions or something similar
Actors and Processes#
-
An actor is an independent virtual processor with its own local (and private) state
- Each actor has a mailbox
- When a message appears in the mailbox and the actor is idle, it kicks into life and processes the message
- When it finished processing, it processes another message in the mailbox, or, if the mailbox is empty, it goes back to sleep
- When processing a message, an actor can create other actors, send messages to other actors that it knows about, and create a new state that will become the current state when the next message is being processed
- Actors execute concurrently, asynchronously, and share nothing
-
A process is typically a more general-purpose virtual processor, often implemented by the operating system to facilitate concurrency
- Processes can be constrained (by convention) to behave like actors
Blackboards#
-
Messaging systems (such as Kafka) can be like blackboards
- These systems offer persistence (in the form of an event log) and the ability to retrieve messages through a form of pattern matching
- These are platforms that can be used both as a blackboard system and a platform on which you can run a bunch of actors
-
While the actor/blackboard/microservice/messaging system approach solves a whole class of concurrency problems, that benefit comes at a cost
- These approaches are harder to reason about, because a lot of the action is indirect
- You need good tooling to be able to trace messages and facts as they progress through the system (e.g. through unique trace IDs that are propagated as an event moves through the system’s actors)
Chapter 7: While You Are Coding#
Listen to Your Lizard Brain#
-
Sometimes, code just flies from your brain into the editor: ideas become bits with seemingly no effort
- Other times, coding feels like walking uphill in mud - when this happens, ask yourself why this is
- It may be that this is harder than it should be. Maybe the structure or design is wrong, maybe you’re solving the wrong problem, or maybe you don’t fully understand the context
- Listen to your instincts: give yourself a little time and space (maybe by going on a walk) to organize your thoughts, or use a rubber duck
-
When you’re having trouble getting started (staring at an empty code editor), go into prototyping mode
- Just start trying things out, reminding yourself that prototypes are meant to fail and get thrown away, so there’s no downside to doing this
- In your empty editor buffer, create a comment describing in one sentence what you want to learn or do
- Then start coding
-
A big part of the job is dealing with existing code written by others
- When you spot things done in a way that seems strange, jot it down
- Continue doing this, and look for patterns
- If you determine what drove other engineers to write code that way, you’ll be able to learn new things and read new code more easily
Programming by Coincidence#
-
Accidents of implementation are things that happen simply because that’s the way the code is currently written
- You end up relying on undocumented error or boundary conditions, and make implicit design decisions without realizing it
- When some code is working, it’s important to understand exactly WHY/HOW it’s working, so that you know it’s fully correct and doesn’t have any undocumented/unintended behavior
- For code you write, make a well-specified contract and clear interface for users of your API to interact with
- For code you call, rely only on documented behavior
-
How to program deliberately
- Always be aware of what you are doing
- Don’t build an application you don’t fully grasp or use a technology you don’t understand
- Proceed from a plan, whether that plan in in your head, on paper, or on a whiteboard
- Rely only on reliable things; don’t depend on assumptions
- Document your assumptions
- Don’t just test your code, but test your assumptions as well
- Prioritize your effort; spend time on the important/hard aspects
- Don’t let existing code dictate future code if it makes the wrong design decisions - all code can be replaced if it is no longer appropriate
Algorithm Speed#
-
It’s useful to just do a subconscious check of the runtime and memory requirements anytime you’re writing something containing loops or recursive calls, just to make sure what you’re doing in that context is sensible
-
Big O Notation
- $O(1)$: Constant (access element in array, simple statements)
- $O(\log n)$: Logarithmic (binary search)
- $O(n)$: Linear (sequential search)
- $O(n * \log n)$: Worse than linear, but not much worse. (Average runtime of quicksort, heapsort)
- $O(n^2)$: Square law (selection and insertion sorts)
- $O(n^3)$: Cubic (multiplication of two $n$ x $n$ matrices)
- $O(C^n)$: Exponential (traveling salesman problem, set partitioning)
-
Be wary of premature optimization
- It’s a good idea to make sure an algorithm really is a bottleneck before investing time into trying to improve it
Refactoring#
-
Rather than construction, software is more like gardening - it is more organic than concrete
- It’s common to need to rewrite, rework, and/or re-architect code (refactoring)
- Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior
-
Many things may cause code to quality for refactoring:
- Duplication: you’ve discovered a violation of the DRY principle
- Nonorthogonal Design: you’ve discovering something that could be made more orthogonal
- Outdated Knowledge: code needs to keep up with changing requirements
- Usage: some features may be more important than previously thought based on usage
- Performance: you may need to move functionality from one area of the system to another to improve performance
-
Refactor Early, Refactor Often
- Refactoring is easier to do while the issues are small, as an ongoing activity while coding
-
Tips for refactoring
- Don’t try to refactor and add functionality at the same time
- Make sure you have good tests before you begin refactoring, and run the tests as often as possible
- Take short, deliberate steps; refactoring often involves making many localized changes that result in a larger-scale change
Test to Code#
-
Testing is not (always) about finding bugs
- The major benefits of testing happen when you think about and write the tests, not when you run them
- Thinking about writing a test for your code makes you think about it from the outside, as if you were a client of the code and not its author
-
A good way to think of unit testing is testing against contract
- We want to write test cases that ensure that a given unit honors its contract
- We want to test that the module delivers the functionality it promises, over a wide range of test cases and boundary conditions
Property-Based Testing#
- If you write the original code and you write the tests, it’s possible that an incorrect assumption is expressed in both, so the tests pass (but they shouldn’t)
- To help address this problem, we can use some amount of property-based testing to automate our testing based on formalized contracts and invariants
Stay Safe Out There#
-
Basic principles of security
- Minimize attack surface area
- Principle of Least Privilege
- Secure defaults
- Encrypt sensitive data
- Maintain security updates
-
Minimize attack surface area
- Code complexity makes the attack surface larger, leading to more attack vectors - simple, smaller code is better
- Never trust data from an external entity - always sanitize it before using it
-
Principle of Least Privilege
- Use the least amount of privilege for the shortest time you can get away with
- Relinquish your permissions quickly to reduce risk
-
Secure defaults
- The default settings on your app should be the most secure values
-
Encrypt sensitive data
- Don’t leave personally identifiable information, financial data, passwords, or other credentials in plain text, including in database storage
- Don’t check in secrets, API keys, SSH keys, encryption passwords, or other credentials alongside your source code in version control
-
Maintain security updates
- Apply all security patches quickly
Naming Things#
-
Use the conventions that are idiomatic in the programming language or environment that you are using
-
Within a given project, be consistent with the jargon and naming conventions
-
Renaming is often even harder than writing names in the first place
- If you aren’t vigilant about updating names when code is refactored, your names can quickly become misleading
Chapter 8: Before the Project#
The Requirements Pit#
- Creating the requirements for a project is an active process that involves a feedback loop
- First, help the client understand the consequences of their stated requirements
- Then, generate feedback and let them use that feedback to refine their thinking
Solving Impossible Puzzles#
-
When working on a difficult problem, recognize the absolute constraints which must be honored and what preconceived notions might be overridden in order to develop a creative solution
- Recognize the degrees of freedom available to you
- Enumerate all the possible avenues you have before you, and only then start eliminating options
-
Ask yourself:
- Why are you solving this problem?
- What’s the benefit of solving it?
- Are the problems you’re having related to edge cases? Can you eliminate them?
- Is there a simpler, related problem you can solve?
Working Together#
-
When working with teammates, it’s useful to not just ask questions, have discussions, and take notes, but to ask questions and have discussions while you’re actually coding
-
Pair Programming
- The Driver focuses on low-level details of syntax and coding style, while the Navigator is free to consider higher-level issues and scope
- The inherent peer pressure of a second person makes the Driver less inclined to take shortcuts, like bad naming decisions
The Essence of Agility#
-
Values from the manifesto:
- Individuals and interactions over processes and tools
- Working software over comprehensive documentation
- Customer collaboration over contract negotiation
- Responding to change over following a plan
-
There is no such thing as an agile process, because agility is all about responding to change and the unknowns you encounter after you set out
- No fixed, static plan can survive this uncertainty
-
The simplest recipe for working in an agile way:
- Work out where you are
- Make the smallest meaningful step towards where you want to be
- Evaluate where you end up, and fix anything you broke
-
Step #3, needing to be able to fix what we break and make our feedback loop efficient, justifies why a good design produces something that’s easier to change than a bad design
Chapter 9: Pragmatic Projects#
Pragmatic Teams#
-
As team size grows, communication paths grow at the rate of $O(n^2)$, where $n$ is the number of team members
- On larger teams, communication begins to break down and becomes ineffective
-
A pragmatic team is small, under 12-12 or so members
- Members come and go rarely
- Everyone knows each other well, trusts each other, and depends on each other
-
Make time for improving the knowledge portfolios of team members
- Conduct maintenance work on old systems
- Reflect on and refine processes that the team relies on
- Experiment with new technologies with prototypes and analyze results
- Make sure team members are learning and improving skills, via informal brown-bag lunches or more formal training sessions
-
Communication on a team should be frictionless: easy and low-ceremony to ask questions, share your progress, your problems, your insights and learnings, and to stay aware of what your teammates are doing
Coconuts Don’t Cut It#
-
Particular artifacts, superficial structures, policies, processes, and methods are not enough
- Imitating form without content will only lead to failure
-
The only way to know what truly works for your team is to try it and iterate
- The goal isn’t to “do Scrum”, “do agile”, or “do Lean”; the goal is to be in a position to deliver working software that gives the users some new capability, quickly
Pragmatic Starter Kit#
-
Version Control
- Everything needed to build your project should be kept under version control
- Machines should be ephemeral - build machines and/or clusters can be created on demand as spot instances in the cloud
- Deployment configuration is under version control as well, so releasing to production can be handled automatically
- Overall, use version control to drive builds, tests, and releases
-
Ruthless and Continuous Testing
- Test early, test often, and test automatically to find our bugs as fast as possible
- Unit Testing: exercise an individual module
- Integration Testing: test that the major subsystems that make up the project work and play well with each other
- Validation and Verification: verify that the executable user interface/prototype meets the functional requirements of the system
- Performance/Stress Testing: test that the software meets the performance requirements under real-world conditions (i.e. expected number of users, connections, transactions per second) - is it scalable?
-
Test the coverage of possible states that your program could be in, not just code coverage by number of lines executed
Delight Your Users#
-
The expectations of business value derived from a project are what really matter - not just the software project itself
- Make sure everyone on the team is on the same page about these expectations
- When making decisions, think about which path forward moves closer to those expectations
-
Delight users; don’t just deliver code
Pride and Prejudice#
- Engineers are artisans, and artisans are proud to sign their work
- Pride of ownership is important - “I wrote this, and I stand behind my work”
- Your signature should come to be recognized as an indicator of quality
- People should see your name on a piece of code and expect it to be solid, well-written, tested, and documented