Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. – Antoine De Saint Exupery
The approach to programming is concerned with finding the best ways to translate programmer’s intention into the good system design and code.
The programming is communication. The programmer continuously add, change and refine ideas in the code. Source code has two important goals: tell a computer what to do and tell people what the computer should do. The program code is the only true medium for storing and communicating ideas about the software system behavior. Quality of the ideas expression in the code directly affects overall quality of the system.
So, what are characteristics of the good code?
- clear – easier to work with ideas;
- minimal – less effort to understand and change ideas;
- testable – easier to validate ideas.
These are 6 top reasons for bad design and code:
- lack of expertise
- unrestrained technical curiosity and creativity
- missing big picture: system purpose and customer goals
- blindly following popular methods and over-using technology
- sloppiness; lack of attention to details
- over-complicating design to have more work or increase job security
The programmer can write better code (and avoid most of these problems) by improving programming style and approach.
- Intention – understand your task and how to get it done
- Approach – basic principles of writing code
- Composition – organization of code
- Expression – expressing ideas in code
- Object Oriented Pragmatic Style
Two most important rules are
- work from the simplest design
- revise and refactor after each task
1. Work from the simplest design
The simplest design that solves business needs is the best design. The simplest design is the product of discipline and mastery. The simplest design requires creative thinking – it is easier to follow traditional ways than to come up the simple, but clear and powerful solution.
We’re saying consider all solutions to your task that could possibly work. Implement the simplest solution. Refactor from there if and when needed… And we’re saying that when you do enhance the simple solution (and you generally will), you will always wind up with a system that is just right for what it does so far. And we’re saying that that is just where you want to be. Everything just right, nothing added that isn’t needed.
For example, if you love design patterns, MVC and object oriented programming, you may end up with design below for finding orders by customer’s postal code.
I love these patterns too, but I would still recommend to start with the simpler design.
The simplest design is the most effective way to deliver business value. You spend less time, build more reliable, easier to understand and change solution. You save energy for other tasks and reduce load on your hard-working brains.
Why do people not go for the simplest solution all the time? There are three main reasons:
- unintentional – the programmer didn’t find simpler solution (lack of experience, pressure and no time to think, relying on traditional ways, compliance with architect’s grand design and others)
- intentional – the programmer wants to over-complicate solution for personal benefits (play with interesting technology, build up resume or increase scope of paid work)
- foretelling – the programmer bets on the future advantage of more complex design
The last reason is the most compelling for many programmers. What if your experience and knowledge tells you that you will need this more advanced design and additional code?
Lets do cost-benefit analysis
What are the chances of mismatch with our initial expectations for a non-trivial project? Big. Mismatch with expectations happens when we learn more from technology, domain and customer feedback while programming. Mismatch happens when we understand the problem deeper and come up with the better solutions. Mismatch happens when the new requirements are coming, priorities are altered and the whole world around is changing (e.g. new company opportunities or credit crunch and economic downturn).
As we can see the best approach is the simplest design that requires little refactoring in the future and doesn’t prohibit anticipated design evolution. Therefore, use you experience and intuition to predict direction of the design evolution. Use this knowledge not for making design more complex to accommodate this prediction, but for ensuring that the simplest solution is not stupid, prohibitive for evolution and will survive coming changes.
However, the level of expertise has significant impact on the choice. The novice programmer should use standard well-described solutions and learn how to simplify them later. The expert programmer should be more comfortable to design simple, sound and easy to evolve solutions from the start.
2. Revise and refactor
Focus on the simplest local design and code for individual features without revisions and design improvements could sometimes create global mess and painful complexity .
For example, it is bad, if after adding several features, your OrderDB contains many methods like LoadForPostalCode, LoadForPostalCodeAndStatus, LoadShippedOrders and web pages are overloaded with domain logic. This kind of code calls for refactoring.
Revise and refactor after completing any programmer tasks.
You should ask yourself 4 questions:
- Can I reduce code?
- Can I make code clearer?
- Can I make code more testable?
- Can I make the whole system simpler?
- remove duplication and increase code reuse
- create abstractions to generalize individual cases
- use design patterns to simplify design
Refactoring is one the most important practice for productive software creation. Refactoring allows to keep the system in the good shape all the time – simple, well designed, ready to change. Implementing the simplest solutions without refactoring and evolutionary approach is risky.
Other rules should enhance your approach further.
3. Discover Form and Function in parallel.
Form follows Function is a traditional principle in design. In other words, customers requirements dictate user interface. However, discovery of complex interactions could affect what the customer wants and change understanding of needs. Sometimes starting with UI paper sketches and discussing them with customers could expose missing details, gaps in understanding and suggest better ways to satisfy customer needs. Therefore, work on discovery of Form and Function of the system in parallel.
4. Avoid using new tools
New Systems Mean New Problems – The Fundamental Theorem of Systemantics
Do not use new languages, tools and libraries unless existing are bad and you can tolerate high risk and delays.
Every new programming tool claim to make you more productive and powerful. Few of them really do this. But , it is guaranteed, that every new tool takes time for learning and brings new problems.
I know that it is hard to continue using old tools for a long time and stay competitive in fast moving software development world. But try to limit impact on customer. It is not fair to make them victims of your learning and experiments. Create separate experimental prototypes and greenfield projects to evaluate new tools and approaches. Prefer to be honest and transparent with your client about risks of new technologies, tools and approaches.
5. Use creative ideas sparingly
Creative ideas can bring excellent solutions. However, any creative idea adds element of unexpectedness and departure from established ways to solve similar problems. Programmers will spend more time to understand creative ideas and will have challenges to support them. Therefore, use creative ideas when they bring significant advantages (especially to simplify solution) and avoid them in trivial situations. Don’t worry, you will always find where to apply your creative energy if you are productive, build good solutions and have more and more customers.
6. Prefer standard solutions to the non-standard
This rule compliments the previous rule. Standard solutions are based on experience of many people. Maybe they are not the best in your particular case, but they often are safe bet if these problems are new for you. You can find many examples, quickly learn how to apply them and understand consequences beforehand. New creative solutions will make life more interesting in expense of productivity and certainty in the final results. Therefore, prefer standard solutions to reinventing the wheel again. Don’t forget to learn how to make them simpler.
7. Separate production code and experiments
Restrain technical curiosity when writing production code. Many programmers find the joy of programming in solving complex problems. It is so tempting to solve difficult technical puzzles and try out new cool techniques without need. However, the pragmatic programmer solves complex customer problems with simple solutions, instead of creating complex solutions for the simple problems.
Think first about customer requirements and simple design. Experiment and look for the new solutions only if you cannot find simple solutions with familiar methods. Keep your production and experimental code separately. Adopt ideas from experiments without unnecessary complication of the solution.
8. Do not overwrite (or over-eingeneer)
Write minimal code relevant to present customer requests. Surprisingly, complex over-engineered solutions are easier to create – just follow recent trends and try to anticipate everything that came to mind. The simplest and clear solutions require extra effort.
Over-engineering harms the software system, takes away precious time and project money. Avoid over-engineering if you are motivated to build good system and satisfy your customer.
9. Write own code for the core and use external components for supporting functionality.
Concentrate your effort on writing code for the core functions specific to customer domain or code that is bringing competitive advantage. Don’t waste too much effort on secondary supportive functions. For example, there are many good enough libraries, for solving standard dynamic web user interface needs – prototype, jQuery, Microsoft AJAX, Yahoo user interface library and others – but many programmers are still building their own solutions. I like to have full control over my software systems and write as much as possible components, but one cannot embrace unembraceable (Kozma Prutkov). You have to trust some parts of the system to other libraries that will save time and do better job (but bring new problems).
10. Do not write code for future.
The dirty trick of predicting the future is that future is unpredictable. Everything else is fine. Written for the future code adds unnecessary complexity. This code is useless for the present problems. Avoid writing code for the future and strive for the simplest solutions for the present needs.
11. Write code for other programmers
Can other programmers understand your code? Do you have clear names, consistent formatting and self-explaining code? Writing code for others will make code better for you.
12. Switch between big picture and details
Good programmers can focus on the big picture and still pay attention to details. Clear intention and forward thinking are as important as reliable implementation.
13. Pair program for complex problems
Pair programming brings significant benefits:
- better ideas – two brains solving the problems
- better quality – two pair of eyes validating code
- better knowledge – two programmers understand implementation and ideas behind
- better productivity – problems are solved faster
- more enjoyment – people like to communicate and work together
I’m still not sure about serious benefits of pair programming for simple and routine programming tasks. But many Agile teams pair program for all production code and like results.
Putting It All Together
Inspiring reference: The Elements of Style, W. Strunk Jr. and E.B. White