Recently I started to write my code in a slightly different way. Nothing that I'm going to write here is news, however it makes the code more flexible and testable. Lets start with a simple server example.
class Server(object): def __init__(self): self._address = "0.0.0.0:1234" ... # more methods follow
This code is just bad. The server address is hardcoded, which makes it difficult to reuse. I'm not even mentioning it running in automated tests. We can do better and rewrite it like this
class Server(object): def __init__(self, address): self._address = address
Now it's much better, we can instantiate our server class with address we want. In our application code wed do something similar to this:
def main(): s = Server(args.address) s.start()
This is really amazing, but what if we want to use server somewhere where command line arguments are not available, say unit tests? We can provide a context information to our server.
Contextual information is what I started putting more and more into my code. It does not get passed as a bunch of random variables but as an context class instance.
class ServerContext(object): def __init__(self): # Put defaults here self.address = "0.0.0.0:1234" ctx = ServerContext() s = Server(ctx)
Seems like a small gain, however it allows for greater application extensibility. Let's say we start with a simple default context above. Now we decide to extend our application to support config files, environment variables and command line args.
def main(): # Default context with default values ctx = ServerContext() read_server_config(config_file, ctx) read_env_config(ctx) read_cli_args(ctx) s = Server(ctx) s.Start()
Each of the read functions above are called in the priority order. Lower priority calls are followed by a higher priority one. For example, config file can be overwritten by env vars, and env vars can be overwritten by command line args. We can also use similar approach in our unit tests to spin up test servers.
Like I said, this is no news and context passing has been around for a while; however, adoption of such method is somewhat uncommon at least in my experience. I was mostly motivated to start passing around context objects in order to make dokerized applications more flexible. Sometimes you need to pass variables via env vars, sometimes you want to load some global precomputed configuration by mounting data onto a container, and sometimes you need to do both.