Pragmatic Programmer Notes
The Pragmatic Programmer:
From Journeyman to Master
By Andrew Hunt and David Thomas
Notes:
Chapter 0: Preface
"Kaizen" is a Japanese term that captures the concept of continuously making many small improvements.
Chapter 1: A Pragmatic Philosophy
Your knowledge portfolio
- Invest regularly
- Diversify
- Manage risk
Goals
- Learn at least one language every year
- Read a technical book each quarter
- Read nontechnical books too
- Take classes
- Participate in local user groups
- Experiment with different environments
- Stay current (trade magazines and journals)
- Get wired
Communicate!
Use WISDOM:
- What do you want them to learn?
- What is their Interest in what you've got to say?
- How Sophisticated are they?
- How much Detail do they want?
- Whom do you want to Own the information?
- How can you Motivate them to listen to you?
Chapter 2: A Pragmatic Approach
Orthoganality
In computing, the term has come to signify a kind of independece or decoupling.
Two or more things are orthogonal if changes in one do not affect any of the others.
(Developers) may use words such as modular, component-based, and layered to describe the process.
Tracer bullets:
- Users get to see something that works early
- Developers build a structure to work in
- You have an integration platform
As the system is connected end-to-end, you have an environment to which you can add new pieces of code. - You have something to demonstrate
- You have a better feel for progress
How to Use Prototypes
When building a prototype, what details can you ignore?
- Correctness
- Completeness
- Robustness
- Style
Estimating
Build a model of the system
- Build a rough and ready bare-bones mental model
- For a project, the model may be the steps that your organization uses during development, along with a rough picture of how the system might be implemented
- The process of building the model leads to discoveries about the underlying patterns and processes that weren't apparent on the surface.
- Building the model introduces inaccuracies into the estimating process.
You are trading off model simplicity for accuracy
- Discover the mathematical rules that describe how these components interact
- The trick is to work out which parameters have the most impact on the result, and concentrate on getting them about right.
- As the system get more complex you'll want to hedge your answers
- Run multiple calculations, varying the values of the critical parameters, until you work out which ones really drive the model.
Chapter 4: Pragmatic Paranoia
Design by Contract
- Be strict in what you will accept before you begin, and promise as little as possible in return
Chapter 5: Bend, or Break
Metaprogramming
- Metadata is any data that describes the application
- Typically, metadata is accessed and used at runtime, not at compile time
- Metadata can be expressed in a manner that's much closer to the problem domain
Temporal Coupling
- Design using services- independet concurrent objects behind well-defined, consistent interfaces.
- Design for concurrency
- Concurrency may reveal time-based dependencies
- Any global or static variable must be protected from concurrent access
- Objects must always be in a valid state when called.
Since in a concurrent system the object may be called at any time you must ensure that it is in a valid state at all times.
It's Just a View
- Event- simply a special message that says "something interesting just happened". Signals changes in one object that others might be interested in.
Using events in this way minimizes coupling between those objects- the sender of the event doesn't need to have any explicit knowledge of the receiver. - Publish/Subscribe
- Objects should be able to register to receive only the events they need, and should never be sent events they don't need.
- Variations on Pub/Sub:
- Peer-to-peer
- Software bus- A centralized object maintains the database of listeners and dispatches messages appropriately
- And others... ex) critical events broadcast to all listeners, whether subscribed or not
- Model-View-Controller
- Model- Data itself, with common operations to manipulate it
The abstract data model representing the target object. The model has no direct knowledge of any views or controllers. - View- Representations of some aspect of the model
A way to interpret the model. It subscribes to changes in the model and logical events from the controller. - Controller- Manipulates aspects of the view
A way to control the view and provide the model with new data. It publishes events to both the model and the view.
- Model- Data itself, with common operations to manipulate it
Chapter 6: While you are coding
How to Program Deliberately:
- Always be aware of what you are doing.
- Don't code blindfolded. Attempting to build an application you don't fully understand, or use a technology you aren't familiar with, is an invitation to be mislied by coincidences.
- Proceed from a plan
- Rely only on reliable things. If you aren't sure, assume the worst
- Document your assumptions. Design by contract can help.
- Don't just test your code, but test your assumptions as well.
- Prioritize your effort.
- Don't be a slave to history. Don't let existing code dictate future code. Be ready to refactor.
Algorithm Speed
Most significant algorithms are not linear.
Good new: Many, like a binary search, are sublinear.
Bad news: Others are considerabbly worse than linear.
The O() Notations
- O() notation is a mathematical way of dealing with approximations.
- Think of `O` as meaning `order of`
- Puts and upper bound on the value of the thing we are measuring.
Something that is O(n^2) at the upper bound will not grow faster than n^2 - Sometimes we come up with fairly complex O() notation functions, but because the highest-order term will dominate the value as n increases, the conventions is to remove all low-order terms and not to bother showing any constant multiplying factors.
O((n^2)/2) => O(n^2) - This is a weakness of O() notation, one O(n^2) may be 100x faster than another, but you won't know it from the notation.
- Simple loops- ex) max value in array, generating checksums, exhaustive searches: O(n)
- Nested loops- ex) bubble sort (O(n^2)): O(m x N) where m and n are the two loops' limits.
- Binary Chop- ex) binary search of a sorted list, traversing a binary tree: O(lg(n))
- Divide and Conquer- ex) quicksort and other algorithms that partition their input, work on the two halves independently, and then combine the results O(nlg(n))
- Combinatoric- ex) traveling salesmen, optimally packed container (problems described by factorials)
Refactoring
- Though software development often compared to construction, it is more like gardening
- When should you refactor?
- Duplication
- Nonorthogonal design
- Outdated knowledge
- Performance
- A medical analogy: Think of the code that needs refactoring as a "growth". Removing it requires invasive surgery . You can go in now, and take it out while it is still small. Or, you could wait while it grows and spreads- but removing it will be both more expensive and more dangerous.
- How do you refactor?
- Don't refactor and add functionality at the same time
- Make sure you have good tests before refactoring and run them often
- Take short, deliberate steps.
- It can also be helpful to make sure that drastic changes to a module- such as altering its interface or its functionality in an incompaticle manner- break the build. That is, old clients of this code should fail to compile. You can then quickly find the old clients and make the necessary changes to bring them up to date.
Code That's Easy to Test
- Ad Hoc Testing- During debugging, we may end up creating some particular tests on-the-fly....
At the end of the debugging session, you need to formalize the ad hoc test. If the code broke once, it is likely to break again. Don't just throw away the test you created; add it to the existing unit test.
Chapter 7: Before the Project
Documenting Requirements
- use cases to capture requirements
- One way of looking at use cases is to emphasize their goal-dirven nature
- Where requirements are concerned, the simplest statement that accurately reflects the business need is best.
Requirements are not architecture/design/the user interface. Requirements are need. - The key to managing growth of requirements is to point out each new feature's impact on the schedule to the project sponsors.
...it can be helpful to have an accurate, complete picture of how, and when, requirements growth occured. - Maintain a project glossary- one place that defines all the specific terms and vocabulary used in a project.
Solving Impossible Puzzles
- The secret to solving the puzzle is to identify the real (not imagined) constraints, and find a solution therein.
- "thinking outside the box" encourages us to recognize constraints that might not be applicable and ignore them. But this phrase isn't entirely accurate. If the "box" is the boundary of constraints and conditions, then the trick is to find the box, which may be considerably larger than you think.
- Categorize and prioritize your constraints. Identify the most restrictive constraints first, and fit the remaining constraints within them.
- There must be an easier way!
- 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?
The Specification Trap
- Program specification is the process of taking a requirement and reducing it donw to the point where a programmer's skill can take over. Communicate/explain/clarify the world in sucha a way as to remove major ambiguity. A record for future developers and maintainers. Codifies user needs and contract that the final system will be in line with that requirement.
- As a pragmatic programmer you should tend to view requirements gathering, design, and implementation as different facets of the same process- the delivery of a quality system. Distrust environments where requirements are gathered, specs written, and then coding starts, all in isolation.
Chapter 8: Pragmatic Projects
Pragmatic Teams
- Quality can come only from the individual contributions of all team memebers
- Monitor the environment for change (scope, time scales, additional features, new environments) and keep metrics
- Communicate- the team as an entity need to communicate clearly with the rest of the world.
Generate a team brand to build identity. - Don't repeat yourself- Appoint a team librarian or people as focal points for various functional aspects of work.
- Orthogonality- Traditional team organization is based on the old-fashioned waterfall method of software construction. Individuals are assigned roles based on their job function. We favor splitting teams functionally. Divide your people into small teams, each responsible for a particular functional aspect of the final system. We're looking for cohesive, largely self-contained teams of people.
How does this functional style of organization help? Organize our resources using the same techniques we use to organize code (contracts, decoupling, orthogonality) and we help isolate the team as a whole from the effects of change. - Know when to stop adding paint- Give each team memeber the ability to shine in his or her own way. Give them just enough structure to support them and ensure that the project delivers against its requirements
Ruthless Testing
- What to test:
- Unit testing
- Integration testing
- Validation and verification
- Resource exhaustion, errors, and recovery
- Performance testing
- Usability testing
- How to test:
- Regression testing- Did you break something?
- Test data-
- Real world data- Represents typical user data. The big surprises come as you discover what typical means. This is most likely to reveal defects and misunderstandings in requirement analysis.
- Synthetic data
- You need a lot of data, possibly more than any real-world sample can provide. Might use real data as a seed to generate a larger sample size, and tweak certain fields that need to be unique.
- Data to stress boundary conditions
- Data that exhibits certain statistical properties. unsorted/presorted data, failures, etc.
- Exercising the GUI systems
- Testing the tests
- Testing thoroughly- Test state coverage, not code coverage
- When to test
- As soon as production code exists it needs to be tested.
- Test before checking code into the source repo
- Some tests may not be easily run on a frequent baiss. Stress tests may require special setup or equipment, and some hand holding. These tests may be run less often... but it is important that they be run on a regular schedule.
- Tightening the net- Find bugs once. Add a new test with every bugfix.
It's all writing
- In general, comments should discuss why something is done... code already shows how it is done, so commenting on this is redundant- and it is a violation of the DRY principle.
Chapter Appendix: Tips For the Pragmatic Programmer
- Care About Your Craft
Why spend your life developing software unless you care about doing it well? - Think! About Your Work
Turn off the autopilot and take control. Constantly critique and appraise your work. - Provide Options, Don't Make Lame Excuses
Instead of excuses, provide options. Don't say it can't be done; explain what can be done. - Don't Live with Broken Windows
Fix bad designs, wrong decisions, and poor code when you see them. - 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. - Remember the Big Picture
Don't get so engrossed in the details that you forget to check what's happening around you. - Make Quality a Requirements Issue
Involve your users in determining the project's real quality requirements. - Invest Regularly in Your Knowledge Portfolio
Make learning a habit. - Critically Analyze What You Read and Hear
Don't be swayed by vendors, media hype, or dogma. Analyze information in terms of you and your project. - It's Both What You Say and the Way You Say It
There's no point in having great ideas if you don't communicate them effectively. - DRY–Don't Repeat Yourself
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. - Make It Easy to Reuse
If it's easy to reuse, people will. Create an environment that supports reuse. - Eliminate Effects Between Unrelated Things
Design components that are self-contained. independent, and have a single, well-defined purpose. - There Are No Final Decisions
No decision is cast in stone. Instead, consider each as being written in the sand at the beach, and plan for change. - Use Tracer Bullets to Find the Target
Tracer bullets let you home in on your target by trying things and seeing how close they land. - Prototype to Learn
Prototyping is a learning experience. Its value lies not in the code you produce, but in the lessons you learn. - Program Close to the Problem Domain
Design and code in your user's language. - Estimate to Avoid Surprises
Estimate before you start. You'll spot potential problems up front. - Iterate the Schedule with the Code
Use experience you gain as you implement to refine the project time scales. - Keep Knowledge in Plain Text
Plain text won't become obsolete. It helps leverage your work and simplifies debugging and testing. - Use the Power of Command Shells
Use the shell when graphical user interfaces don't cut it. - Use a Single Editor Well
The editor should be an extension of your hand; make sure your editor is configurable, extensible, and programmable. - Always Use Source Code Control
Source code control is a time machine for your work—you can go back. - 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 still needs to be fixed. - Don't Panic When Debugging
Take a deep breath and THINK! about what could be causing the bug. - "select" Isn't Broken.
It is rare to find a bug in the OS or the compiler, or even a third-party product or library. The bug is most likely in the application. - Don't Assume It—Prove It
Prove your assumptions in the actual environment-- with real data and boundary conditions. - Learn a Text Manipulation Language.
You spend a large part of each day working with text. Why not have the computer do some of it for you? - Write Code That Writes Code
Code generators increase your productivity and help avoid duplication. - You Can't Write Perfect Software
Software can't be perfect. Protect your code and users from the inevitable errors. - Design with Contracts
Use contracts to document and verify that code does no more and no less than it claims to do. - Crash Early
A dead program normally does a lot less damage than a crippled one. - Use Assertions to Prevent the Impossible
Assertions validate your assumptions. Use them to protect your code from an uncertain world. - Use Exceptions for Exceptional Problems
Exceptions can suffer from all the readability and maintainability problems of classic spaghetti code. Reserve exceptions for exceptional things. - Finish What You Start
Where possible, the routine or object that allocates a resource should be responsible for deallocating it. - Minimize Coupling Between Modules
Avoid coupling by writing "shy" code and applying the Law of Demeter. - Configure, Don't Integrate
Implement technology choices for an application as configuration options, not through integration or engineering. - Put Abstractions in Code, Details in Metadata
Program for the general case, and put the specifics outside the compiled code base. - Analyze Workflow to Improve Concurrency
Exploit concurrency in your user's workflow. - Design Using Services
Design in terms of services—independent, concurrent objects behind well-defined, consistent interfaces. - Always Design for Concurrency
Allow for concurrency, and you'll design cleaner interfaces with fewer assumptions. - Separate Views from Models
Gain flexibility at low cost by designing your application in terms of models and views. - Use Blackboards to Coordinate Workflow
Use blackboards to coordinate disparate facts and agents, while maintaining independence and isolation among participants. - Don't Program by Coincidence
Rely only on reliable things. Beware of accidental complexity, and don't confuse a happy coincidence with a purposeful plan. - Estimate the Order of Your Algorithms
Get a feel for how long things are likely to take before you write code. - Test Your Estimates
Mathematical analysis of algorithms doesn't tell you everything. Try timing your code in its target environment. - Refactor Early, Refactor Often
Just as you might weed and rearrange a garden, rewrite, rework, and re-architect code when it needs it. Fix the root of the problem. - Design to Test
Start thinking about testing before you write a line of code. - Test Your Software, or Your Users Will
Test ruthlessly. Don't make your users find bugs for you. - Don't Use Wizard Code You Don't Understand
Wizards can generate reams of code. Make sure you understand all of it before you incorporate it into your project. - Don't Gather Requirements–Dig for Them
Requirements rarely lie on the surface. They're buried deep beneath layers of assumptions, misconceptions, and politics. - Workwith a User to Think Like a User
It's the best way to gain insight into how the system will really be used. - Abstractions Live Longer than Details
Invest in the abstraction, not the implementation. Abstractions can survive the barrage of changes from different implementations and new technologies. - Use a Project Glossary
Create and maintain a single source of all the specific terms and vocabulary for a project. - Don't Think Outside the Box–Find the Box
When faced with an impossible problem, identify the real constraints. Ask yourself: "Does it have to be done this way? Does it have to be done at all?" - Start When You're Ready.
You've been building experience all your life. Don't ignore niggling doubts. - Some Things Are Better Done than Described
Don't fall into the specification spiral—at some point you need to start coding. - Don't Be a Slave to Formal Methods.
Don't blindly adopt any technique without putting it into the context of your development practices and capabilities. - Costly Tools Don't Produce Better Designs
Beware of vendor hype, industry dogma, and the aura of the price tag. Judge tools on their merits. - Organize Teams Around Functionality
Don't separate designers from coders, testers from data modelers. Build teams the way you build code. - Don't Use Manual Procedures
A shell script or batch file will execute the same instructions, in the same order, time after time. - Test Early. Test Often. Test Automatically
Tests that run with every build are much more effective than test plans that sit on a shelf. - Coding Ain't Done 'Til All the Tests Run
'Nuff said. - Use Saboteurs to Test Your Testing
Introduce bugs on purpose in a separate copy of the source to verify that testing will catch them. - Test State Coverage, Not Code Coverage
Identify and test significant program states. Just testing lines of code isn't enough. - Find Bugs Once
Once a human tester finds a bug, it should be the last time a human tester finds that bug. Automatic testsshould check for it from then on. - English is Just a Programming Language
Write documents as you would write code: honor the DRY principle, use metadata, MVC, automatic generation, and so on. - Build Documentation In, Don't Bolt It On
Documentation created separately from code is less likely to be correct and up to date. - Gently Exceed Your Users' Expectations
Come to understand your users' expectations, then deliver just that little bit more. - Sign Your Work
Craftsmen of an earlier age were proud to sign their work. You should be, too.