THE PRAGMATIC PROGRAMMER

The Pragmatic Programmer is a skilful and practical guide that provides insights and the fundamentals of being a good programmer, increasing your specialisation and technicalities in modern software development. The book was published in October 1999. Andrew Hunt covers personal responsibility and career development topics to architectural techniques for keeping your code flexible and easy to adapt and reuse. The book will teach you how to fight software rot, avoid the trap of duplicating knowledge and make your developments more precise with automation.

HOW THIS BOOK HELPED US?

This book helped us understand how developers can become better programmers by understanding the core fundamentals required for software development success. It shows being a good programmer is not all about technical skills. It concentrates on practical topics like decoupling, power editing, debugging and testing, which help us assist developers in building better code for our clients and become consultants and members of large project teams. The book helped us understand how to use our experience in making more informed decisions in both our professional and personal lives.

THE BOOK EXPLAINED IN UNDER 60 SECONDS

The book uses analogies and short stories to present development methodologies and caveats, for example, the broken windows theory, the story of the stone soup, or the boiling frog. It also has small exercises to practise your programming skills.

The pragmatic programmer explains that software defects manifest in various ways, from misunderstood requirements to coding errors. Unfortunately, modern computer systems are still limited to doing what you tell them and not what you want them to do.

According to the book, no one writes perfect software. So it’s given that debugging will consume a major portion of your day. Debugging is a sensitive, emotional subject for many developers.

TOP THREE QUOTES

“Tools amplify your talent. The better your tools, and the better you know how to use them, the more productive you can be.”

“The greatest of all weaknesses is the fear of appearing weak.”

“The editor will be an extension of your hand; the keys will sing as they slice through text and thought.”

BOOK SUMMARY AND NOTES

Chapter One: A Pragmatic Philosophy

  1. The cat ate my source code

Take responsibility: Responsibility is something you actively agree to; you devote yourself to guaranteeing that something is executed right. However, you don’t necessarily have direct control over every aspect. You have the right not to take responsibility for an unimaginable situation or one surrounded by high risks. When you accept responsibility for a situation, have the humility to be held accountable. Everyone makes mistakes. Therefore, when you make one judgement, admit it honestly and try to find options.

  1. Software Entropy

Having an unrepaired broken window in the house for a substantial period instils in the occupiers of the house a sense of abandonment which eventually creates more damage and ruin. The same applies to software; one lousy piece of code can result in more harm if not repaired soon enough. Therefore, don’t live with destructive code. Fix it as soon as a bug is discovered. Take some action and show initiative to prevent further damage and to show that you’re on top of the situation.

  1. Good-enough software

Good enough does not imply sloppy or poorly produced code. All systems must meet their users’ requirements to be successful. You can subject yourself to writing software that’s good enough for your users, future maintainers and your peace of mind. Writing good-enough software makes you more productive and your users happier. Building good-enough code simply advocates that users be allowed to engage in the process of deciding whether what you’ve produced is good enough.

When building good enough code, know when to stop. Don’t ruin an outstanding program by overembellishment and over-refinement. Move on, and let your code stand in its own right for a while. It may not be perfect. Don’t worry: it could never be perfect. Good software now is better than ideal software in a year.

Favourite quote from the chapter: “Great software today is often preferable to perfect software tomorrow.”

Chapter Two: A Pragmatic Approach

  1. The evils of duplication

As a programmer, you collect, organise, maintain and harness knowledge. Unluckily, learning is not constant or fixed and changes rapidly. Your understanding of a requirement may change after a meeting with the client. This means you spend most of your time trying to maintain code daily. Most people think maintenance begins when an application is launched, that maintenance means fixing bugs and enhancing features. Those people are wrong.  Maintenance is not a separate activity but a part of the development process because new requirements arrive as you design code.

The only way to develop software reliably and make your developments easier to understand and maintain is to follow the DRY principle (Don’t Repeat Yourself): Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Most of the duplication we see falls into one of the following categories: 

  • Imposed duplication. You feel you have no choice—the environment seems to require replication.
  • Inadvertent duplication. You don’t realise that you are duplicating information.
  • Impatient duplication. You get lazy and duplicate because it seems easier.
  • Interdeveloper duplication. Multiple people on a team duplicate a piece of information and do not realise it.
  1. Reversibility.

Changes don’t have to be extreme or even that immediate. But as time goes by and your project progresses, you may find yourself stuck in an untenable position. The project team commits to a smaller target with critical decisions, creating a version of reality with fewer options. The problem is that critical decisions aren’t easily reversible.

Nothing is forever, and if you strongly rely on some fact, you can almost guarantee it will change. Plan for reversibility because no decision is final. Aim for flexible code, architecture and vendor integration.

By sticking to recommendations like the DRY principle, decoupling and using metadata, you won’t have to make many critical and irreversible decisions.

  1. Tracer bullets

Tracer bullets help you find your target fast under actual circumstances and provide you and your users with feedback. A tracer code could be a single feature implemented end-to-end across all layers. A tracer code gets to the target fast and provides immediate feedback. And from a practical standpoint, they’re a relatively cheap solution. To get the same effect in code, we’re looking for something that brings us from a requirement to some aspect of the final system quickly, visibly, and repeatedly.

Advantages

  • You have an integration platform.
  • You have a better feel for progress.
  • Users get to see something working early.
  • Help build a structure to work in.

Tracer code versus prototyping

You might think that the tracer code concept is nothing more than prototyping; there is a difference. When prototyping, you aim to explore specific aspects of the final system. With an actual prototype, you will throw away whatever you lashed together when trying out the concept and recode it properly using the lessons you’ve learned. The tracer code approach discusses a different problem. You need to know how the application as a whole hangs together. You want to show your users how the interactions will work in practice and give your developers an architectural skeleton on which to hang code.

Favourite quote of the chapter: “Nothing is more dangerous than an idea if it’s the only one you have.” 

Chapter Three: The Basic Tools

  1. The power of plain text

As a Pragmatic Programmer, your base material is knowledge. You gather requirements like knowledge and then express it in your designs, implementations, tests, and documents. The best format for storing knowledge is plain text.

What Is Plain Text?

The plain text comprises printable characters so people can read and understand it directly. With plain text, you give yourself the ability to manipulate knowledge both manually and programmatically using virtually every tool at your disposal.

  1. Shell games

Every woodworker needs a suitable, solid, reliable workbench somewhere to hold workpieces at a convenient height while they work on them. 

As a programmer manipulating text files, your workbench is the command shell. Through the shell prompt, you can invoke your entire repertoire of tools. The shell prompt will enable you to launch applications, debuggers, browsers, editors, and utilities. You can search for files, query the status of the system and filter output. By programming the shell, you can build multiplex macro commands for activities you perform often.

Shell Utilities and Windows Systems

Although the command shells provided by Windows systems are improving gradually, Windows command-line utilities are still inferior to their Linux/Unix counterparts. However, all is not lost.

Cygnus Solutions has a package called Cygwin. As well as providing a Linux/Unix compatibility layer for Windows, Cygwin comes with a collection of more than 120 Unix utilities, including favourites such as 1s, grep, and find. The utilities and libraries may be downloaded and used for free, but read their license. The Cygwin distribution comes with the Bash shell.

  1. Power Editing

Tools are an extension of your hand. Well, this applies to editors more than to any other software tool. Choose an editor that will enable you to manipulate text as effortlessly as possible because the text is the primary raw material of programming.

One editor

It’s better to know one editor very well and use it for all editing tasks: code, documentation, memos, system administration and so on. With multiple editors, you face potential modern-day chaos of confusion. You may have to use the built-in editor in each language’s IDE for coding, an all-in-one office product for documentation and maybe a different built-in editor for sending e-mails. Even the keystrokes you use to edit command lines in the shell may differ. It is difficult to be proficient in any of these environments if you have a different set of editing conventions and commands in each.

Therefore choose a single editor, know it in-depth and use it for all editing tasks. Use a single editor or set of keybindings across all text editing activities. You don’t have to stop and think to accomplish text manipulation: the necessary keystrokes will be a reflex. The editor will be an extension of your hand, the keys will sing as they slice their way through text and thought. Ensure that the editor you choose is available on all platforms you use. Emacs, vi, CRiSP, Brief, and others are available across multiple platforms, often in GUI and non-GUI (text screen) versions.

Editor Features:

  • Configurable
  • Extensible
  • Programmable

Favourite quote from the chapter: “Tools amplify your talent. The better your tools, and the better you know how to use them, the more productive you can be.”

Chapter Four: Pragmatic Paranoia

  1. Design by contract

The concept of Design by Contract is a simple but powerful technique that focuses on documenting and agreeing to the rights and responsibilities of software modules to ensure program correctness. A correct program does no more and no less than it claims to do. Documenting and verifying that claim is the heart of Design by Contract. Every function and method in a software system does something. Before it starts, the routine may have some expectations of the state of the world. The expectations and claims include;

  • Preconditions: What must be valid for the routine to be called?
  • Class invariants: A class ensures that this condition is always actual from a caller’s perspective.
  • Postconditions: The fact that the routine has a postcondition implies that it will conclude: infinite loops aren’t allowed.

Implementing Design by Contract

The primary benefit of using DBC is that it forces the issue of requirements and guarantees to the forefront. Simply enumerating at design time what the input domain range is, the boundary conditions and what the routine promises to deliver or what it doesn’t promise to deliver is a giant leap forward in writing better software. When you don’t state these things, you are then programming by coincidence and this is where many projects start, finish, and fail. In languages that do not support DBC in the code, this is as far as you can go, which is not too bad. After all, DBC is a design technique.

  1. Dead programs tell no lies

It’s easy to fall into the “it can’t happen” mindset. Most of you have written code that didn’t check that a file closed successfully or that a trace statement got written as you expected. All things being equal, it’s likely that you didn’t need to, the code in question wouldn’t fail under any normal conditions. But you’re coding defensively. You’re looking for rogue pointers in other parts of your program and trashing the stack. You’re checking that the loaded versions of shared libraries are correct.

All errors give you information. You could convince yourself that the mistake can’t happen and choose to ignore it. Instead, a pragmatic programmer tells himself that something very, very bad has occurred if there is an error.

Crash, Don’t Trash

One of the benefits of detecting problems as soon as possible is that you can crash earlier.

And many times, crashing your program is the best thing you can do. The alternative may be to continue writing corrupted data to some robust database or commanding the washing machine into its twentieth consecutive spin cycle.

  1. Assertive Programming

The count can’t be negative, print can’t fail, logging can’t fail or this can never happen. Don’t practice this kind of deception, especially when coding. If It Can’t Happen, apply assertions to ensure that it won’t.

Whenever you think that “it could never happen,” add code to check it. The easiest way to do this is with the use of assertions. In most C and C++ implementations, you’ll find some form of assert or assert macro that checks a Boolean condition. These macros can be invaluable. Assertions also help to check on an algorithm’s operation. Say you’ve written a clever algorithm; assertions will check to ensure it works.

Don’t apply assertions in place of actual error handling. Assertions check for things that should never happen and make sure the assertion method you use doesn’t do any side effects that might create new errors.

Leave assertions on

People who write compilers and language environments have a common misinterpretation of assertions made widely known. It goes, “Assertions add some overhead to code. Because they check for things that should never happen, they’ll get triggered only by a bug in the code. Once the code has been tested and shipped, turn off the assertions to make the code run faster.”

Favourite quote from the chapter: “Assertions add some overhead to code. Because they check for things that should never happen, they’ll get triggered only by a bug in the code. Once the code has been tested and shipped, they are no longer needed and should be turned off to make the code run faster.”

Chapter Five: Bend or Break

  1. Decoupling and the Law of Demeter

Organise your code into modules and control the interaction between them. If one module is compromised and has to be replaced, the other modules should carry on.

Minimise Coupling

There is nothing wrong with having modules that know about each other. However, you need to be careful about how many other modules you interact with and, more importantly, how you came to interact with them. When you ask an object for a specific service, you’d like the service to execute on your behalf. You do not want the object to give you a third-party entity that you have to deal with to get the required service.

The law of Demeter

The law of Demeter for functions states that any method of an object should call only processes belonging to itself, any parameters passed into the method, any objects it created and any directly held compound objects. The law of Demeter aims at minimising coupling between modules in any given program; it prevents you from reaching into an object to gain access to a third object’s methods. Writing “shy” code honours the Law of Demeter and achieves your objective of minimising coupling between modules.

  1. Metaprogramming

Details mess up your perfect code significantly if they change frequently. Each time you have to go in and change the code to fit in with some change in business logic, in the law, or management’s tastes of the day, you run the risk of breaking the system by introducing a new bug.

Enough with the details! Get them out of the code. While you’re at it, you can make your code highly configurable and “soft.” That is, easily adaptable to changes.

Dynamic Configuration

Configure, don’t integrate. You ought to make your systems highly configurable. Not just screen colours or prompt text, but deeply ingrained items like the choice of algorithms, database products, middleware technology and user-interface style should be applied as configuration options and not through integration or engineering.

When to configure

Represent your configuration metadata in plain text; it makes life that much easier. But when should a program read this configuration? Many programs scan such things only at startup, which is unfortunate because you‘ll be forced to restart the application when you need to change the layout. A more flexible approach is to write programs that reload their configuration while running. Though this flexibility comes at a cost, it is more complex to implement.

So consider how users will use your application: if it is a long-running server process, you will want to provide some way to reread and apply metadata while the program is running. For a small client GUI application that restarts quickly, you may not need to.

  1. Temporal coupling

 Here, the writer talks about time as a design element of the software itself. There are two aspects of time that are important to you: concurrency (things happening simultaneously) and ordering (the relative positions of things in time). During temporal coupling, coexistence is always called before ordering; you can only run one report at a time; after, wait for the screen to redraw before the button click is received.

Favourite quote from the chapter: “No amount of genius can overcome a preoccupation with detail.”

Chapter Six: While you’re coding

  1. Programming by coincidence

There are hundreds of traps just waiting to catch you each day as a developer. Remember to be cautious of drawing false conclusions. Avoid programming by coincidence, relying on luck and accidental successes in favour of programming deliberately. When you program by coincidence and your code fails, you won’t know why it failed because you didn’t know why it worked in the first place provided the limited testing you did.

Accidents of Implementation

Accidents of implementation are things that happen simply because that’s the way the code is written. You end up relying on undocumented errors or boundary conditions. The boundary condition you rely on may just be an accident. In different circumstances (a different screen resolution, perhaps), might behave differently.

Undocumented behaviour may change with the next release of the library.

How to program deliberately

  • Don’t code blindfolded.
  • Rely only on reliable things. Don’t depend on accidents or assumptions.
  • Document your assumptions.
  1. Algorithm Speed

As a pragmatic programmer, you estimate algorithms’ resources, such as time, processor and memory. Most nontrivial algorithms handle some kind of variable input; the size of this input will impact the algorithm. The larger the information, the longer the running time or the more memory is used.

You will find that you subconsciously check the runtime and memory requirements whenever you write anything containing loops or recursive calls. This process is a quick confirmation that your actions are sensible in the circumstances. Therefore, you find yourselves executing a more detailed analysis. That’s when the O() notation comes in useful. The O() notation is a mathematical way of dealing with approximations.

  1. Refactoring

Code needs to evolve, it’s not static. As a program develops, it becomes necessary to rethink earlier decisions and rework portions of the code. Rewriting, restructuring and rearchitecting an existing body of code, altering its internal structure without changing its external behaviour is known as refactoring.

When Should You Refactor?

Say you come across a stumbling block because the code doesn’t quite fit anymore or you notice two things that you should merge, don’t hesitate to change it. Here are several things that may cause code to qualify for refactoring:

Duplication. You’ve discovered a violation of the DRY (Don’t Repeat Yourself) principle.

Nonorthogonal design. You’ve discovered some code or procedure that could be made more orthogonal (Orthogonality).

Outdated knowledge. Things change, requirements drift, and your understanding of the problem increases. The code needs to keep up.

Performance. You need to move functionality from one area of the system to another to improve performance.

  1. Code that’s easy to test

In more complex chips and systems, hardware developers include features like a complete Build-In Self Test (BIST) which runs base-level diagnostics internally and Test Access Mechanism (TAM) which provides a test harness that enables the external environment to provide stimuli and collect responses from the chip. You can do the same thing in software, build testability into the software from the beginning, and test each piece in detail before trying to wire them together.

Unit Testing

Chip-level testing for hardware is equivalent to unit testing in software. Testing is done on each module in isolation to verify its behaviour. You can better understand how a module will react in the big wide world once you’ve tested it thoroughly under controlled conditions.

A software unit test is a code that exercises a module. Usually, the unit test will set up an artificial environment and then evoke routines in the module being tested. The unit test then checks the returned results, either against known values or against the results from the previous runs of the same test.

Later, when you put together your “software Integrated Circuits” into a complete system, you’ll have confidence that the individual parts work as expected. Then you can use the same unit test facilities to test the system as a whole.

Favourite quote of the chapter: “All software you write will be tested—if not by you and your team, then by the eventual users—so you might as well plan on testing it thoroughly.”

Chapter Seven: Before the project

  1. The Requirement pit

Requirements gathering is an early stage of the project. Gathering implies the requirements are already there, you need to find them merely. Put them in your basket and get on your way. Although it doesn’t work like that, requirements rarely surface. They’re buried deep beneath layers of assumptions, misconceptions and politics.

Digging for requirements

How will you identify a genuine requirement when digging through the surrounding dirt? The answer is both complex and straightforward. The straightforward answer is a statement of something that needs to be accomplished. On the other hand, very few requirements are clear-cut, making requirements analysis complex.

In case the requirement is stated as “Only personnel can view an employee record,” then you’ll end up coding a straightforward test every time the application accesses these files.  However, if the statement is “Only authorised users may access an employee record,” the developer will probably design and implement some kind of access control system. When policy changes, only the metadata for that system will need to be updated. Gathering requirements in this way leads you to a system that is well-factored to support metadata.

  1. Solve impossible puzzles

Consider real-world puzzles that turn up as Christmas presents or at garage sales. You have to remove the ring or fit the T-shaped pieces in the box. You pull the ring and quickly discover that the apparent solutions won’t solve the puzzle. The answer lies elsewhere. The secret to solving the puzzle is identifying the real constraints and finding a solution therein. Some constraints are absolute, others are merely preconceived notions. Fundamental constraints must be honoured, however distasteful or stupid they may appear to be. On the other hand, some apparent constraints may not be accurate.

Don’t Think Outside the Box— Find the Box

When facing an intractable problem, enumerate all the possible avenues you have before you.

Don’t dismiss anything, no matter how unusable or stupid it sounds. Go through the list and explain why you cannot take a particular path. Can you prove it?

  1. Not until you’re ready

When you feel persistent doubt or reluctance when facing a task, pay attention to it. You may not be able to figure out what exactly is wrong. Give it time and your doubts will probably crystallise into something more solid, something you can address. Software development is still not a science. Let your instincts contribute to your performance.

Good Judgment or Procrastination?

Starting a new project or even a new module in an existing project can be a painful experience. Many would prefer to put off making the initial commitment of starting. So how can you tell when you’re simply procrastinating rather than responsibly waiting for all the pieces to fall into place?

In these circumstances, a technique that will work for you is prototyping. Select an area you feel will be difficult and begin producing some proof of concept. Shortly after, you will think that you’re wasting your time. This boredom is probably a good indication that your initial reluctance was just a desire to put off the commitment to start. Give up on the prototype, and hack into the actual development.

Favourite quote of the chapter: “Perfection is achieved, not when there is nothing left to add, but when there is nothing left to take away….”

Chapter Eight: Pragmatic Projects

  1. Ruthless testing

Test early, often test and test automatically. You should start testing as soon as you have the code. Develop elaborate test plans for your projects. Teams that use automated tests have a much better chance of success. Tests that run with every build are much more effective than test plans on a shelf. Remember, the earlier you find a bug, the cheaper it is to remedy. “Code a little, test a little.”

For your project to be satisfactory, it must have more test code than production code. The time lost when producing this test code is worth the effort and cheaper in the long run. You also stand a chance of creating a product with close to zero defects.

  1. Great expectations

The success of your team’s project is measured by how it meets the expectations of its users. If your project falls below their expectations, then it’s considered a failure regardless of how good the deliverable is in absolute terms.

The extra mile

Gently exceed your users’ expectations. Don’t scare your users; instead, surprise them. Give them a little bit more than they were expecting. The extra effort required to add some user-oriented feature to the system will pay for itself time and time again in goodwill.

  1. Pride and prejudice

As a pragmatic programmer, you shouldn’t run away from responsibility. But rather rejoice when facing challenges and make your work expertise well known. If you’re responsible for a design or a piece of code, you’ll do a job you’re proud of.

Sign your work

Have pride in ownership. “You wrote this and you should stand behind your work.” Your signature should come to be recognised 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—a professional job done by a real professional.

Favourite quote from the chapter: “Civilization advances by extending the number of important operations we can perform without thinking.”

HOW THIS BOOK CAN HELP SOFTWARE DEVELOPERS?

“The Pragmatic Programmer” by Andrew Hunt and David Thomas is a classic software development book that offers practical advice and techniques to improve the way developers work. It covers topics such as debugging, testing, automation, and project management, as well as the importance of communication and continuous learning. By following the book’s guidance, developers can improve their coding skills, increase productivity, work more efficiently, become more effective team members and deliver better software products.

DevologyX OÜ
Harju maakond, Tallinn, Lasnamäe
linnaosa,
Vaike-Paala tn 1, 11415

+372 6359999
[email protected]
DevologyX Limited
Nakawa Business Park
Kampala
Uganda

+256206300922
[email protected]