This is my summary of the book The Pragmatic Programmer.
I like to read and make highlights of the parts that I think are most important. This reading approach helps me absorb the content better. This post is a collection of my highlights tailored to my own life and experience.
I've started reading this book to become a better programmer. This content will be full of tips to incorporate into your own professional life. It also includes some tooltips into some less known words to me. That's a good way to learn new words, specially for a non-native English speaker.
There are some other great summaries of this book out there, like this one from @HugoMatilla. I'm doing my own to soak in the content and make it more personal.
If you're a publisher and don't like me doing this, please let me know via email.
Preface
Pragmatism is all about the practical side of things, from beliefs and theories to practical consequences and real-world applications.
Pragmatic programmers exhibit the following characteristics:
- Early adopter / fast adapter: you need to be able to learn new things quickly and adapt to new situations, never stop learning. Your confidence is born of experience.
- Inquisitive: you ask questions and seek answers, you are a pack rat for small bits of information.
- Critical thinker: you rarely take things as given, you question assumptions and challenge the status quo.
- Realistic: you don't get caught up in fantasies, you focus on the practical aspects of the situation, with a good feel for how difficult things are, and how long things will take.
- Jack of all trades: you don't need to be an expert in everything, but you keep abreast of new developments.
If you’re going to spend your life developing software, why not strive to do it well?
Tip
Care about your craft.
Think about what you're doing while you're doing it.
Tip
Turn off the autopilot and take control. Constantly critique and appraise your work.
It's a continuous process. Just like great lawns require consistent, small efforts, craftsmanship in software development thrives on daily care and improvement. Kaizen (改善) is the Japanese concept of continuously making many small improvements.
Chapter 1: A Pragmatic Philosophy
Pragmatic programmers think beyond the immediate problem. They won't sit idly by and watch their projects fall apart through neglect. They like to keep them pristine, maintaining momentum.
A big cornerstone of this philosophy is that you need to take responsibility for yourself and your actions in terms of your career advancement, your project, and your day-to-day work, not being afraid to admit ignorance or mistakes. No matter how thorough your planning, testing, documentation and automation is, you will always encounter unexpected problems. Pragmatic programmers deal with them professionally by being proactive and taking responsibility.
To achieve this, you must analyze the situation for risks that are beyond your control, admitting it honestly and trying to offer options. Don't blame someone or something else, nor make up lame excuses. It's up to you to provide solutions, not excuses.
Tip
Instead of excuses, provide options. Don't say it can't be done; explain what can be done.
When you're about to approach someone trying to say why something can't be done, stop and listen to yourself. Talk to the rubber duck on your monitor, or the cat. Does it sound reasonable, or stupid? Run through the conversation in your mind. What is the other person likely to say?
One broken window, left unrepaired, will soon become a haven for vandals. Don't live with broken windows, it can deteriorate functional systems pretty quickly. Fight software entropy.
Tip
Fix bad designs, wrong decisions, and poor code when you see them.
You may be in a situation where you know exactly what needs doing and how to do it. But ask permission to tackle the whole thing and you'll be met with delays and blank stares. Work out what you can reasonably ask for. Develop it well. Once you've got it, show it to people and let them marvel. People find it easier to join an ongoing success. Then say "of course, it would be better if we added ..."
Tip
Be a catalyst for change. You can't force change on people. Instead, show them how the future might be and help them participate in creating it.
Don't focus too tightly and forget about the rest of the world. Try to minimize things creeping up on you. It's often the accumulation of small things that breaks morale and teams. If a frog is put in a pot of boiling water, it will jump out. But if you put it in a pot of cold water and slowly bring the heat up, it will stay in the water until it's too late.
Tip
Remember the bigger picture. Don't get so engrossed in the details that you forget to check what's happening around you. It's not just about what you personally are doing.
The frog problem is different from the broken windows issue. On the former, people lose the will to fight entropy because they perceive that no one else cares. The frog just doesn't notice the change.
Write good enough software. Good enough does not imply sloppy or poorly code. Give your users the opportunity to participate in the process of deciding when what you've produced is good enough. Good enough for your users, for future maintainers, for your own peace of mind. You'll find that it is more productive, your users are happier, and programs are better for short term incubation. How good is good enough?
Tip
Make quality a requirement issue. Involve your users in determining the project's real quality requirements.
Great software today is often preferable to perfect software tomorrow. If you give your users something to play with early, their feedback will often lead you to a better eventual solution.
Also know when to stop. Don't spoil a perfectly good program with overembellishment and over-refinement. Keep it simple. Move on, and let your code stand in its own right for a while. It may not be perfect, but don't worry, it could never be perfect.
An investment in knowledge always pays the best interest. That's a quote from Benjamin Franklin - never at a loss for a pithy homily. Your knowledge and experience are your most valuable assets. Ben really hit the nail on the head with this one. You should treat your knowledge portfolio as a investment. Serious investors invest regularly - as a habit. Diversify. Manage risk. Buy low, sell high. Review and rebalance.
Tip
Invest regularly in your knowledge portfolio. Make learning a habit.
Learn at least one new language every year. Avoid getting stuck in a rut. Read a technical book each quarter. Read non-technical books too. Take classes. Participate in local user groups. Isolation can be deadly to your career. Experiment with different environments. Stay current. Get wired by participating in newsgroups.
If somebody asks you a question, and you don't know the answer, freely admit it. Don't let it stop there. Take it as a personal challenge to find the answer. Search the web, ask a guru. Decide if you want to ask publicly or privately. If you're emailing, use a meaningful subject line. Need help!!!
doesn't cut it. Be specific.
Manage your learning time wisely. Always have something to read in an otherwise dead moment. Time spent waiting for doctors and dentists can be a great opportunity to catch up on your reading.
Tip
Critically analyze what you read and hear. Don't be swayed by vendors, media hype, or zealots' dogma. Analyze information in terms of you and your project.
Communicate! It's better to be looked over than it is to be overlooked. It doesn't matter if you have the best ideas and/or the finest code if you can't communicate about them with other people. Know what you want to say, and plan your message. Write an outline, jot down the ideas you want to communicate. Then, rehearse and ask yourself, "Does this get across whatever I'm trying to say?". Refine it until it does.
Know your audience. Understand their needs, interests and capabilities. Form a strong mental picture of your audience. The acrostic WISDOM may help you remember the key points:
- Who are you talking to? What do you want them to learn?
- Interesting - why should they care?
- Sophisticated - is it at the right level?
- Details - do you need to go into much detail?
- Over what period of time should this be communicated?
- Motivation - how to keep them listening to you?
By making the appropriate pitch to each group, you can maximize the chances of your message being heard and acted upon. Make sure that you choose the right moment and medium. Sometimes all it takes is the simple question "Is this a good time to talk about ...?". Make it look good — presentation and delivery are equally important. Produce a stunning output that involves your audience. Encourage people to talk by asking questions. Get back to people.
Tip
It's both what you say and the way you say it. There's no point in having great ideas if you can't communicate them effectively.
The more effective you communicate, the more influential you become.
Chapter 2: A Pragmatic Approach
Software development applies certain ideas that are almost axiomatic, and processes that are virtually universal, but they're rarely documented as such. This chapter talks about those, such as:
- The evils of duplication
- Orthogonality
- Reversibility
- Tracer bullets
- Prototypes and Post-it Notes
- Domain Languages
- Estimating
Unfortunately, knowledge isn't stable. It changes, often rapidly. Our understanding of our system changes day by day. It's very easy to duplicate knowledge during those changes. If you have the same thing expressed in two or more places, you change one place, and you have to remember to change the others. This isn't a question of whether you'll remember: it's a question of when you'll forget. Memory is a fragile thing. At these times, you need to make sure that you have a single source of truth.
Tip
DRY — Don't Repeat Yourself. Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
The DRY principle is a powerful tool to develop software reliably, easier to understand and maintain. Often, duplication arises from:
- Inadvertent duplication: developers don't realize that they're duplicating code, usually because flaws of design and communication.
- Impatient duplication: developers get lazy and copy code to get things done easier, or faster because of deadlines and time pressure.
- Imposed duplication: developers feel they have no choice, the environment seems to require duplication.
- Interdeveloper duplication: multiple people on a team (or different teams) duplicate a piece of knowledge because they don't know about each other's work.
You can overcome these duplication issues with simple techniques. For example, you can use a single source of truth, such as a metadata file that can automatically generate other representations in different formats and languages. If the problem is about communication, programmatic programmers set up forums to discuss common issues. The idea is to facilitate the exchange of knowledge, and make it easier to reuse it.
Tip
Make it easy to reuse. It it's easy to reuse, people will. Create an environment that supports reuse.
Orthogonality, in computing, references a kind of independence or decoupling. The term is borrowed from geometry. Two or more things are orthogonal if changes in one do not affect any of the others. Move along one of the lines, and your position projected onto the other doesn't change. In an orthogonal system, the database code would be separate from the user interface code. An example of a non-orthogonal system is the helicopter control: an unbelievably complex system, where every change impacts all the other inputs. When components of any system are highly interdependent, there is no such thing as a local fix. In software development, we often want to avoid this kind of non-orthogonal behavior.
Tip
Eliminate effects between unrelated things. Design components that are self-contained, independent, and have a single, well-defined purpose.
You get two major benefits if you write orthogonal code: increased productivity and reduced risk. Changes are localized. You get more functionality per unit effort by combining orthogonal components. To achieve that, organize code and teams into groups with well-defined responsibilities and minimal overlap. Start by separating infrastructure from application. Orthogonal teams are more efficient, encourage subteams to communicate constantly with each other.
To design orthogonal systems, organize components into layers. An easy test for orthogonal designs is to ask "If I change X, will it affect Y?" If the answer is "None", then X and Y are orthogonal. Also, when you come across a problem, assess how localized the fix is.
Nothing is more dangerous than an idea when it's the only one you have. We don't always make the best decisions the first time around, so be prepared to reverse your decisions. Don't assume that a decision is cast in stone, instead, think about decisions as being written in the sand at the beach. It's a mistake to not be prepared for the contingencies that will arise. Think about them up front. Use encapsulation, lightweight coupling and metadata to help you abstract vendors, technologies and whatnot. No one knows what the future may hold.
Tip
There are no final decisions. Plan for change.
Tracer bullets are a technique to help you identify the target while shooting. It's very similar to proof of concepts, users get to see something working early and developers build a structure to work in. It doesn't always hit the target, but it helps you identify the direction. You use this technique in situations where you're not 100% certain of where you're going. Gather feedback early and often.
Tip
Use tracer bullets to find the target by trying things and seeing how close they land.
Prototyping is different because you throw away whatever you lashed together when trying out the concept, and recode it properly using the lessons you've learned. Prototype anything that carries a large amount of risk. Prototyping generates disposable code. Tracer code is lean but complete, and forms part of the skeleton of the final system.
Tip
Prototype to learn. Its value lies not in the code you produce, but in the lessons you learn.
Domain languages are specialized languages that are used to solve a particular problem. They are often used to describe the problem domain in a way that is more natural and easier to understand than traditional languages. Languages influence how you think about a problem, and how you think about communication. Choose the right language for the job. You wouldn't use a SQL-like language to write a concurrent program.
Tip
Program close to the problem domain. Design and code in your user's language.
How many months will it take to deliver your project? This is one of my favorite questions. All answers are estimates, it's just a matter of how accurate they are. When someone asks you for an estimate, think about the context in which your answer will be taken. Do they need high accuracy, or just a ballpark figure?
Tip
Estimate before you start. You'll spot potential problems up front.
First, understand what's being asked. Have a grasp of the scope of the domain. "Assuming there are no traffic delays and there's gas in the car, I should be there in 20 minutes". Then, build a rough bare-bones mental model. For a project, the model may be the steps that your organization uses during development, along with a very rough picture of how the system might be implemented. Once you have a model, you can decompose it into components. Each component typically have parameters that affect how it contributes to the overall model. Identify each parameter. Give each parameter a value. Work out which parameters have the most impact on the result.
The units you use make a difference. Choose the units of your answer to reflect the accuracy you intend to convey. This time scale is recommended by the book:
Duration | Quote estimate in |
---|---|
1-15 days | days |
3-8 weeks | weeks |
8-30 weeks | months |
30+ weeks | think hard before giving an estimate |
Start keeping a log of your estimates. For each, track how accurate you turned out to be. If your error was greater than 50%, try to find out where your estimate went wrong. A spreadsheet can be a big help.
Tip
Iterate the schedule with the code. Use experience you gain as you implement to refine the project time scales.
When someone asks you for an estimate, you say "I'll get back to you". You get better results if you slow the process down and spend some going through the steps above.
Chapter 3: The Basic Tools
Having a basic set of good-quality tools is key for every developer. Tools are the extensions of your brain. They amplify your talent. The better your tools, and the better you know how to use them, the more productive you can be. Expect to add to your toolbox regularly. Let need drive your acquisitions. Always be on the lookout for better ways of doing things.
Programmers' base material is knowledge. We gather requirements as knowledge, and then express that knowledge in our design, implementation, tests and documents. Plain text is the best format for storing knowledge; that way, we give ourselves the ability to manipulate it both manually and programmatically. Plain text is made up of printable characters in a form that can be read and understood by humans.
Tip
Keep knowledge in plain text. It won't become obsolete. It helps leverage your work and simplifies debugging and testing.
Play around with your command shell and invest some energy in becoming proficient. Learn basic yet useful commands, like grep
, find
, sed
and sort
.
Tip
Use the power of command shells when graphical user interfaces don't cut it.
Also, learn regular expressions; they are a powerful tool for finding and manipulating text.
You need to manipulate text with minimal effort. Use a single editor (or set of keybindings) across all text editing activities. Some options include Vim, Emacs, VSCode, Cursor, Windsurf, etc.
Tip
Use a single editor well. The editor should be an extension of your hand; make sure your editor is configurable, extensible and programmable.
It should offer basic features:
- Syntax highlighting
- Auto-completion
- Auto-indentation
- Initial code or boilerplate for your language
- IDE-like features (compile, run, debug)
Always use source control. With git, you can always go back to a previous version of your software. It helps answer questions such as: Who made changes in this line of code? What's the difference between the current version and last week's? How many lines of code did we change in this release? Which files get changed most often? This kind of information is invaluable for bug tracking, audits, performance and quality purposes.
No one writes truly perfect software. Bugs are a certainty. These software defects manifest themselves in a variety of ways, from misunderstood requirements to coding errors. Debugging those errors is just problem solving. Concentrate on fixing the problem, not the blame. Also, be sure that you're attacking the root of the problem, not just the symptoms.
Tip
Fix the problem, not the blame. It doesn't really matter whether the bug is your fault or someone else's — it is still your problem, and it needs to be fixed.
Always interview the user who reported the bug in order to gather more data than you were initially given. You must brutally test both boundary conditions and realistic end-user usage patterns. Bug reproduction is key. The best way to start fixing a bug is to make it reproducible. Sometimes, by forcing yourself to isolate the circumstances that display the bug, you'll even gain an insight on how to fix it. Visualize your data, use a debugging tool. Analyze stack traces. Add tracing statements as you descend the call tree. Don't panic.
Tip
Don't panic when debugging. Take a deep breath and think about what could be causing the bug.
Reevaluate truths you hold dear. Don't make assumptions and gloss over a routine you "know" is working. Prove it. Prove it in this context, with these data, with these boundary conditions. When you fix a bug, that should be the last time you ever have to deal with it. Add tests so you don't have to deal with it again. If it took a long time to fix, ask yourself why.
Chapter 4: Pragmatic Paranoia
As mentioned earlier, an axiom of life is that you can't write perfect software. Accept it and turn that depressing reality into an advantage. Code defensively by not trusting other people's code. If there's any doubt, validate all information you're given. Don't trust yourself either. When everybody is out to get you, paranoia is just good thinking. Play it safe.
Client and suppliers must agree on rights and responsibilities. Design by contract is a way to ensure that both parties understand the expectations and constraints of the relationship. Each party meets its obligations and everyone benefits. Use contracts as a mechanism to write a correct program and evaluate its correctness. A correct program is one that does no more and no less than it claims to do. Those expectations and claims are described as follows:
- Preconditions: the assumptions that must be true before the program is executed.
- Postconditions: the promises that must be true after the program is executed.
- Invariants: the assumptions that must be true during the program's execution.
Be strict in what you will accept before you begin, and promise as little as possible in return. Simply enumerate at design time what the input range is, what the boundary conditions are, and what the routine promises to deliver — or, more importantly, what it doesn't promise to deliver — and you're on your way to writting better software.
Have the compiler check your contracts for you. Inheritance and polymorphism are the cornerstones of object-oriented languages and an area where contracts can really shine. Specify a contract only once, in the base class, to have it applied to every future subclass automatically.
Ensure that your code does no damage while you're working the bugs out. Try to check things often and terminate the program if things go awry. An easy way to do this is to leverage early returns in your code. It's much easier to find and diagnose the problem by crashing early. Dead programs tell no lies.
Tip
Crash early. A dead program normally does a lot less damage than a crippled one.
Write code that actively verifies your assumptions with assertions. Don't use assertions instead of actual error handling.
In practice, if you check for every possible error, it often leads to some pretty ugly code with nested if
branches. Use try-catch blocks to have a clear flow of control and move all the error handling to a single place. If either party fails to live up to the terms of the contract, an exception should be raised, or the program terminates.
One of the problems with exceptions is knowing when to use them. The author believes that they should rarely be used. Exceptions should be reserved for unexpected events. An exception represents an immediate, nonlocal transfer of control, like a cascading goto
. Programs that use exceptions for their normal processing suffer from readability and maintainability problems.
Tip
Use exceptions for exceptional problems. Exceptions can suffer from all the readability and maintainability problems of classic spaghetti code. Reserve exceptions for exceptional things.
If an exception is often thrown and adding code to handle these exceptions becomes tedious, consider using error handler routines.
When dealing with resources — memory, transactions, threads, files, timers — you need to balance allocation and deallocation. The author suggests a simple tip:
Tip
Finish what you start. Where possible, the routine or object that allocates a resource should be responsible for deallocating it.
Encapsulate resources in classes. The constructor provides a mechanism to allocate the resource, and the destructor deallocates it.
For nested allocations, deallocate resources in the opposite order of allocation. When allocating the same set of resources in different places in your code, always allocate them in the same order.
Check the resource balancing. A good place to ensure that resource usage has not increased since the last execution is the main processing loop of a long-running program, like a server.listen()
call.
Chapter 5: Bend, or Break
Life doesn't stand still, and neither does code. Write code that's as flexible as possible. Otherwise, we may find our code quickly becoming outdated, or too brittle to fix. You need code that will roll with the punches.
Organize your code into modules and limit the interaction between them by minimizing coupling. Increasing coupling dependencies raises the risk that an unrelated change somewhere else in the system will affect your code. Traversing relationships between objects directly can quickly lead to a combinatorial explosion of dependency relationships.
Keep dependencies to a minimum by following The Law of Demeter for functions. It states that a function should only call functions that are declared in the same class. It tries to prevent you from reaching into an object to gain access to a third object's methods.
Tip
Minimize coupling between modules. Avoid coupling by writing "shy" code and applying the Law of Demeter.
The Law of Demeter states that an object should only talk to its immediate friends. This means a method should only call methods on objects that are directly accessible to it, avoiding chains like object.getX().getY().doZ()
. This reduces coupling and makes code more maintainable.
Write less code. Move details out of the code completely. Make them configurable. Details mess up our pristine code. Every time we have to make a change to accommodate a new business rule, we run the risk of breaking the system, or introducing a new bug.
You need to make your system highly configurable to the point of easily switching deeply ingrained items, such as choice of algorithms, database products, middleware technology and user-interface style.
Tip
Configure, don't integrate. Implement technology choices for an application as configuration options, not through integration or engineering.
Programming with metadata (data about data) might not be feasible in all applications and scenarios, but it's a high ROI technique. To implement metadata-driven apps, your goal is to think declaratively by specifying what is to be done — not how — and then create highly dynamic and adaptable systems.
This approach has significant benefits:
- it forces you to decouple your design
- you defer details out of the code
- you can change the behavior of the application without recompiling it
Tip
Put abstractions in code, details in metadata. Program for the general case, and put the specifics in the metadata.
Do you depend on the "tick" coming before the "tock"? If you do, you might be suffering from temporal coupling. There are two aspects we need to consider:
- Concurrency: things happening at the same time
- Ordering: the relative positions of things in time
When we write code, we often make assumptions about the order of execution that may not hold true in all circumstances. This can lead to subtle bugs, especially in concurrent systems.
Find out what can happen at the same time, and what must happen in a strict order. UML activity diagrams are useful to maximize parallelism by identifying activities that could be performed in parallel. When we design an architecture or write a program, things tend to be linear. We need to be proactively looking out for concurrency.
The author provides a good workflow example: "making a piña colada".
Recipe:
- Open blender
- Open piña colada mix
- Put mix in blender
- Measure 1/2 cup white rum
- Pour in rum
- Add 2 cups of ice
- Close blender
- Liquefy for 2 minutes
- Open blender
- Get glasses
- Get pink umbrellas
- Serve
If we were in a piña colada-making contest, we could optimize this workflow as follows:

It can be eye-opening to see where the dependencies truly exist.
Tip
Analyze workflow to improve concurrency. Exploit concurrency in your user's workflow.
Design your program to work with time as an explicit parameter. Make the order of execution clear and controllable. This makes your code more flexible and easier to maintain, especially when requirements change or when you need to add concurrent processing.
Keep your data separate from how it's presented. This way, you only have to change the data once, and all views will update automatically.
Tip
Separate views from models. Let views subscribe to model changes.
You can also use blackboards to coordinate work as a meeting place where modules can exchange data anonymously and asynchronously, with almost no restrictions on what can be added, its data format, or its organization.
Think of detectives working on a case: they gather evidence and post their findings on a blackboard. Other detectives see this information, make connections with their own findings, and add new insights. No detective needs to know who added what - they just work with the available information.
A real-world application of this pattern is processing mortgage or loan applications. Different departments (credit check, property appraisal, income verification, etc.) can work independently, adding their findings to a central blackboard. When all required information is present, the final approval process can begin. Each department works at its own pace, and the system coordinates their efforts without tight coupling between modules.
Tip
Use blackboards to coordinate disparate facts and agents, while maintaining independence and isolation among participants.
Chapter 6: While You Are Coding
Coding is not mechanical. There are decisions to be made every minute that require careful thought and judgment. Developers who don't actively think about their code are programming by coincidence. Pragmatic programmers think critically about all code. We constantly see room for improvement. We make our code easy to test. We put ourselves into good positions in case the unexpected happens.
Programming by coincidence happens when developers rely on undocumented or accidental behaviors in their code rather than understanding how and why it works. Don't be a war soldier tiptoeing through a minefield, wincing with every step, only to be blown to pieces as you gain confidence. There are hundreds of traps just waiting to catch us each day. Don't let false conclusions lead you to disastrous results. Be wary not to rely on undocumented error or boundary conditions.
To avoid programming by coincidence, rely only on documented behaviors, eliminate unnecessary calls, and adhere to modular design principles. Ensure assumptions are explicit and well-documented, particularly for edge cases and dependencies on external systems, such as specific environments or user characteristics. Testing should prove causality rather than assuming it, as implicit and unverified assumptions can undermine projects at every level.
Tip
Don't program by coincidence. Rely on reliable things. Beware of accidental complexity, and don't confuse a happy coincidence with a purposeful plan.
How to program deliberately?
- Always be aware of what you are doing
- Don't code blindfolded
- Proceed from a plan
- Rely only on reliable things
- Document your assumptions
- Don't just test your code — test your assumptions as well
- Prioritize your effort
- Don't be a slave to history, be ready to refactor
Besides estimating timeframes for a project, you should also estimate the resources that algorithms use: time, processor, memory and so on. This kind of estimating is crucial. Given a choice between two ways of doing something, which do you pick? It's critical to choose the fastest implementation, predicting scalability and performance bottlenecks. To evaluate algorithm efficiency, the "big O" notation is commonly used.

I, personally, didn't study this in school because my graduation was in Electrical Engineering, so I had to learn it on my own. TLDR: the "big O" notation is used to describe the upper bound of an algorithm's time-space complexity. I won't be diving too much into this topic, but you can estimate the order of many basic algorithms using common sense:
- Simple loops are
O(n)
, examples include exhaustive searches, finding the maximum value in an array, and generating checksums. - Nested loops are
O(m * n)
, examples are simple sorting algorithms such as bubble sort. - Binary halves are
O(log n)
, examples include binary search and binary tree operations that traverse it. - Divide and conquer algorithms are
O(n log n)
, examples include algorithms that partition their input and work on two halves independently like quicksort. - Combinatorial algorithms are
O(n!)
, examples include algorithms that try all possible combinations of elements, like the traveling salesman problem. Often, heuristics are used to reduce the running times of these algorithms in particular problem domains.
Tip
Estimate the complexity of your algorithms. Ask yourself how large the input can get.
Also, be wary of premature optimization.
As a metaphor exercise, think of software development more like gardening than construction — it is more organic than concrete. You constantly monitor the health of the garden, and make adjustments as needed. You should rework your code whenever it strikes you as being "wrong". Don't hesitate to change it. There's no time like the present.
Some things that qualify refactoring are:
- Duplication
- Non-orthogonal design
- Outdated knowledge
- Performance
Refactoring is an exercise of pain management. Time pressure is often used as an excuse for not refactoring. That excuse does not hold up. Think of code that needs refactoring as a malignant cancer. The longer you wait, the more damage it does.
Tip
Refactor early, refactor often. Don't let your code rot.
Keep track of things that need to be refactored by maintaining a backlog. Make sure it is placed on the schedule.
Refactoring is redesign. It needs to be undertaken slowly, deliberately and carefully. You don't want to do more harm than good. If you proceed to rip up vast quantities of code with wild abandon, you may find yourself in a worse position than when you started.
Here are three 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. Maintaining a good regression test suite is key to refactoring.
- Take short, deliberate steps. Make many localized changes that result in a larger-scale change.
Speaking of tests, you should make your code easy to test. Integrated circuits only work if the components you are using are known to be reliable. We need to do the same thing in software — test each piece thoroughly before trying to wire them together.
When you design a module, you should design both its contract and the code to test that contract, considering boundary conditions and other issues that wouldn't occur to you otherwise. In fact, if you build the tests before you implement the code, you get to try out the interface before you commit to it. That's Test-Driven-Development (TDD)!
Tip
Design to test. Start thinking about testing before you write a line of code.
The unit tests for a module shouldn't be shoved away in some far-away corner of the source tree. Tests need to be conveniently located. Co-located with the code they are testing. Remember: if it's not easy to find, it won't be used.
You should also have a solid test harness, like Jest. It should be easy to write tests, run them and see the results. They should be fast to be run frequently. You should have mechanisms to run tests in different environments, or in isolation if you're trying to debug a failing test.
Unit tests are the foundation, but you can also write integration tests, composing modules to test how they work together. You can even consider writing end-to-end tests, which simulate the interaction between an end-user and the system.
Testing is more cultural than technical. It's about guaranteeing quality and minimizing risks.
Tip
Test your software, or your users will. Don't make your users find bugs for you.
Be wary of wizards that automatically generate skeleton or boilerplate code. You need to understand the produced code too, otherwise you're just programming by coincidence. No one should be producing code that they don't fully understand.
Chapter 7: Before the Project
At the very beginning of a project, establish some basic ground rules to make sure the developer (you) and the project are not fated to doom.
First, determine the requirements. They rarely lie on the surface. Instead, they're buried deep beneath layers of assumptions, misconceptions, and politics. You have to dig for them.
Tip
Don't gather requirements — dig for them!
How can you recognize a true requirement? It's hard to answer, but can be simplified to statements of something that needs to be accomplished. Very few requirements are clear-cut statements. Requirements are commonly bound to business policies, and those policies change regularly. The author recommends documenting those policies separately from the requirements, and hyperlink the two. Gathering requirements in this way naturally leads you to a system that is well factored to support metadata.
To understand user requirements, become a user. Understand their pain and learn their expectations by spending some period of time with them while they're using the system.
Tip
Work with a user to think like a user. It's the best way to gain insight into how the system will really be used.
As you find requirements, you need to write them down and publish a document that everyone on the team can understand — the developers, the project manager, the customer, the marketing team, etc. That's a pretty wide audience. The author recommends "use cases" to capture requirements. The form that they will take is based on your audience: simple prose, structured document, level of detail, etc. Emphasize the goal-driven nature of the use cases.
Don't be a slave to any notation. UML diagrams, formal vs informal documents, whatever works for your audience.
Overspecification can be dangerous. Good requirement documents remain abstract. Requirements are plain language translations of needs.
Tip
Abstractions live longer than details. They can survive the barrage of changes from different implementations and new technologies.
Many project failures are blamed on an increase in scope. What can we do to prevent requirements from creeping up on us? The key to managing growth of scope is to point out a new feature's impact on the schedule. If you're using Agile sprints or cycles, it's easy to have an accurate, complete picture of how, and when, requirement growth occurred.
Maintain a project glossary. It's hard to succeed on a project where users and developers refer to the same thing by different names, or even worse, refer to different things by the same name.
To solve tough problems when obvious solutions don't work, we need to think outside the box. The solution lies elsewhere. The secret to solving the puzzle is to identify the real (not imagined) constraints. For example, can you connect all of the dots in the following puzzle and return to the starting point with just three straight lines — without lifting your pen from the paper or retracing your steps?
Ask yourself these questions:
- Is there an easier way?
- Are you trying to solve the right problem, or have you been distracted by a peripheral technicality?
- Why is this thing a problem?
- What is it that's making it so hard to solve?
- Does it have to be done this way?
- Does it have to be done at all?
Chapter 8: Pragmatic Projects
Pragmatic techniques that work for individuals can be applied to teams as well. A pragmatic team is more than just a collection of pragmatic individuals — it's a team where the whole is greater than the sum of its parts.
Tip
Build teams around motivated individuals. Give them the environment and support they need, and trust them to get the job done.
Quality is a team issue. The project team should have a "no broken windows" policy, where no one is allowed to check in code that breaks the build or causes tests to fail. Teams should avoid the "boiled frog syndrome" - where quality degrades so gradually that no one notices until it's too late.
Communication is key in pragmatic teams. Teams should adopt a common vocabulary, avoid "us versus them" mentality, and maintain small, stable teams whenever possible.
Tip
Don't use one large team when you can use two smaller ones.
Automation is a force multiplier for your team, reducing manual effort and increasing consistency. Pragmatic programmers automate everything they can.
Tip
Don't use manual procedures when you can automate.
Automation should be applied to:
- The build process
- Testing
- Documentation generation
- Deployment
- Monitoring
A fully automated build process is essential. It should be a single command that builds the entire system from scratch, runs all tests, and produces deployable artifacts.
Tip
Invest in the build process. Make it easy to deploy your application.
Managing expectations is perhaps the most difficult part of software development. Users, clients, and managers all have different expectations about what the software will do and when it will be done.
Tip
Gently exceed your users' expectations.
Under-promise and over-deliver. It's better to surprise people with earlier delivery or more features than to disappoint them with delays or missing functionality.
Be honest about timeframes and capabilities. If you don't know how long something will take, say so. If you're not sure if a feature is possible, be upfront about it.
Tip
Sign your work. Craftspeople of an earlier age were proud to sign their work. You should be, too.
Taking pride in your work is the essence of being a pragmatic programmer. When you truly care about the quality of what you produce, it shows in everything you do.