Empathetic Code Design
-
Today's talk is about empathetic code design.
-
The project I will use to demonstrate this is Peace, a framework to create empathetic and forgiving software automation.
-
Peace provides constraints over logic and data types, and common functionality.
-
By using the framework, logic and data types will be shaped to produce an automation tool that is clear in its communication.
-
A user uses that tool to get their work done, with good user experience.
-
Here's a 3 step process to upload a file to the internet.
-
Download the file, create an S3 bucket, upload the file to that bucket.
-
We can use a shell script to execute this process. (live demo)
-
We can also use a tool, created using the Peace framework, to do the same thing.
-
envman
is a tool I created earlier to do this process. (live demo) -
This is a short demonstration of why one would use this framework.
-
When changing any environment, first we want to understand where we are.
-
Second, we also want to know, where we are headed.
-
Third, we want to know the cost of getting there.
-
Finally, when we understand enough about the change, we can decide to do it.
-
Now, when I say clear communication, it should be quite easy to see that A B C here maps to 1 2 3 there.
-
By presenting information in the right form, at the right level of detail, it's much easier to map between the concept and what is seen.
-
This is how Peace takes care of the user.
-
Today, we are going to look at how Peace takes care of the automation tool developer.
-
How clearly can we communicate between the mental model of a process, and code.
-
When designing a flow, we need to define the items in the flow, and the ordering between those items.
-
The reason these are called "items" instead of "steps", is because in Peace, the emphasis is on the outcome, rather than the actions.
-
Said another way, the purpose of technological processes is the end state, not the work undertaken to get there.
-
That statement does not apply to other kinds of processes, where the work is meaningful.
-
In code, adding items and ordering looks like this.
-
This shows linear ordering; it is possible to have work done in parallel when there is no dependency between items.
-
So here we can adjust the ordering such that A and B are both predecessors of C. A comes before C, and B comes before C.
-
So A and B can both begin at the same time, reducing the overall duration.
-
We'll adjust this in the example, I want you to see that both the file download and the s3 bucket turn blue, and when both are done, then the upload begins. We'll slow down our connection so it is easier to see. (live demo)
-
Try doing that in a shell script.
-
After a developer defines the items in a flow, they also need to define how data passes through these items.
-
If you've written automation using bash scripts or YAML, usually data is just strings.
-
For the non-devs among us, using "strings" everywhere is like saying, "I took stuff and did stuff, and here is stuff".
-
It would be much better to say, "I grab my camera, and took a shot, and here's a picture."
-
To better communicate with developers, Peace uses type safety.
-
Because Peace is a framework, each item's automation logic should be shareable.
-
This means, the input to each item, and its output, are API.
-
In Peace, we call the input "parameters", and we use call the output "state".
-
For a file download item, the parameters are the source URL to download from and the path to write to.
-
and the state, can be either "the file's not there", or "it's there, and here is its content hash".
-
Given each item defines its parameters and state, we need a way for developers to pass values into the parameters.
-
Usually, at the start of a process, we know the values of some parameters, and we can pass them straight in.
-
However, sometimes we need part of the process to be completed, to have the information to pass to a subsequent item.
-
For example, if we had a server instead of an S3 bucket, we cannot know what its IP address will be until we the server is created.
-
But we still want to encode into the flow, that the state from an earlier item, is needed to determine the parameter value for a subsequent item.
-
For the simple case, where we know the parameter values up front, we can just plug them in.
-
But we want to be able to express, "get the state from step two, and pass it into step three".
-
So I came up with this, for every item's parameters, the framework will generate a specification type that records how to figure out what values to use.
-
and you can pass it direct values, or you can tell it "here's what states to read, and here's how to extract the value to use for this field".
-
Here's how it looks like in code.
-
Yes, we can read states from multiple items, and use them to compute a value.
-
Best of all, it's type safe -- so within the code you can ask "what parameters do I need to pass in, and get them right".
-
If you make a mistake, you have a really big safety net called the compiler, that will save you.
-
The error message needs improvement, though I haven't yet figured that out.
-
If the parameter type changes, such as when you upgrade your item version, then you get a compilation error, which means you don't have to work to discover where to change -- the compiler tells you.
-
Now that we have our logic and our data, we need a way to interact with it.
-
Peace is designed to separate the automation logic and data, from how it is interacted with and presented.
-
Whether it is interactive shell, uninteractive shell, web API, or web page, there is no rendering code in any item.
-
This allows automation developers to use different front ends to the same automation backend.
-
In code, the developer can choose which output to use.
-
Currently the only user facing output implementation is the
CliOutput
, which you've seen. -
There are a number of other output implementations used in tests,
-
such as the
NoOpOutput
which discards all information, and theFnTrackerOutput
which stores what output methods were called, with what parameters. -
Developers can choose to write their own output implementation as well, but
-
It is within the scope of Peace provide implementations for common use cases.
-
So far, we've seen Items, Parameters, Specifications, and Output.
-
As a developer, this should be sufficient to run some commands.
-
First we group all of the above into a
CmdCtx
, then we call the command we want to run for the flow. -
StatesCurrentReadCmd
andEnsureCmd
are provided by the framework. -
The real code still contains code to format the output, but this is something that the framework can provide.
-
For any automation software, the user should only need to pass in item parameters once.
-
Commands run using that flow should be able to recall parameters for the user.
-
How can we make this easy for the automation software developer?
-
The easiest thing, is for them to do nothing.
-
So whenever a command context is built, the following happens.
-
Previous parameter specifications will attempt to be loaded from disk.
-
The provided parameter specification will be merged with those.
-
Validation happens, to make sure there is enough information to resolve parameters for all items.
-
The merged parameter specifications will be serialized to disk.
-
If the parameter specifications don't pass validation, we should get a useful error message.
-
When a command is run, Peace stores information about that execution.
-
It stores this in a
.peace
directory within a workspace directory. -
The automation tool developer needs to tell Peace how to determine the workspace directory.
-
Because if the tool is run in a sub directory, information should still be stored in the workspace directory.
-
If you think about git, if you run
git status
in any subdirectory of a repository, it will still find the.git
directory somewhere up the chain. -
One undeniable way to tell if software works, is to run it.
-
And to tell if it will work in production, we usually want to run it in a replica environment first.
-
Out of the box, Peace provides support for logically separate environments, through profiles.
-
So you can have multiple environments under different names, just like git branching.
-
Let's see a demo.
-
How do we communicate clearly in code?
-
We make sure the API matches the mental model.
-
We make sure we have good error messages.
-
When we require something of the developer, make it easy for them to provide it.
-
and when we adhere to these constraints, it will improve the automation development experience.