Code Like Pragmatic Programmer
A pragmatic philosophy
PP_Tip 1: Care about your craft
- Why spending your life developing software unless you care doing it well?
PP_Tip 2: Think! About your work
- Turn off the autopilot and take control
- Constantly critique and evaluate your work
PP_Tip 3: Provide options, don't make lame excuses
- Things go wrong:
- Deliverables are late
- Unforeseen technical problems come up
- Specs are misunderstood
-
...
-
Don't be afraid to admit ignorance or error
- Before saying that something cannot be done, consider what the other people are likely to say
- "Have you tried this?"
- "Did you consider that?"
- ...
Broken windows
- = bad designs, wrong decisions, poor code, rotten software
PP_Tip 4: Don't live with broken windows
- A broken window left un-repaired instills a sense of abandonment
- Don't live with broken windows un-repaired
- If there is no time, board it up
- Take action to show that you are on top of the situation
PP_Tip 5: Be a catalyst for change
- Sometimes you know what is right, but if you ask for permissions you will be slowed down
- Request for feedback
- Committees, budgets, ...
-
It's the so-called "startup fatigue"
-
It's easier to ask forgiveness, than it is to get permission
PP_Tip 6: Remember the big picture
- Projects slowly and inexorably get totally out of hand
- Missing a deadline happens one day at a time
PP_Tip 7: Make quality a requirement issue
- One of the requirements from the user should be "how good do you want the software to be?"
- Good software today is better than perfect software tomorrow
PP_Tip 8: Invest regularly in your knowledge portfolio
- Your knowledge and experience are your most important professional assets
-
Unfortunately they are expiring assets
-
It's like investing for retirement
-
Invest as a habit
- Study every day
- Diversify
- The more different things you know, the more valuable you are
- Balance conservative and high-risk / high-reward investments
- Don't put all the technical eggs in one basket
- Review and rebalance periodically
PP_Tip 9: Critically analyze what you read and hear
- Beware of media hype
- Beware of zealots who insist that their dogma provides the only answer
- A best seller book is not necessarily a good book
PP_Tip 10: It's both what you say and the way you say it
- Plan what you want to say: write an outline
- Choose the right moment
- Choose a style; if in doubt, ask)
- Make it look good
- Involve your audience in early drafts
- Be a listener
- Get back to people
PP_Tip 10: Remember 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?
A pragmatic approach
PP_Tip 11: DRY - Don't repeat yourself
- Every piece of information must have a:
- Single
- Unambiguous
-
Authoritative representation within a system
-
Information is duplicated in multiple places -> maintenance nightmare
PP_Tip 11: Programs = knowledge
- Programs are made of knowledge (e.g., requirements, specifications, code, internal and external documentation)
- Knowledge is unstable
- Over time one gets better understanding of requirements
- Requirements change
- The solution to a problem changes over time
- E.g., tests show that an algorithm is not general or does not work
PP_Tip 11: How does duplication arise?
-
There are 4 I's of duplication:
-
Imposed
- The environment seems to require duplication
- Inadvertent
- A developer does not realize that he/she is duplicating information
- Impatient
- A developer gets lazy and duplicates code because it is easy
- Inter-developer
- Multiple developers duplicate a piece of info
PP_Tip 11: Imposed duplication
- Multiple representations of the same piece of info
- E.g., same info in two pieces of code written in different languages
-
Solution: write a filter or code generator
-
Internal documentation and code
- Solution: comments should not duplicate info present in the code
-
Comments should be for why, code for how
-
External documentation and code
-
Solution: be careful, generate it automatically
-
Language issues
- Some languages require duplication (e.g., C/C++ header vs cpp)
- Solution: at least warnings and errors are reported
- Do not duplicate comments in headers and code
PP_Tip 11: Inadvertent duplication
- Un-normalized data structures
- E.g.,
- A
Truck
has a type, maker, a license plate, and a driver - A
DeliveryRoute
has a route, a truck, and a driver - Now if the driver of a truck is changed, this info needs to be changed in two objects
-
Solution: use a 3rd object to bind
DeliveryRoute
,Truck
,Driver
-
Data elements that are mutually dependent for perf reason
- E.g., a line has
startPoint
andendPoint
, and a fieldlength
is redundant - Solution: use accessors to read/write object attributes
PP_Tip 11: Impatient duplication
- The temptation is always to cut-and-paste code and then modify it
- It increases the technical debt: shortcuts end up in long delays
- Solution: re-factor and then change it
PP_Tip 11: Inter-developer duplication
-
It is hard to detect
-
Solutions:
- Encourage communication
- Do code reviews
- Project libraries: central place for utility and scripts (so one knows where to look before writing a routine)
- Project librarian to supervise the reuse
PP_Tip 12: Make it easy to reuse
- If something is not easy to find, use, reuse, people won't reuse
Orthogonality
- = independence, decoupling
PP_Tip 13: Eliminate effects between unrelated things
- Systems are orthogonal when changes in one sub-system don't affect other sub-systems
- E.g., helicopter controls are not orthogonal
PP_Tip 13: Orthogonality: pros
- Easier to change
- Changes are localized
- Changes are easier to be unit tested
- Gain productivity
- Less overlap means more useful function per unit of code
- Promote reuse
- Reduce risk
- Easier to change design
- Not being tied to a particular vendor / product / platform
PP_Tip 13: How to maintain orthogonality
- Avoid global state (e.g., singletons)
- Write shy code
- Modules / objects that don't reveal anything unnecessary
- Pass required context to modules, objects, functions
- Avoid coupling (e.g., law of Demeter)
- Always look for opportunities to refactor
- Reorganize
- Improve structure
- Increase orthogonality
PP_Tip 14: There are no final decisions
- Do not carve decisions into stone
- Think of them as being written on the sand
PP_Tip 14: There are no final decisions: why
- Requirements can change on us
- The first decision is not always the best one
- Change 3rd party components
PP_Tip 15: Use tracer bullets to find the target
- Ready, fire, aim!
Analogy between shooting bullets in the dark and software engineering
-
How to shoot a bullet to a target in the dark?
-
Open loop approach
- Compute all the interesting quantities (wind, angles, distance)
- Hope that the conditions don't change at all
-
Closed loop approach
- Use tracer bullets
- They leave trace in the dark so one can see if they are hitting the target
- If not, adjust the aim
- If they hit the target, other bullets will also hit the target
-
Software engineering:
-
Open loop approach
- Specify the system to death
- Closed loop approach
- Achieve end-to-end connection among components with minimal functionality
- Then adjust, re-aim, ... until you are on target
PP_Tip 15: Tracer bullets: pros
- Users see something working early
- You have an integration platform, instead of big-bang integration
PP_Tip 15: Tracer bullets: cons
- Tracer bullets don't always hit their target
- Still imagine the result using a waterfall approach
PP_Tip 16: Prototype to learn
- The value of prototype lies not in the code produced, but in the lessons learned
PP_Tip 16: Prototype to learn: cons
- Set the right expectations
- Make sure everyone understands that you are writing disposable code
- Otherwise management might insist on deploying the prototype or a cleaned up version of it
PP_Tip 16: Tracer code vs prototyping
- Prototype:
- When experimentation is done, the prototype is thrown away and it is reimplemented using the lessons learned
-
E.g., write prototype in Python and then code the production system in C++
-
Tracer code:
- Build architectural skeleton
- Components have minimal implementation
- Over time stubbed routines are completed
- The framework stays intact
PP_Tip 17: Program close to the problem domain
- Always try to write code using the vocabulary of the application domain
-
So you are free to concentrate on solving domain problems and ignore petty implementation details
-
Use mini-languages
- Other people can implement business logic
- Code can issue domain specific errors
- Create metadata compiled or read-in by the main application
PP_Tip 18: Estimate to avoid surprises
-
All answers are estimates: some are more accurate than others
-
Do we need a high accuracy or a ballpark figure?
- The unit of measurement of the estimate conveys a message about accuracy
- E.g., 130 working days vs 6 months
PP_Tip 18: How to make estimates
- Ask people that have done a similar project before
- Specify what's the scope of an estimate: "under assumptions X and Y, the estimate is Z"
- Break the system in pieces, understand which parameters they take, assign a value to each parameter
- Understand which are the critical parameters and try to estimate them properly
- When estimates are wrong, understand why
PP_Tip 19: Iterate the schedule with the code
-
Management often wants a single estimate for the schedule before the project starts
-
In reality:
- Team
- Productivity
- Environment will determine the schedule
PP_Tip 19: Estimates at the coffee machine
- Estimates given at the coffee machine will come back to haunt you
- When asked for an estimate, answer "I'll get back to you"
The basic tools
PP_Tip 20: Keep knowledge in plain text
-
The base material for a programmer is knowledge
-
Knowledge is made of:
- Requirements
- Specifications
- Documentation
- Design
- Implementation
-
Test
-
The best format to store knowledge is plain text
- Plain text can be manipulated manually and programmatically using virtually any tool (e.g., source code control, editors, stand-alone filters)
- Human-readable self-describing data will outlive
- All other forms of data; and
- The application that created it
PP_Tip 21: Use the power of command shells
- GUIs
- Are great
-
Allow to do only what they were designed for
-
In life one does not know what exactly will be needed
- Shells allow to automate and combine tools in ways that one didn't intended or planned for
PP_Tip 22: Use a single editor well
- Better to know one editor very well than knowing many editors superficially
- Use the same editor for all editing tasks
PP_Tip 23: Always use source code control
- Source control is like an undo key, a time machine
- Use it always: even if you are a single person team, even if you are working on a throw-away prototype
PP_Tip 23: Advantages of source code control
- It allows to answer questions like:
- Who made changes to this line of code?
- What are the difference between this release and the previous one?
- It allows to work on a new release, while fixing bugs in the previous (i.e., branches)
- It can connect to automatic repeatable builds and regressions
PP_Tip 24: Fix the problem, not the blame
- No one writes perfect software so debugging will take up a major portion of your day
- Attack debugging as a puzzle to be solved
- Avoid denial, finger pointing, lame excuses
PP_Tip 25: Don't panic
- Before you start debugging, adopt the right mindset:
- Turn off defenses that protect your ego
- Tune out project pressure
- Get comfortable
- Don't panic while you debug, even if you have your nervous boss or your client breathing on your neck
PP_Tip 25: How to debug
- Don't waste time thinking "this cannot happen"
-
Obviously it is happening
-
Make the bug reproducible with a single command
- So you know you are fixing the right problem
- So you know when it is fixed
-
You can easily add a unit test for that bug
-
What could be causing the symptoms?
- Check for warnings from the compiler
- Always set all the compiler warnings so the compiler can find issues for you
- Step through the code with a debugger
- Add tracing statements
- Corrupt variables?
- Check their neighborhood variables, use
valgrind
PP_Tip 25: Rubber ducking
- = explain the issue step-by-step to someone else, even to a yellow rubber duck
PP_Tip 25: Why rubber ducking works?
- The simple act of explaining the issue often uncovers the problem
- You state things that you may take for granted
- Verbalizing your assumptions lets you gain new insights into the problem
PP_Tip 26: select
is not broken
- Do not assume that the library is broken
- Assume that you are calling the library in the wrong way
PP_Tip 27: Don't assume it: prove it
- Don't assume that a piece of code works in any condition
- Avoid statements like "that piece of code has been used for years: it cannot be wrong!"
- Prove that the code works:
- In this context
- With this data
-
With these boundary conditions
-
After you find a surprising error, are there any other places in the code that may be susceptible to the same bug?
- Make sure that whatever happened, never happens again
PP_Tip 28: Learn a text manipulation language
- Spending 30 mins trying out a crazy idea is better than spending 5 hours
- With scripting languages (e.g., Python, perl) you can quickly prototype ideas instead of using a production language (e.g., C, C++)
PP_Tip 29: Write code that writes code
- Passive code generators
- They are run once and the origin of the code is forgotten
- E.g., parameterized templates, boilerplate
-
They can be 90\% accurate and the rest is done by hand
-
Active code generators
- They convert a single representation of knowledge into all the forms that are needed
- This is not duplication, it is the DRY principle in action
Pragmatic paranoia
PP_Tip 30: You cannot write perfect software
- Accept it and celebrate it
- Unless you accept it, you'll end up wasting time chasing an impossible dream
Know when to stop
- A painter needs to know when to stop adding layers of paint
Don't trust others
- Code defensively
- Anticipate the unexpected
Don't trust yourself
- Code defensively against your own mistakes
Preconditions
- = what must be true in order for the routine to be called
- It is caller's responsibility to pass good data
Postconditions
- = what the routine is guaranteed to do
Class invariants
- = a class ensures that some conditions are always true
- E.g., between invocations to public methods
Contracts
- If all routines' preconditions are met
- => the routine guarantees that all postconditions and invariants will be true when it completes
PP_Tip 31: Design with contracts
- Some languages support design by contract:
- In the compiler (e.g., static assertion, type system)
- In the runtime systems (e.g., assertions)
PP_Tip 32: Crash early
- Better to crash than to thrash (=corrupting the state of the system)
- When something unexpected happens throw a runtime exception
- The exception, if not caught, will percolate up to the top level halting the program
PP_Tip 33: If it cannot happen, use assertions to ensure that it won't
- Assertions check for things that should never happen
- E.g., at the end of a sorting routine the data is not sorted
- Don't use assertions in place of real error handling
Leave assertions enabled
- Assertions should be left on even after the system is tested and shipped
- The assumption that testing will find all the bugs is wrong
- Testing tests a miniscule percentage of possible real-world conditions
PP_Tip 34: Use exceptions for exceptional problems
- Interleaving normal control flow code and error handling code leads to ugly code
- With exceptions one can split the code neatly into two parts
- Exceptions are like
goto
Exceptions are for unexpected events
- Use exceptions only for truly unexpected events
- The code should still run in normal conditions if one removes all the exception handlers
PP_Tip 35: Finish what you start
- Resources (e.g., memory, DB transactions, threads, files, timers) follow a pattern:
- Allocate
- Use
- Deallocate
- Routine or objects that allocate resources are responsible for deallocating them
- To avoid deadlocks always deallocate resources in the opposite order to that in which you allocate them
Bend or break
PP_Tip 36: Minimize coupling between modules
- We want to limit the interaction between modules
- If one modules has to be replaced / is broken, the other modules can carry on
- Traversing relationships between objects can quickly lead to a combinatorial explosion of dependencies
PP_Tip 36: Law of Demeter for functions
- Any method
O.m(A, B, C)
of an objectO
should call only methods belonging to: - Object
O
itself - Objects through parameters passed to the method (e.g.,
A
) -
Objects instantiated within
m
-
A rule of thumb in OOP is to use a single dot, e.g.,
a.m()
and avoid multiple dots, e.g.,a.b.m()
PP_Tip 36: Intuition of the Law of Demeter
- Object
A
can call a method ofB
A
cannot reach throughB
to access an objectC
- Otherwise
A
knows about the internal structure ofB
B
needs to be changed to expose the interface ofC
- Cons: lots of wrapper methods to forward requests to delegates
PP_Tip 36: Law of Demeter as general contractor
-
It's like using a general contractor
-
Pros
- You ask a job to be done, but you don't deal with subcontractors directly
- Cons
- The client needs to go through the general contractor all the times
PP_Tip 37: Configure, don't integrate
-
Every time we change the code to accommodate a change in business logic we risk to break the system or to introduce a new bug
-
Make the system highly configurable
- Always use metadata, i.e., data about data (e.g., choice of algorithm, database, middleware, user-interface style, ...)
- Use
.ini
files
PP_Tip 38: Put abstractions in code, details in metadata
- The goal is to think declaratively: specify what to do, not how to do it
- We want to configure and drive the application via metadata as much as possible
- So we program for the general case and put the specifics outside the compiled code
Mechanisms vs policies
- Mechanisms = primitives, what can be done
- Policies = how to put together primitives
PP_Tip 38: Advantages of splitting mechanisms and policies
- Decouple components in the design, resulting in more flexible and adaptable programs
- Customize the application without recompiling it
- Metadata can be expressed in a form closer to problem domain
- Anybody can change the behavior without understanding the code
PP_Tip 38: Put business logic in metadata
- Business logic and rules are the parts that are most likely to change
- So we want to maintain them in a flexible format, e.g., metadata
- Metadata should be encoded in plain text
PP_Tip 39: Analyze workflow to improve concurrency
- Avoid temporal coupling
- Use activity diagrams to identify activities that could be performed in parallel
PP_Tip 40: Temporal coupling
- Time is often ignored when designing a software architecture
- We tend to think in a linear sequential fashion: "do this, then do that"
-
This leads to temporal coupling, i.e., coupling in time
-
When designing an architecture, we need to think about:
- Concurrency = things happening at the same time
- Ordering =
A
must occur beforeB
PP_Tip 40: UML activity diagram
- Actions are represented by rounded boxes
- Arrows between actions mean "temporal ordering"
- Actions with no incoming arrows can be started at any time
- Synchronization points are represented by a thick bar:
- Once all the actions leading to a barrier are done, one can proceed with the arrows leaving the synchronization point
Service
- = independent, concurrent objects behind well-defined, consistent interfaces
PP_Tip 40: Design using services
- Using services allows to avoid temporal coupling
PP_Tip 41: Hungry consumer model
- There are multiple independent consumers and a centralized work queue
- Each consumer grabs a task from the work queue and processes it
-
Load balancing: if a consumer task gets bogged down or it is slower, the others can pick up the slack
-
PP_Tip 41: Example of 3-tier architecture with hungry consumer model
- Goal
-
Read a request from multiple lines and process the transactions against the database
-
Constraints
- DB operations are slow
- We need to keep accepting service requests even when waiting on DB
-
DB performance suffers with too many concurrent sessions
-
Solution
- 3 tier, multi-processing distributed application
- Tier 1: input tasks monitor the input lines and add requests to application queue
- Application queue
- Tier 2: read application queue, apply business logic, add transaction to DB queue
- DB queue
-
Tier 3: read transactions from DB queue and apply it to DB
-
Each component is an independent entity, running concurrently (on the same machine or on multiple machines)
- Actions are asynchronous: as soon as a request is handled by a process and put on next queue, the process goes back to monitor inputs
PP_Tip 41: Always design for concurrency
- Programming with threads imposes some design constraints
- Concurrency forces you to think things more carefully
Examples of problems with concurrency
- Global or static variables must be protected from concurrent access
- Do you really need a global variable?
- Objects must always be in a valid state when called
- Classes with separate constructor and initialization routines are problematic
- Interfaces should not keep state
- Make services stateless
Event
- = special message that says "something interesting just happened"
PP_Tip 42: Objects communicating through events
- We know that we need to separate a program in modules / classes
-
A module / class has a single, well-defined responsibility
-
At run-time how do objects talk to each other?
- We don't want objects to know too much about each other
- E.g., how does object
A
know which objects to talk to? - By using events one can minimize coupling between objects: objects are interested in events and not in other objects
PP_Tip 42: All events through a single routine approach
- One approach is to send all events to a single routine that dispatches them to the objects
- This is bad!
- A single routing needs to know all interactions among objects
- It's like a huge case statement
- It violates encapsulation, increases coupling
PP_Tip 42: Publish / subscribe model
- Objects should only receive events they want (do no spam objects!)
- Subscribers register themselves with publisher objects for interesting events
- Publisher calls all subscribers when a corresponding event occurs
- Organization can be:
- Peer-to-peer
- Software bus (a centralized object maintains DB of listeners and dispatches messages)
PP_Tip 42: Sequence diagram
- It shows the flow of messages among several objects
- Objects are arranged in columns
- Each message is an arrow from sender's column to receiver's column
- Timing relationship between messages is captured by vertical ordering
PP_Tip 42: Push / pull model for event services
- Push mode
- Event producers inform the event channel that event has occurred
-
Event channel distributes event to all client objects registered for that event
-
Pull mode
- Event consumers poll the event channel periodically
- Event channel polls possible suppliers and report interesting events
PP_Tip 42: Separate views from models
-
Model-View-Controller design pattern
-
Model:
- It stores the data
- It has methods to change its state, to report changes (through events), and to report data
- It is like a DB
- View:
- A way to interpret the Model
- It subscribes to changes in the Model
- It subscribes to events from the Controller to change representation
- Controller:
- It controls the View
-
It provides new data to the Model
-
One can even have a network of MVCs: e.g., a View for one Model can be the Model for another View
-
PP_Tip 42: Examples of MVC
- Almost any GUI
- Tree widget = a clickable, traversable tree
- Spreadsheet with a graph attached to represent data as bar charts, running totals, ...; controllers allow to zoom in / out, enter new data, ...
- System reporting interesting information about a baseball game
- Model is a DB with all possible information
- New data from the Controller: a pitcher is changed, a player strikes out, it starts raining
- A View reports the score
- A View reports info about current batter
- A View looks for new world records
- A View posts information on the web
PP_Tip 43: Use blackboards to coordinate workflow
- A blackboard system lets decouple objects from each other completely
- There is even less coupling that publish / subscribe model
- Consumers and producers exchange data anonymously and asynchronously
PP_Tip 43: Example of blackboard implementation
-
Blackboard is like a DB providing atomic and distributed storage of objects
-
One can store any object, not just data
- Objects can be retrieved by partial matching fields (templates and wildcards) or by subtypes
- Operations can be:
- Read = search and retrieve item from blackboard
- Write = put an item on the blackboard
- Take = read + remove from blackboard
- Notify = set up notification when an object matching a template is written
- Advantage is a single and consistent interface to blackboard, instead of different APIs for every transaction / interaction in the system
PP_Tip 43: Example of blackboard application
-
Program that accepts and process loan applications
-
Constrains
- The laws are very complex
- There is no guarantee on the order in which data arrives
- Data is gathered by different people
- Certain data is dependent on other data
- Arrival of new data may raise request for more data and policies
-
As regulation change, the workflow must be re-organized
-
Solutions
- Encoding all the workflow is a nightmare
- A better solution is a blackboard + rules engine
- Any time new data arrives, a new rule is triggered
- Rules can output more data on the blackboard, triggering more rules and so on
While you are coding
PP_Tip 44: Don't program by coincidence
- Do not program by coincidence (= relying on luck and accidental successes)
- Program deliberately
PP_Tip 44: How to program by coincidence
- You type some code, try it, and it seems to work
- Type more code and still works
- After few weeks the program stops to work
- You don't know why the code is failing, because you didn't know why it worked in the first place
- The code seemed to work, given the (limited) testing did, but it was just a coincidence
PP_Tip 44: Why program by coincidence seems to work?
- One ends up relying on undocumented boundary conditions and when the code is fixed / changed, our code breaks
- One tries until something works, then it does not wonder why it works: "it works now, better leave it alone"
PP_Tip 44: How to program deliberately
- Rely only on reliable things
- Document your assumptions (e.g., design by contract)
- Add assertions to test assumptions
- Don't just test your code, test your assumptions as well (e.g., with small unit tests)
- Don't let existing code (even your own code) dictate future code
PP_Tip 45: Estimate the order of your algorithms
- We always want to estimate the resources (e.g., time, memory) required by algorithms
- E.g., "Would the algorithm scale up from 1k records to 1M records?"
Big-Oh notation
- Big-Oh notation represents the worst-case time taken by an algorithm
- Simple loops: $O(n)$
- Nested loops: $O(n^k)$
- Binary chop (e.g., binary search): $O(\log(n))$
- Divide and conquer (split, recurse, combine): $O(n \log(n))$
- Combinatoric: $O(2^n)$
Estimating Big-Oh
- If you are not sure about Big Oh, vary input record size and plot the resource needed (e.g., time, memory) against the input size
Big-Oh in the real world
- It is possible that a $O(n^2)$ algorithm is faster than a $O(n \log(n))$ for small inputs
- Even if runtime looks linear, the machine might start trashing for lack of memory and thus not scale linearly in the real world
PP_Tip 46: Test your estimate
- It's tricky to get accurate execution times
- Use code profilers to count the number of times different steps of your algorithm get executed and plot against the input size
Be wary of premature optimization
- Make sure an algorithm is really the bottleneck before investing precious time trying to improve it
Refactoring
- = re-writing, re-working, re-architecting code
PP_Tip 47: Refactor early, refactor often
- If you cannot refactor immediately
- Make space in the schedule
- File a bug
- Limit the spread of the virus
How to refactor
- Refactoring needs to be undertaken slowly, deliberately, and carefully
- Don't refactor and add functionality at the same time
- Make sure you have good tests before refactoring
- Take baby steps to avoid prolonged debugging
PP_Tip 47: Software development is more gardening than building
-
Unfortunately the most common metaphor for software development is building construction: blueprints, build, release, maintenance, ...
-
As a program evolves, it is necessary to rethink earlier decisions and rework portions of the code
- Software is more like gardening:
- Start with a plan
- Some plants die
- Move plants to get in the sun / shade
- Pull weeds
- Monitor health of the plants
- Make adjustments
PP_Tip 47: When to refactor?
- It is time to refactor when you notice:
- Duplication
- Non-orthogonal design
- Outdated knowledge
- Bad performance
PP_Tip 47: Management and refactoring
- How do you explain to your boss that "the code works, but it needs to be refactored?"
- Fail to refactor now and the investment to fix the problem later on will be larger
- It's like accumulating debt: at some point it will need to be repaid, with interests!
PP_Tip 48: Design to test
- Chips are designed to be tested
- At the factory
- When they are installed
-
When they are deployed in the field
-
BIST (Built-In Self Test) features
-
TAM (Test Access Mechanism) = test harness to provide stimuli and collect responses
-
We need to build testability into the software from the very beginning
- Test each piece thoroughly (unit testing) before wiring them together (integration testing)
PP_Tip 48: Testing against contract
- Write test cases that ensure that a unit honors its contract
-
This also checks whether the contract means what we think it means
-
We need to check over a wide range of test cases and boundary conditions
- There's no better way to fix errors than by avoiding them in the first place
Test-driven development
- By building the tests before you implement the code, you try out the interface before you commit to it
PP_Tip 48: Where to put unit tests?
- Unit tests should be somehow close to the code they test
- E.g., in a parallel directory
-
If something is not easy to find and use, it won't be used
-
By making the test code readily accessible, one provides:
- Examples of how to use a module
- A means to validate any future changes to code
PP_Tip 48: Provide a test harness
- It's good to have a way to:
- Select tests to run
- Pass arguments on the command line to control the testing
- Analyze the test output
- Automatically find all the unit tests (instead of having a list of tests, so that we can honor the DRY principle)
- Test harness can be built using OOP
- E.g., objects provide setup and cleanup methods, standard form of failure report, ...
- There are standard test harness (e.g.,
unittest
for Python,cppunit
for C++)
PP_Tip 48: Build a test backdoor
- No piece of software is perfect and bugs show up in real world
- Have log files containing trace messages
- Log messages should be in a regular, consistent format
- An interesting technique is to have a built-in web server in the application
- Pointing to a certain port of the machine one can see internal status, log entries, a debug control panel
PP_Tip 49: Test your software or your users will
- All software you write will be tested
- If not by you, then by the eventual users
- It is better to test it thoroughly than being swamped in help desk calls
PP_Tip 50: Don't use wizard code you don't understand
- Applications are getting harder and harder to write
- User interfaces are becoming increasingly sophisticated
- The underlying applications are getting more complex
-
One can use wizards to generate code to perform most functions, but one must understand all of it (otherwise one is programming by coincidence)
-
One could argue that we routinely rely on things we don't understand
- E.g., quantum mechanics in integrated circuits
- What we don't understand is behind a tidy interface
- Wizard code is interwoven with our application
Before the project
Requirement
- = a statement about something that needs to be accomplished
PP_Tip 51: Don't gather requirements: dig for them
- Gathering requirements implies that the requirements are already there
- In reality they are buried deep beneath layers of
- Assumptions
- Misconceptions
- Politics
PP_Tip 51: Example of tricky requirements
- A requirements can sound like: "Only an employee's supervisors and HR may view an employee's records"
- This requirement has a policy embedded in it
- We don't want to hard-wire policies into requirements, since policies change all the time
- Requirements should be a general statement: "An employee record may be viewed only by a nominated group of people"
- Give the policy as an example of what should be supported
- Policies should eventually go in the metadata of the application
PP_Tip 52: Work with a user to think like a user
- We need to discover the underlying reason why users do a thing, rather than just the way they currently do it
- We want to solve a business problem, not just check off requirements
- You can ask the user to sit with him for a week while he does his job
- See how the system will be really used, not how management intended to be used
- Build trust and establish communication with your users
Use cases
- = capture requirements through a particular use of the system
PP_Tip 53: Use cases
- While sitting with the user, you see a few interesting scenarios that describe what the application needs to do
- Write the scenarios in a document that everyone (developers, end users, project sponsor, management) can discuss
PP_Tip 53: Abstractions live longer than details
- Good requirement docs should remain abstract
- They reflect the business needs, not architecture, not design, not user interface, ...
PP_Tip 53: Feature-itis
- Aka feature bloat
- The issue is that by adding "just one more feature", the scope of the project keeps growing
- One should track the number of bugs reported and fixed, the number of additional features requested and who approved them
Project glossary
- = one place that defines all the specific terms and vocabulary used in a project
PP_Tip 54: Use a project glossary
- It's hard to succeed if users and developers
- Refer to the same thing with different names, or
- Refer to different things with the same name
- Create and maintain a project glossary
PP_Tip 55: Don't think outside the box: find the box
- The secret to solve a puzzle is to identify the real (not imagined) constraints and find a solution therein
- Some constraints are preconceived notions
-
E.g., "the Gordian knot"
-
E.g., the Trojan horse:
- How do you get troops into a walled city?
-
Would you have dismissed the idea of getting troops "through the front door"?
-
To solve a problem enumerate all the possible avenues
- Don't dismiss anything, then explain why a certain path cannot be taken. Can you prove it?
PP_Tip 55: Impossible problems
- You are stuck on a problem that seems "impossible"
- You are late on the schedule
- Step back and ask yourself:
- Is there an easier way?
- What is that makes this problem so hard to solve?
- Does it have to be done this way?
- Does it have to be done at all?
PP_Tip 56: Listen to nagging doubts: start when you are ready
- When you experience some reluctance when faced with a task, take notice
- Sometimes your instinct is right on the spot, although you cannot put a finger on it
- How can you tell that is not just procrastination?
-
You can start prototyping and verify if some basic premises were wrong
-
Program specification
- = process of reducing requirements to the point where coding can start
- The goal is to remove major ambiguities
PP_Tip 57: Some things are better done than described
- Program specification is an agreement with the user
- It's important to stop increasing level of detail and start coding, prototyping, tracer bullet, because:
- It is naive to assume that a specification will ever capture every detail
- Once the system is running, users will ask for changes
- Natural language is not expressive enough to clarify everything
PP_Tip 58: Don't be a slave to formal methods
- Many methods have been developed to make programming more like engineering (e.g., waterfall development, UML, ...)
- These formal methods use a combination of diagrams and supporting words
- The user typically does not understand them and cannot provide feedback
- It's better to give the users a prototype and let them play with it
PP_Tip 59: Expensive tools do not produce better designs
- Never give in into a methodology just because it is the hot new fad
- Do not think about how much a tool costs when you look at its output
Pragmatic projects
PP_Tip 60: Pragmatic teams
-
Most of the pragmatic programming principles apply to teams, as much as they apply to an individual
-
No broken windows
- Quality is a team issue
-
Team as a whole should not tolerate broken windows (= small imperfections that no one fixes)
-
Boiled frogs
- People assume that someone is handling an issue or that someone must have OK-ed a change request from the user
-
Fight this: everyone must actively monitor the environment for changes
-
Communicate
-
Great teams speak with one voice and are always prepared
-
Don't repeat yourself (DRY)
-
Some teams appoint a member as the project librarian, responsible for checking on duplication, coordinating documentation, ...
-
Orthogonality
- Traditional team organization is based on the old-fashioned waterfall
method of software construction
- Individuals are assigned roles based on their job function, e.g.,
- Business analysts
- Architects
- Designers
- Programmers
- Testers
- Different activities of a project (analysis, design, coding, testing) can't happen in isolation
PP_Tip 60: Organize around functionality, not job functions
- Organize people in the same way one organizes code
- Design by contract
- Decoupling
-
Orthogonality
-
Split teams by functionality not by job function (e.g., architect, programmer, tester)
-
We look for cohesive, self-contained teams of people
-
Let each team organize themselves internally
- Each team has agreed upon responsibilities and commitments to the other teams
- Of course this approach works only with responsible developers and strong project management
PP_Tip 61: Don't use manual procedures
- We want to ensure consistency and repeatability in the project
- Manual procedures leave consistency up to chance
-
Let the computer do the mundane jobs: it will do a better job than we do
-
Compiling the project should be reliable and repeatable
- We want to check out, build, test, ship with a single command
make
andcronjobs
are the solutions
PP_Tip 62: Test early
- Look for your bugs now, so you don't have to endure the shame of others finding your bugs later
- Start testing as soon as you have code
- Code a little, test a little
PP_Tip 62: Test often
- The earlier a bug is found, the cheaper it is to remedy
PP_Tip 62: Test automatically
- Tests that run with every build are better than test plans that sit on a shelf
- A good project may well have more test code than production code
PP Top 63: Coding ain't done 'til all the tests run
- Just because you finished coding, you cannot tell that the code is done
- You cannot claim that the code is done until it passes all the available tests
- Code is never really done
PP_Tip 63: What to test
- There are multiple types of tests:
- Unit
- Integration
- Validation
- Performance
- Usability tests
Unit test
- = exercise a module
- If the parts don't work by themselves, they won't work together
Integration test
- = show that the major subsystems work well together
- Integration is the largest source of bugs in the system
- Test that the entire system honors its contract
Validation and verification test
- = the users told you what they wanted, but is it what they really need?
- A bug-free system that answers the wrong question is not useful
Resource exhaustion, errors, and recovery test
- Resources are limited in the real world, e.g.:
- Memory
- Disk space / bandwidth
- CPU
- Network bandwidth
- Video resolution
- How will the system behave under real-world conditions?
Performance test
- = testing under a given load (e.g., expected number of users, connections, transactions per second)
- Does the system scale?
Usability test
- = performed with real users, under real environmental conditions
PP_Tip 63: How to test
- Run regression tests for all types of tests
Regression testing
- = compare output of current test with previous known values
- It ensures that fixes for today's bugs don't break things that were working yesterday
- All types of tests can be run as regression tests
- Unit
- Integration
- Validation
- Performance
- Usability tests
Test data
- Test data is either real-world data or synthetic data
- One needs to use both, since they expose different kind of bugs
- Synthetic data
- Stresses boundary conditions
- Can have certain statistical properties (e.g., data to sort is already sorted or inversely sorted)
Exercising GUI systems
- Often specialized testing tools are required, e.g.,
- Event capture / playback model
- A data processing application with GUI front end should be decoupled so that one can test each component by itself
Testing the tests
- We cannot write perfect software
- We need to test the tests and the test infrastructure
Testing thoroughly
- Use coverage analysis tools to keep track of which lines of the code have been / not been executed
PP_Tip 64: Use saboteurs to test your testing
- If the system is a security system, test the system by trying to break in
- After you have written a test to detect a particular bug, cause the bug deliberately and make sure the tests catch it
- Write test for both positive and negative cases
PP_Tip 65: Test state coverage, not code coverage
- Knowing that you executed all code does not tell you if you tested all states
- This is a combinatorial problem
- You can tame the complexity thinking about:
- Boundary conditions
- Structure of the code
PP_Tip 65: When to test
- As soon as any code exists, it must be tested
- Testing should be done automatically as often as we can (e.g., before code is committed to the repository)
- Also results should be easy to interpret: ideally the outcome is binary
ok
/not_ok
- Expensive / special tests can be run less frequently, but on a regular basis
PP_Tip 66: Find bugs once
- Once a human tester finds a bug, a new test should be created to check for that bug every time
- You don't want to keep chasing the same bugs that the automated tests could find for you
PP_Tip 67: Treat English as just another programming language
- Embrace documentation as an integral part of software development
- Keep the documentation in the code itself as much as possible
- Treat code and documentation as two views of the same model
- Apply all the principles learned for coding (DRY principle, orthogonality, ...) to English as well
PP_Tip 68: Internal documentation
- Source code comments
- Design documents
- Test documents
External documentation
- = anything that is shipped or published to the outside world together with the software product (e.g., user manuals)
Documentation vs code
- Documentation and code are different views of the same underlying model
- If there is a discrepancy, the code is what matters
PP_Tip 68: Comments
- Code should have comments, but too many comments are as bad as too few
- Comments should discuss \textit{why} something is done (e.g, engineering trade-offs, why decisions were made)
- The code already shows \textit{how} it is done
- Modules, class, methods should have comments describing what is not obvious
- Javadoc notation is useful (
\@param
,\@return
, ...) to extract information from the code automatically
Naming concepts
- Variable names should be meaningful
- Remember that you will be writing the code once, but reading it hundreds of time: avoid write-only code
- Misleading names are worse than meaningless names
PP_Tip 68: Automatically generated documentation
- Also for documentation we want to use pragmatic principles
- DRY principle
- Orthogonality
- Model-view-controller
-
Automation
-
Printed material is out of date as soon as it is printed
- Publish documents on-line
- There should be a single command to generate and publish the documents on-line
- Use a timestamp or review number for each page
PP_Tip 69: Gently exceed your users' expectations
- Success of a project is measured by how well it meets the expectations of its users
Examples of difference between actual and expected results
- A company announces record profits, and its share price drops 20\%
- It didn't meet analysts' expectations
- A child opens an expensive present and bursts into tears
- It wasn't the cheap doll that she wanted
- A team works miracles to implement a complex application
- The users don't like it because it does not have an help system
PP_Tip 69: Communicating expectations
- Users come to you with some vision of what they want
- It may be
- Incomplete
- Inconsistent
- Impossible
- They are invested in it: you cannot ignore this
Manage expectations
- Work with your users so that they understand what you are delivering
- Never lose sight of the business problems your application is intended to solve
Go the extra mile
- Surprise and delight your users
- E.g., balloon help, colorization, automatic installation, splash screen customized for their organization, ...
PP_Tip 70: Sign your work
- Craftsmen of earlier ages were proud to sign their work
- Your signature should come to be recognized as an indicator of quality
PP_Tip 70: Code ownership vs anonymity
- Code ownership can cause cooperation problems: people become territorial
- Anonymity can enable sloppiness, laziness