3 ms·
Sometimes, the complexity of the world you're modelling requires there to be many, many `build_url`s and `pluck_definition`s piping into and out of each other.
by gen220 6y ago
Sometimes, the complexity of the world you're modelling requires there to be many, many `build_url`s and `pluck_definition`s piping into and out of each other.
I've found these organizing principles to be quite useful.
1. identify the key that your application is processing (in this case, it's `word`)
2. make it so that as many functions as possible take this "key" as their only argument.
3. whenever you are fetching data about a model, it is either directly or transitively in terms of this key.
4. push model-fetching (technically the I part of I/O) as far to the leaves of your program-tree as possible, hiding them behind "model client" classes, that are passed in to your job at construction.
5. Perform all upserts idempotently, atomically, and in the bottom-right corner of the execution tree. This makes it easy to reason about mutations, and also easy to omit when you're wanting to do "dry runs" or read-only local runs.
6. (4) means you will occasionally fetch the exact the same model more than once in the scope of one execution. This is OK. You can optimize this later with execution-tree-scoped caching.
With this approach, the user-directed I/O (imperative shell) is at the root of your tree, and very narrowly-defined to be the key of computation. You can build a run-loop on top of this, or a CLI tool, or an rpc service. It's narrowness is kind to all kinds of interfaces.
The functional core is everything that isn't the leaves of the execution tree. These functions essentially compose model client calls, and only take the keys as inputs.
The leaves are a collection of reusable one-to-five-liner RPC requests / data base queries. If you like, you can wrap these in a class that caches the queries, as described in (5).
The tests for leaves mock nothing if they target a DB. They mock the rpc service if they target an RPC service.
The tests for the functional core functions mock the model-clients (leave functions).
With this structure, as long as your tests are brittle enough to break when an RPC contract or data model changes, you do not need integration tests.
I do this in Python, but it would work just as well in any language with structs/interfaces. I've observed the same benefits proclaimed in the article, although my approach is a bit different.
If people are interested, I could write more on this, with examples. Let me know!