Of Workflows, Data and Services
This is yet another in my series of posts musing about what my ideal language would look like. This one is about readability.
Most code I write these days seems to utilize three types of classes: Data, Services and Workflow.
These are generally POCO object hierarchies with fields/accessors but minimal logic for manipulating that data. They should not have any dependencies. If an operation on a data object has a dependency, it's really a service for that data. Data objects don't get mocked/stubbed/faked, since we can just create and populate them.
These are really containers for verbs. The verbs could have been methods on the calling object, but by pulling them into these containers we enable a number of desirable capabilities:
- re-use -- different workflows can use the same logic without creating their own copy
- testing -- by faking the service we get greater control over testing different responses from the service
- dependency abstraction -- there might be a number of other bits of logic that have to be invoked in order to provide the work the verb does but isn't a concern of the workflow
- organization -- related verbs
These end up being classes, primarily because in most OO languages everything's a class, but really workflow classes are organizational constructs used to collected related workflows as procedural execution environments. They can be set up with pre-requisites and promote code re-use via shared private members for common sub-tasks of the workflow. They are also responsible for condition and branching logic to do the actual work.
Actions (requests from users, triggered tasks, etc.) start at some entry point method on a workflow object, such as a REST endpoint, manipulate data via Data objects using services, the results of which trigger paths defined by the workflow.
Same construct, radically different purposes
Let's look how this works out for a fictional content management scenario. I'm using a C#-like pseudo syntax to avoid unecessary noise (ironic, since this post is all about readibility):
UpdatePage is part of
PageWorkflow, i.e a workflow in our workflow class. It is configured with
_authService as our service classes. Finally
page are instances of our data classes. Nice for maintainability and separation of concerns, but awkward from a readibility perspective. It would be much more readable with syntax like this:
Much more like we think of the flow. Of course this could easily be done by creating those methods on
PageWorkflow, but that's the beginning of the end of building a god object, and don't even get me started on putting
Update on the
Page data object.
So let's assume that this separation of purposes is desirable -- i'm sure there'll be plenty of people who will disagree with that premise, but the premise isn't the topic here. What we really want to do here is alias or import the functionality into our execution context. Something like this:
The above construct is closer to the
@EXPORT syntax of the perl
Exporter module. Except instead of exporting functions,
exports exports methods on an instance as methods on the current context. It also extends the export concept in three ways:
Instead of just blindly importing a method from a service class, the
exports syntax allows for aliasing. This is useful because imported method likely defered some of its functionality context to the owning class and could collide with other imported methods, e.g.
As the methodname is rewritten, the argument order may no longer be appropriate, or we may want to change the argument modifiers, such as turn a method into an extension method.
The above syntax captures arguments into
c and then aliases the method into the current class' context and turns it into a method attached to
p, i.e. the
page, so that we can call
But why stop at just changing the argument order and modifiers. We're basically defining expressions that translate the calls from one to the other, so why shouldn't we be able to make every argument an expression itself?
This syntax curries the
Permissions.Write argument so that we can define our aliases entrypoint without the last argument and instead name it to convey the write permissions implcitly.
Writing workflows more like we think
Great, some new syntactic sugar. Why bother? Well, most language constructs are some level of syntactic sugar over the raw capabilities of the machine to let us express our intend more clearly. Generally syntactic sugar ought to meet two tests: make code easier to read and more compact to write.
The whole of the import mechanism could easily be accomplished (except maybe for the extension method rewrite) by creating those methods on
PageWorkflow and calling the appropriate service members from there. The downside to this approach is that the methods are not differentiated from other methods in the body of
PageWorkflow therefore not easily recognizable as aliasing constructs. In addition the setup as wrapper methods is syntactically a lot heavier.
exports mechanism allows for code to be crafted more closely to how we would talk about accomplishing the task without compromising on the design of the individual pieces or tying their naming and syntax to one particular workflow. It is localized to the definition of the service classes and provides a more concise syntax. In this way it aids the readibility as well as theauthoring of a common task.