This is a collection of pragmatic programming practices I've learned from reading, working, and reading more.
The one golden rule: make your code easy to read.
Implementing the “easy to read” concept lends itself to ideal side effects: easy to change, DRY, pure functions, single responsibility principle, and more.
Pragmatic programming
DRY: Do not repeat yourself
STOP! What if we want to make both drinks iced? We need to call add_ice()
before all the pour_in_cup()
. What if we have 1000 variations of this matcha drink? (Sesame, Ube, yum.) We'd have to include add_ice()
1000 times to all these matcha functions.
Let's practice DRY. Combine the shared steps into_make_matcha_base()
.
This reduces code duplication. Less chance of bugs, and a single source of truth.
Do not mistake this with trying to force DRY with functions that do the same thing, but have different meaning. For example:
Orthogonal
Functions should be orthogonal.
Flying a helicopter is not orthogonal. Changing one control affects multiple outputs—pushing forward the stick changes pitch, altitude, and speed all at the same time.
Do not build helicopters. Every function must be focused, must have unique/independent functionality, and should not affect other systems.
Design by contract
Functions should have clear:
- Preconditions
- Postconditions
- Error cases
Decoupling
Unrelated functionality should not be tied together. Let's say we have a right click menu that shows options, and these options also show up in a spotlight-type search bar.
If we change the right-click menu structure, the search items can break unexpectedly! Let's decouple this.
Transformation programming
We can think of functions as purely transforming data → data.
Functions
Functions should do something or answer something. They also
- Should be small
- Should DO ONE THING WELL
- Should try to have 2 indents at most (a more loose personal rule)
- Take advantage of early returns vs a ton of nested conditionals
Functions—Levels of abstraction
Try to keep one level of abstraction per function.
Let's not mix crypto details with db calls.
Functions—Pure functions
Functions should always return the same output for the same input, and have no side effects (no changes to the outside world). Side effects are LIES!
This function create_matcha_americano()
**is lying! It says it's making matcha, but it's secretly reducing inventory AND cleaning my room. Why is it cleaning my room?
Change this function to a pure function.
Functions—Arguments
The less arguments the better. Try not to have more than 3. We can use objects—do not worry, that is not cheating. Being able to group arguments into an object means the information is related to each other. DO NOT use booleans for arguments.
So many args, let's reduce them.
Comments
COMMENTS DO NOT MAKE UP FOR BAD CODE. Use comments extremely sparingly. They have to be kept in sync with the code and will eventually become outdated.
Naming
Do not use useless words. CustomerData
vs CustomerInfo
? PluginController
vs PluginManager
? Data and Info, Controller and Manager are not great suffixes to add to class names. Customer
is sufficient enough.
Vertical formatting
Keep variables close to usage, and callers above callees. We want to read top to bottom, left to right.
Objects and data structures
Objects hide data behind abstractions and expose functions. Think of objects like a vending machine! 🤖
- You don't care how it stores drinks inside
- You just press buttons to get what you want
- It hides all the messy details behind a nice interface
Data structures expose data and have no meaningful functions. Think of data structures like a box of Legos! 🧱
- All the pieces are right there to see and use
- No hidden machinery, just pure data
- You decide how to use the pieces
Do not mix the two and create hybrids.
Law of Demeter
Only talk to your friends. Don't reach through objects to talk to their objects. It creates brittle chains of dependencies.
Error handling
Think in try-catch-finally blocks. This separates error-handling code from the main logic.
Boundaries
You can wrap 3rd party code (API) in a class for encapsulation!
This hides all the implementation of ThirdPartyWeather()
in our WeatherAPI
class. If the API changes, the user is not exposed to the changes.
Tests
Test a single concept per test.
We're checking the email and the active status. Let's separate these.
Classes
Classes should be small. Names like Processor, Manager, or Super ARE DANGEROUS!
Consider the single responsibility principle: classes should have one responsibility.
For example, a Cashier
class should be in charge of managing money at the register only. It should not be taking orders for the store.
Tackling a problem
You need a plan.
Without a plan, your computer and brain will explode. If you don't know what you're doing, create a plan with a series of minor goals. Even if some goals seem tangential to the main problem, you will make measurable progress toward a solution and feel that your time has been spent usefully.
Understand the problem
Restate the problem. Many times I have yapped to myself in a phone booth, and just from verbalizing the problem, I discovered why something was not working.
- Constraints: what restrictions do I have?
- Operations: what actions can I take?
Techniques
Start with what you know.
- Like a sudoku puzzle, if a square has 8 numbers filled, you can fill the 9th one, and build up a solution from there.
Experimenting with a subproblem.
- Reduce the problem. For instance, given a list of 3D coordinates, we are asked to find the two that are closest to each other. What if we reduced the problem to 2D? What if we only had 3 values in this list?
Finding the most constrained part of the problem
- Let's say we are managing a boba store and need to assign worker shifts. Let's say, without fail, 9:00am on Monday is the time of highest traffic: an army of 50 college students take the boba shop by storm. We can then plan staffing around this.
Experimenting in a controlled manner
- Change one variable at a time and compare outcomes. Practice systematic debugging.
General laws to abide by
Do not program by coincidence
Finding an answer that happens to fit is not the same as the right answer. Don't assume it, prove it.
When things break, if you don't understand exactly how you got somewhere, you will have no idea why things are broken.
Don't be a slave to history.
Just because something worked in the past doesn't mean it works now. Using past code is great, but make sure to understand why it works.
Successive refinement
Create a working first draft. It does not have to be clean as long as the foundation of your solution is correct. Then, clean it up with pragmatic programming practices. We can use test driven development to make sure we're not breaking things, and refactor incrementally.
Further reading
- Cognitive load is what matters by Artem Zakirullin
- The Pragmatic Programmer by Andy Hunt and Dave Thomas
- Clean Code by Robert C. Martin