I often oscillate between idealistic development practices and more pragmatic, get-it-done approaches. But in the day-to-day grind, I tend balance my idealism with pragmatism, while slightly leaning more towards the former, for a number of reasons:
- personal sanity
- enjoyment of the craft
- simplicity
- maintainability
To that end, I often find a few traits are necessary for code to work really well and be pleasant to work with/extend/understand.
A lot of these concepts have mathematical backgrounds, but I am intentionally ignoring the minutae in favor of practical expressions of these formalisms.
- small, modular ‘units’
- stateless or state-light
- simple, common structure
- type aware, or type cognizant
Examples of the above in various contexts
Small modular units
Simple reusable functions, preferably organized as modules (not classes)
Simple reusable templates, preferably composed of partials (I am particularly fond of jinja2 macros in this instance)
Stateless or state-light
Functions that rely sparsely on state; instead, they use composition of multiple functions to achieve the same result.
State is maintained within the “smallest bounds” possible. A sort of “bounded context” that tries to encapsulate state in the smallest function required, so it does not float around, polluting namespaces, or causing collisions at different scope levels.
Simple, common structure
Structure in this context is more about the “shape” of the data. What I mean here is that code should try to follow some basic rules of thumb:
Rely on simple data structures (for configuration, for arguments)
- Have a reasonable arity (number of function arguments) to allow for composition, higher order function generation (currying, partial application, functions that return functions that are customized)
- Uses data structures in place of objects
Note that code and data are often used as separate entities, but in reality there is a large crossover between them. For example, there are a few scenarios where data makes more sense than using “code”, where code is meant to mean to be some executed function, class, or conditional logic:
def get_foo(key, *args, **kwargs):
mydata = dict(
key1=somefunc1,
key2=somefunc2,
)
try:
return mydata[key](*args, **kwargs)
except KeyError:*
return None
is preferable to:
def get_foo(key, *args, **kwargs):
if key == 'key1':
return somefunc1(*args, **kwargs)
elif key == 'key2':
return somefunc1(*args, **kwargs)
return None
Reading data structures is much easier than navigating conditional logic, and is also more maintainable.
Type aware, or type cognizant
Another important principle here is using the simplest types possible. This is a common UNIX adage, and is part-and-parcel to its philosophy. The designers of the pipe model that is the crux of command line programming through bash and other shell interpreters even ensured that data returned in the simplest format (strings, delimited or otherwise) so that they could easily be piped and moved throughout different functions simply.
In the scope of modern programming, this may be slightly different, but the rules are essentially unchanged – simple types for functions, classes, etc… to ensure interoperability.
Consider the following
def hello(obj):
return 'Hello {}'.format(obj.name)
versus:
def hello(name):
return 'Hello {}'.format(name)
The latter reduces the argument in terms of complexity: an object property becomes a string.
It is not hard to argue that a string argument is significantly more interoperable than an object property.
Conclusion
I hope some of these basics seem reasonable, and if so, you’ll consider adopting them in your practices. I can’t take credit for any of these rules; they’ve been there for decades. I can espouse the virtues of them though!