Discipline
For you personally or your small team, just to follow the proper, almost universal “best practices” (no bullshit here), this alone could bring life-changing results for your programming abilities.
Abstraction and modularity
Abstraction is what the Human Mind evolved to do in order to deal with the wast complexity of a hierarchical world. It is a mental technique to pay attention to only what is (seems) relevant in a current situation (“context”).
Remember that the main principle is “modularity”, which is based on abstraction, which, in turn, rely on partitioning and orthogonality.
Orthogonality means that concepts or “things” are unrelated to each other, which most of the time literally means “they have nothing in common”.
This, of course, is not so easy, because things and even concepts have common “environments”.
Another meaning is that “they are clearly separated (partitioned) by an abstraction barrier or reside at different levels of abstraction, just like ”things“ and our concepts about them are.
Levels are actual levels, and partitions are withing a single level. Being at different sizes of a partition means having noting in common.
Everything, including Life Itself, has been evolved out of these what we might call “universal structural patterns”. It is no surprise, that these patterns are everywhere, including our mental constructs.
Knowing for sure that “things” (or concepts) are unrelated is very important.This means they can be considered or developed separately from each other, even by different people or different groups.
Specification (of a “contract” or a “protocol”, which both are a sets of explicit conditions and rules) and the code are at different levels of abstraction (one is “about” the other), so they could (and should) be developed independently.
An abstract interface and the actual representation and corresponding implementation (for a function, a set of interface, such a trait or a type-class, or for a whole module) are at different level of abstraction and are (must be!) clearly separated by a distinct abstraction barrier, which is what an abstract interface is (defines).
This correspond to an evolved physical pattern of a penetrable cell-membrane (with all its “pumps” and “portals”), and it is a universal notion (captures by the mind of an external observer). Always keeping this analogy in mind will make you a way better programmer.
Notice that a single function, a parameterized (a highly-kinded) type, a set of required type-signatures (a trait or a type-class) or a whole module (which may contain a bunch ot types), all form abstraction barriers (or “membranes”).
This is the central notion for all programming (and complexity in general).
The mantra is: Abstraction barriers partition the universe of values just like cells, tissues and organs partition the physical universe (being Life’s building blocks), or sub-process unfold within a single unfolding process.
General principles
There are some facts: Commentaries in the code may be outdated and misleading. Reading a commentary does not mean that the code below is correct.
Documentation is usually outdated and neglected. Everyone hates to write it, because it requires a different set of skills, and, in general, good writing is hard, especially when the precise use of a language is required.
Type-annotations are always current (up to date). They are formal parts of an actual informal specification. They are mechanically checked for you every time you compile the code.
So, the ABC principle – Always Be Compiled. Never leave your code in the state when it does not compile without errors (or even warnings).
Assertions (assert statements) are better than informal specifications in a docstring. They are being validated every time your code runs. This is qualitatively better than just naive assumptions.
More about the actual “best practices” https://cs3110.github.io/textbook/cover.html. Just going through this course will save you a lot of time and effort, because this course is about teaching the principles and programming techniques, not attention seeking.
Simple rules
There are simple rules, which just tells you what to do and why.
Write everything down in org-mode
files.
Your mind will fail in so many subtle ways. Just write everything down.
Org supports Math and LaTex blocks, it is really the right tool.
Write down informal specifications
Sets and logic, just like TLA+
.
To have even an incomplete spec is way better than having no spec.
Start with the stubs of interfaces, and the tests
In the classic languages, such as LISPs, one is using a REPL, which is the way of defining and testing your stubs right away.
This practice is so profound and “natural” that even whole notion of an exploratory programming came out of it – this is when the problem is not even fully understood and properly formulated, so one have to explore the problem space, which basically means a systematic search by trial-and-error and a lot of backtracking and restarts.
A proper testing frameworks requires you to write the test code in a separate files, which is OK. This implicitly forces you to use imports (from modules) and to define the stubs of interfaces first, which is absolutely the right thing to do, justified by more than one non-bullshit theory.
The high-level process is – the Problem-Domain -> extracted Concepts -> Modules -> abstract Interfaces -> Stubs -> Tests before any code or even making any of representation and implementation decisions. This is the essence – all the details must be systematically abstracted away, hidden behind proper abstraction barriers.
DDD and TTD are not bullshit, these are based on how our language-based human minds “work”. Also watch the Gregor Kiczales course on YouTube.
Use all the tools available
This is straight from the horse’s mouth – the John Carmak himself said so.
- advanced language support for your editors (syntax, typing)
- uniform code formatters (everything must look familiar)
- static type checking (even as an external tool, like Erlang does)
- Language Servers (LSP-based modern tooling)
- static analyzers (linters)
- unit and integration testing frameworks
Testing
Every function or an interface has an informally stated “contract” about what is supposed to do.
Specification is a precise formulation of such “contract”. It is (and must be!) completely independent from how it does what it is supposed to do.
The representation and implementation behind an interface must completely hidden behind the abstraction barrier, so it can be changed (for good) without breaking its “contract”.
Once we have the specification (of a “contract”) we can test that everything works correctly (according to the spec).
This is just an airplane self-test before a takeoff. I would not want to board a plane which is not undergoing a complete self-test before a takeoff. I guess you too.
Automated testing is the same thing.
The Principles of testing
There are two fundamental principles of proper testing.
- all “corner case” of the specification must be tested (black-box)
- every path through the code must be tested (glass-box)
Automate everything (with tools and scripts)
- use
GNU autotools
ormeson
- use
make
(of course) - use
pre-commit
with linters and code formatters - use automatic builds (
git clone && ./configure && make test
) - use
CMake
andninja
for C++