The Pragmatic Programmer
David Thomas and Andrew Hunt
Highlights & Annotations
Good Design Is Easier to Change Than Bad Design
Ref. D289-A
A thing is well designed if it adapts to the people who use it. For code, that means it must adapt by changing. So we believe in the ETC principle: Easier to Change. ETC.
Ref. 8A6B-B
Why is decoupling good? Because by isolating concerns we make each easier to change. ETC. Why is the single responsibility principle useful? Because a change in requirements is mirrored by a change in just one module. ETC. Why is naming important? Because good names make code easier to read, and you have to read it to change it. ETC!
Ref. 4FA2-C
ETC Is a Value, Not a Rule
Ref. 35E4-D
We feel that the only way to develop software reliably, and to make our developments easier to understand and maintain, is to follow what we call the DRY principle:
Ref. B781-E
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Ref. B4A8-F
DRY is about the duplication of knowledge, of intent. It’s about expressing the same thing in two different places, possibly in two totally different ways.
Ref. F9C2-G
Here’s the acid test: when some single facet of the code has to change, do you find yourself making that change in multiple places, and in multiple different formats? Do you have to change code and documentation, or a database schema and a structure that holds it, or…? If so, your code isn’t DRY.
Ref. 412A-H
Not All Code Duplication Is Knowledge Duplication
Ref. F3A3-I
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.
Ref. 0B6B-J
Representational Duplication Your code interfaces to the outside world: other libraries via APIs, other services via remote calls, data in external sources, and so on. And pretty much each time you do, you introduce some kind of DRY violation: your code has to have knowledge that is also present in the external thing. It needs to know the API, or the schema, or the meaning of error codes, or whatever. The duplication here is that two things (your code and the external entity) have to have knowledge of the representation of their interface. Change it at one end, and the other end breaks. This duplication is inevitable, but can be mitigated. Here are some strategies. Duplication Across Internal APIs For internal APIs, look for tools that let you specify the API in some kind of neutral format. These tools will typically generate documentation, mock APIs, functional tests, and API clients, the latter in a number of different languages. Ideally the tool will store all your APIs in a central repository, allowing
Ref. 0D5E-K
Make It Easy to Reuse
Ref. 417E-L
What you’re trying to do is foster an environment where it’s easier to find and reuse existing stuff than to write it yourself. If it isn’t easy, people won’t do it. And if you fail to reuse, you risk duplicating knowledge.
Ref. 3D20-M
Orthogonality is a critical concept if you want to produce systems that are easy to design, build, test, and extend. However, the concept of orthogonality is rarely taught directly. Often it is an implicit feature of various other methods and techniques you learn. This is a mistake. Once you learn to apply the principle of orthogonality directly, you’ll notice an immediate improvement in the quality of systems you produce.
Ref. 2CD9-N
computing, the term has come to signify a kind of independence or decoupling. Two or more things are orthogonal if changes in one do not affect any of the others. In a well-designed system, the database code will be orthogonal to the user interface: you can change the interface without affecting the database, and swap databases without changing the interface.
Ref. 3CC6-O
When components of any system are highly interdependent, there is no such thing as a local fix.
Ref. D3FB-P
Eliminate Effects Between Unrelated Things
Ref. 3290-Q
want to design components that are self-contained: independent, and with a single, well-defined purpose (what Yourdon and Constantine call cohesion in Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design [YC79]). 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, you can be confident that you won’t cause problems that ripple through the entire system.
Ref. E538-R
Changes are localized, so development time and testing time are reduced.
Ref. 5201-S
Simple components can be designed, coded, tested, and then forgotten—there is no need to keep changing existing code as you add new code.
Ref. D511-T
An orthogonal approach also promotes reuse. If components have specific, well-defined responsibilities, they can be combined with new components in ways that were not envisioned by their original implementors. The more loosely coupled your systems, the easier they are to reconfigure and reengineer.
Ref. 4D69-U
There is a fairly subtle gain in productivity when you combine orthogonal components. Assume that one component does distinct things and another does things. If they are orthogonal and you combine them, the result does things. However, if the two components are not orthogonal, there will be overlap, and the result will do less. You get more functionality per unit effort by combining orthogonal components.
Ref. 5793-V
Reduce Risk An orthogonal approach reduces the risks inherent in any development.
Ref. 6384-W
Diseased sections of code are isolated. If a module is sick, it is less likely to spread the symptoms around the rest of the system. It is also easier to slice it out and transplant in something new and healthy. The resulting system is less fragile. Make small changes and fixes to a particular area, and any problems you generate will be restricted to that area. An orthogonal system will probably be better tested, because it will be easier to design and run tests on its components. You will not be as tightly tied to a particular vendor, product, or platform, because the interfaces to these third-party components will be isolated to smaller parts of the overall development.
Ref. AED2-X
Systems should be composed of a set of cooperating modules, each of which implements functionality independent of the others. Sometimes these components are organized into layers, each providing a level of abstraction. This layered approach is a powerful way to design orthogonal systems. Because each layer uses only the abstractions provided by the layers below it, you have great flexibility in changing underlying implementations without affecting code. Layering also reduces the risk of runaway dependencies between modules. You’ll often see layering expressed in diagrams:
Ref. 4F50-Y
There is an easy test for orthogonal design. Once you have your components mapped out, ask yourself: If I dramatically change the requirements behind a particular function, how many modules are affected? In an orthogonal system, the answer should be “one.’’[16] Moving a button on a GUI panel should not require a change in the database schema. Adding context-sensitive help should not change the billing subsystem.
Ref. 9866-Z
In an orthogonally designed system, you would need to change only those modules associated with the user interface to handle this: the underlying logic of controlling the plant would remain unchanged. In fact, if you structure your system carefully, you should be able to support both interfaces with the same underlying code base.
Ref. FD1D-A
Also ask yourself how decoupled your design is from changes in the real world. Are you using a telephone number as a customer identifier? What happens when the phone company reassigns area codes? Postal codes, Social Security Numbers or government IDs, email addresses, and domains are all external identifiers that you
Ref. CDDF-B
have no control over, and could change at any time for any reason. Don’t rely on the properties of things you can’t control.
Ref. 634E-C
When you bring in a toolkit (or even a library from other members of your team), ask yourself whether it imposes changes on your code that shouldn’t be there. If an object persistence scheme is transparent, then it’s orthogonal. If it requires you to create or access objects in a special way, then it’s not. Keeping such details isolated from your code has the added benefit of making it easier to change vendors in the future.
Ref. 0B4D-D
Every time you write code you run the risk of reducing the orthogonality of your application. Unless you constantly monitor not just what you are doing but also the larger context of the application, you might unintentionally duplicate functionality in some other module, or express existing knowledge twice.
Ref. B8AA-E
There are several techniques you can use to maintain orthogonality: Keep your code decoupled Write shy code—modules that don’t reveal anything unnecessary to other modules and that don’t rely on other modules’ implementations. Try the Law of Demeter, which we discuss in Topic 28, Decoupling. If you need to change an object’s state, get the object to do it for you. This way your code remains isolated from the other code’s implementation and increases the chances that you’ll remain orthogonal. Avoid global data Every time your code references global data, it ties itself into the other components that share that data. Even globals that you intend only to read can lead to trouble (for example, if you suddenly need to change your code to be multithreaded). In general, your code is easier to understand and maintain if you explicitly pass any required context into your modules. In object-oriented applications, context is often passed as parameters to objects’ constructors. In other code, you can create structures containing the context and pass around references to them. The Singleton pattern in Design Patterns: Elements of Reusable Object-Oriented Software [GHJV95] is a way of ensuring that there is only one instance of an object of a particular class. Many people use these singleton objects as a kind of global variable (particularly in languages, such as Java, that otherwise do not support
Ref. F5E7-F
the concept of globals). Be careful with singletons—they can also lead to unnecessary linkage. Avoid similar functions Often you’ll come across a set of functions that all look similar—maybe they share common code at the start and end, but each has a different central algorithm. Duplicate code is a symptom of structural problems. Have a look at the Strategy pattern in Design Patterns for a better implementation. Get into the habit of being constantly critical of your code. Look for any opportunities to reorganize it to improve its structure and…
Ref. 19B9-G
Testing An orthogonally designed and implemented system is easier to test. Because the interactions between the system’s components are formalized and limited, more of the system testing can be performed at the individual module level. This is good news, because module level (or unit) testing is considerably easier to specify and perform than integration testing. In fact, we suggest that these…
Ref. 4C8D-H
Bug fixing is also a good time to assess the orthogonality of the system as a whole. When you come across a problem, assess how localized the fix is. Do you change just one module, or are the changes scattered throughout the entire system? When you make a change, does it fix everything, or do other problems mysteriously arise? This is a good opportunity to bring automation to bear. If you use a version control system (and you will after reading Topic 19, Version Control), tag bug fixes when you check the…
Ref. B27A-I
truly orthogonal documentation, you should be able to change the appearance dramatically without changing the content. Word processors provide style sheets and macros that help. We personally prefer using a markup system such as Markdown: when writing we focus only on the content…
Ref. EEDE-J
Orthogonality is closely related to the DRY principle. With DRY, you’re looking to minimize duplication within a system, whereas with orthogonality you reduce the interdependency among the system’s components. It may be a clumsy word, but if you use the principle of orthogonality, combined closely with the DRY principle, you’ll find that the systems…
Ref. C8F3-K
you’re brought into a project where people are desperately struggling to make changes, and where every change seems to cause four other things to go wrong,…
Ref. AB6A-L
Consider the difference between tools which have a graphical user interface and small but combinable command-line utilities used at shell prompts. Which set is more orthogonal, and why? Which is easier to use for exactly the purpose for which it was intended? Which set is easier to combine with other tools to meet new challenges? Which set is easier to learn?
Ref. CFA3-M
Reversibility Many of the topics in this book are geared to producing flexible, adaptable software. By sticking to their recommendations—especially the DRY principle, decoupling, and use of external configuration—we don’t have to make as many critical, irreversible decisions. This is a good thing, because we don’t always make the best decisions the first time around. We commit to a certain technology only to discover we can’t hire enough people with the necessary skills. We lock in a certain third-party vendor just before they get bought out by their competitor. Requirements, users, and hardware change faster than we can get the software developed.
Ref. F25F-N
The mistake lies in assuming that any decision is cast in stone—and in not preparing for the contingencies that might arise. Instead of carving decisions in stone, think of them more as being written in the sand at the beach. A big wave can come along and wipe them out at any time.
Ref. 8442-O
There Are No Final Decisions
Ref. A2FF-P
That same principle applies to projects, particularly when you’re building something that hasn’t been built before. We use the term tracer bullet development to visually illustrate the need for immediate feedback under actual conditions with a moving goal.
Ref. 9571-Q
Look for the important requirements, the ones that define the system. Look for the areas where you have doubts, and where you see the biggest risks. Then prioritize your development so that these are the first areas you code.
Ref. 34FB-R
Have a look at the following diagram. This system has five architectural layers. We have some concerns about how they’d integrate, so we look for a simple feature that lets us exercise them together. The diagonal line shows the path that feature takes through the code. To make it work, we just have to implement the solidly shaded areas in each layer: the stuff with the squiggles will be done later.
Ref. B967-S
Tracer code is not disposable: you write it for keeps. It contains all the error checking, structuring, documentation, and self-checking that any piece of production code has. It simply is not fully functional. However, once you have achieved an end-to-end connection among the components of your system, you can check how close to the target you are, adjusting if necessary. Once you’re on target, adding functionality is easy.
Ref. 07F1-T
The tracer code approach addresses 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 you want to give your developers an architectural skeleton on which to hang code. In this case, you might construct a tracer consisting of a trivial implementation of the container packing algorithm (maybe something like first-come, first-served) and a simple but working user interface. Once you have all the components in the application plumbed together, you have a framework to show your users and your developers. Over time, you add to this framework with new functionality, completing stubbed routines. But the framework stays intact, and you know the system will continue to behave the way it did when your first tracer code was completed.
Ref. 212A-U
The distinction is important enough to warrant repeating. Prototyping generates disposable code. Tracer code is lean but complete, and forms part of the skeleton of the final system. Think of prototyping as the reconnaissance and intelligence gathering that takes place before a single tracer bullet is fired.
Ref. F262-V
We build software prototypes in the same fashion, and for the same reasons—to analyze and expose risk, and to offer chances for correction at a greatly reduced cost. Like the car makers, we can target a prototype to test one or more specific aspects of a project.
Ref. 4BAB-W
Prototypes are designed to answer just a few questions, so they are much cheaper and faster to develop than applications that go into production. The code can ignore unimportant details—unimportant to you at the moment, but probably very important to the user later on. If you are prototyping a UI, for instance, you can get away with incorrect results or data. On the other hand, if you’re just investigating computational or performance aspects, you can get away with a pretty poor UI, or perhaps even no UI at all.
Ref. DCF5-X
Prototyping is a learning experience. Its value lies not in the code produced, but in the lessons learned. That’s really the point of prototyping.
Ref. 3CF6-Y
How to Use Prototypes When building a prototype, what details can you ignore? Correctness You may be able to use dummy data where appropriate. Completeness The prototype may function only in a very limited sense, perhaps with only one preselected piece of input data and one menu item. Robustness Error checking is likely to be incomplete or missing entirely. If you stray from the predefined path, the prototype may crash and burn in a glorious display of pyrotechnics. That’s okay. Style Prototype code shouldn’t have much in the way of comments or documentation (although you may produce reams of documentation as a result of your experience with the prototype).
Ref. 1290-Z
Prototypes gloss over details, and focus in on specific aspects of the system being considered, so you may want to implement them using a high-level scripting language—higher than the rest of the project (maybe a language such as Python or Ruby), as these languages can get out of your way. You may choose to continue to develop in the language used for the prototype, or you can switch; after all, you’re going to throw the prototype away anyway.
Ref. CC9B-A
you can prototype on a whiteboard, with Post-it notes or index cards. What you are looking for is how the system hangs together as a whole, again deferring details. Here are some specific areas you may want to look for in the architectural prototype: Are the responsibilities of the major areas well defined and appropriate? Are the collaborations between major components well defined? Is coupling minimized? Can you identify potential sources of duplication? Are interface definitions and constraints acceptable? Does every module have an access path to the data it needs during execution? Does it have that access when it needs it? This last item tends to generate the most surprises and the most valuable results from the prototyping experience.
Ref. 3C02-B
The problem with most binary formats is that the context necessary to understand the data is separate from the data itself. You are artificially divorcing the data from its meaning. The data may as well be encrypted; it is absolutely meaningless without the application logic to parse it. With plain text, however, you can achieve a self-describing data stream that is independent of the application that created
Ref. F7B0-C
For a programmer manipulating files of text, that workbench is the command shell. From the shell prompt, you can invoke your full repertoire of tools, using pipes to combine them in ways never dreamt of by their original developers. From the shell, you can launch applications, debuggers, browsers, editors, and utilities. You can search for files, query the status of the system, and filter output. And by programming the shell, you can build complex macro commands for activities you perform often.
Ref. 670B-D
Parameterize Your App Using External Configuration
Ref. C2EB-E
Port, IP address, machine, and cluster names the app uses Environment-specific validation parameters Externally set parameters, such as tax rates Site-specific formatting details License keys Basically, look for anything that you know will have to change that you can express outside your main body of code, and slap it into some configuration
Ref. C4BB-F
Configuration-As-A-Service While static configuration is common, we currently favor a different approach. We still want configuration data kept external to the application, but rather than in a flat file or database, we’d like to see it stored behind a service API. This has a number of benefits: Multiple applications can share configuration information, with authentication and access control limiting what each can see Configuration changes can be made globally The configuration data can be maintained via a specialized UI The configuration data becomes dynamic
Ref. 7E7C-G
Don’t Write Dodo-Code Without external configuration, your code is not as adaptable or flexible as it could be. Is this a bad thing? Well, out here in the real world, species that don’t adapt die. The dodo didn’t adapt to the presence of humans and their livestock on the island of Mauritius, and quickly became extinct.[45] It was the first documented extinction of a species at the hand of man.
Ref. 9CBF-H