In October 2011 I read The Pragmatic Programmer: From Journeyman to Master by Andrew Hunt and David Thomas. This article is a summary of the major points I picked up.
Read this book if you are serious about software engineering. Some of the sections apply to other areas of life too: all skills are progressed with small continuous improvements -- the Kaizen principle -- and over time these improvements accumulate, resulting in proficiency then mastery.
Being a pragmatic programmer
The book idealizes the idea of the pragmatic programmer. These noble individuals are inquisitive, think critically and care about their craft (including adopting early). Instead of making excuses, they provide solutions to problems and take responsibility for mistakes.
Programming is an intellectual activity and the value of a programmer is based on their knowledge portfolio. The portfolio must be built and maintained with regular investment and diversification: read at least one technical book per quarter and learn a new programming language every year; learn more and emerging technologies.
If you want to be a pragmatic programmer, actively participate in local user groups for career opportunities, and always think critically about what you read and hear. Develop catalysing improvements and let teammates marvel and join in instead of communicating without action. Your signature should be an indicator of quality.
Communication is imperative
Communicating is an important part of a programmer's role and effective communication results in influential positions. When discussing anything, always listen first. Before beginning your communication, plan and refine your statement/ideas to have the optimum value for your target. This includes adjusting the style of delivery to suit their understanding. Choose the moment of communication wisely, understanding audience needs/priorities. If in doubt, ask about their preferred delivery style. To understand an audience, learn the WISDOM acrostic:
- 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?
Rapidly respond to all emails and voicemails, even if it has to be a simple reply or that you will get back to them later.
Tools for pragmatic programmers
Tools amplify talent: productivity soars when you use a single editor well and understand the command shell. Always use source code control and utilize it to produce automatic, repeatable product builds. Binary formats divorce data from meaning; text formats are resistant to obsolescence and can leverage most computing tools.
Knowing a text manipulation language like Perl is useful for active code generation (generating structural source code from schemas) and passive code generation (create new source files, save typing).
Successful systems are those that meet user requirements, thus use users to help decide whether a system is good enough to ship. Gently exceed your users expectations and delight them. Fix or comment on broken code immediately to prevent the Broken Window trigger mechanism, where the first signs of neglect cause entropy and decay.
A requirement is a statement of something that needs to be accomplished. Document business policies away from the more generic requirement, as policies change frequently and may end up as metadata in the final application. Solve the business problem and don't just meet the stated requirements. Work with a user to think like a user. Good requirements documents remain abstract as the simplest statements represent the business need best. Requirements are need. Abstractions live longer than details.
Point out the cost of each new feature against the project schedule to the sponsors of the project. This provides an accurate picture of requirements growth Use a project glossary that contains the specific terms you use. When faced with an intractable problem enumerate all avenues, even though that seem daft. Some things are better done than described. With specifications there is a point of diminishing or negative returns as they become more detailed. Don't be a slave to formal methods because these generally require explanation to end users (UML). Understand the whole system instead of specializing.
Estimate to avoid surprises. The best estimate comes from someone who has already created a similar project. Create a model of the product, break it into components with parameters, and give each parameter a value. Record the estimates and compare them to measured values as the product is created. The schedule should be iterated with the code to provide the most accurate scheduling estimate. Take time with estimates instead of producing them off-the-top of your head.
Assumptions that are not based on well-established facts are the bane of all projects. Do not program by coincidence, document assumptions and test them. Don't let existing code dictate future code, be ready to refactor because the impact will be less than cost of not making the change. Refactor when there is duplication, nonorthogonal design, outdated knowledge, or to improve performance. It must be undertaken slowly, deliberately and carefully. Have good tests before you begin refactoring. Always take short deliberate steps.
Team orthogonality is inversely proportional to the number of people who have to be involved when discussing changes, separating responsibilities increases efficiency. There are no final decisions in software construction and team leaders that ignore this have their eyes forcibly opened when the future arrives. Organize teams around functionality not job functions.
Pragmatic software principles
The DRY principle (Don´t Repeat Yourself) states that every piece of knowledge in a system must have a single authoritative representation. Low-level comments violate this principle by duplicating the information held in the code. Taking short cuts makes for long delays. To reduce interdeveloper duplication have a technical and capable team leader with a clear design for the system, so that code can be reused.
Aim for orthogonal components. When components are orthogonal, their execution is independent, providing more combined functionality. Modules should be self-contained, independent and have a well-defined purpose. When creating classes, normalize data representations according to the business model and use accessor functions to decouple the internal class data from external calls. Singletons can lead to unnecessary linkage when they are used as global variables.
Put abstractions in code and details in configurations, outside the code base. Configuration files allow an application to be customized without recompilation, extremely important in critical live systems and useful when distributing the same core app to different clients (only changing metadata). Long running processes should have a method to dynamically reload metadata while running to prevent downtime.
Be careful about how many modules you interact with and how you came about interacting with them. The Law of Demeter prohibits accessing a third object's modules and reduces errors by reducing the response set size (direct function invocations). Instead of digging through a hierarchy, ask for what you need directly. Note that performance can sometimes be improved by coupling modules, sharing information about class internals.
Tracer bullets are partially functioning code modules that can be adjusted iteratively with feedback from users. They provide the client with early demonstrations and can be changed rapidly, being lightweight compared to a full (perhaps incorrect) implementation.
Prototypes are disposable modules that reduce the cost for correcting design mistakes by analyzing and exposing risk at the start of a project. Anything critical or difficult should be prototyped. Prototypes do not have to be correct, complete, robust or styled. With architectural prototypes, ensure that module responsibilities and collaborations are well defined and that coupling is minimized. Estimate the order of the algorithms and test these estimations against measured data from the prototype.
Domain languages can be used express business needs and allow programming close to the problem domain; this results in specialised code that is adaptable to changing business requirements. Domain languages can be compiled using existing tools like Yacc/Lex into C or another target language. Complex workflow requirements can be encoded in a rule-based (expert) system embedded in the application so it can be configured by writing rules instead of code.
Use automatic procedures like cron and makefiles. Web content should be generated automatically from information in the repository and published without human intervention. Teams with automated tests have a better chance of success.
Comments should discuss why something is done. Code is read more times than it is written, spend time writing clear commented code (sensible variable names). Always put a date on documentation so users can see when it is updated.
Concurrency and resources
Always design for concurrency. An important part of this is decoupling time and order dependencies. The hungry consumer model provides quick and dirt load balancing. Resources should always be allocated in the same order and freed in the opposite order to reduce the possibility for deadlock.
Events are used to signal changes about an object minimizing coupling with the other interested objects (thereby increasing reversibility). To prevent spamming use a publish/subscribe protocol. In MVC, models have no direct knowledge of views or controllers, views subscribe to changes in the model and logical events from the controller, and the controller controls the view and provides the model with new data. Always consider having a separate debugging view.
Blackboard solutions decouple objects completely: shared information is stored in a central place so that contributors and arrival order is irrelevant (based on tuples spaces). JavaSpaces are an example, supporting read/write/take/notify operations.
Do not trust or expect yourself to write perfect software, you cannot. Debugging is just problem solving. When a bug occurs do not panic and prioritize on fixing the problem. The fault may be several steps removed. The best way to start fixing a bug is to make it reproducible; this also means that you know when it is fixed. Data visualization can reveal far more than simple data display and rubber ducking, explaining the problem to another individual, can cause the solution to become apparent. Usually the fault lies with your code not well tested 3rd party software. Once the bug is fixed, determine why it was not caught earlier.
Design and deployment
Design by contact verifies that is a program is correct, that it does no more and no less than it claims. It also forces requirements to the forefront. Each contract contains preconditions, postconditions and class invariants. The latter two are guaranteed if the preconditions are obeyed. Attempt to write lazy code, code that has strict preconditions and return the minimum possible. Assertions can partially implement contracts in languages without native support. Crash early at the site of the problem.
Errors should always be on the side of not processing a transaction instead of duplicating it, err on the side of the consumer. Always have a default case, because the impossible can happen. The code is no longer viable if the impossible just happened so it must be terminated.
Always leave error checking (assertions) turned on. Use exceptions for exceptional problems.
Build testing into the software from the beginning then test each piece of the project independently, then together. Unit testing should attempt to ensure that a given unit honors its contract. When you design a module or even a single routine you should design both its contract and the code to test that contract. Accessible test code not only provides an invaluable example of code use, but also a means to test regressions and validate future changes.
Develop a standard testing harness for the project. Test early, often and automatically.
Unit testing verifies individual modules. Integration testing shows major subsystems interact successfully. Regression tests compare current output with previous (known) values. Use real world and synthetic data to test and stress boundary conditions. Saboteurs can be used to test testing. Coverage analysis tools can determine the lines of code that are hit with tests. If a bug slips through the net, add a new test to trap it next time -- find bugs once.
- "It's easier to ask for forgiveness than it is to get permission" Grace Hopper
- "An investment in knowledge always pays the best interest." Benjamin Franklin
- "The limits of my language are the limits of my mind." Ludwig Wittgenstein
- "Perfection is achieved, not when there is nothing more to add, but when there is nothing left to cut away." Antoine de Saint-Exupéry
The Pragmatic Programmer should be a constant figure on the bookshelf (buy from Amazon). This short summary does not do it justice. I hope you enjoy it as much as I have.