Skip to content

Latest commit

 

History

History
122 lines (99 loc) · 5.79 KB

20190505-conditional-love.md

File metadata and controls

122 lines (99 loc) · 5.79 KB

Conditional love

I've been working on improving the way behaviours can be defined inside a service container using Inversion.Naiad - https://github.com/guy-murphy/inversion-dev/tree/master/Inversion.Naiad - making the configuration intent more obvious and also safer from typos. This is a common problem area when correct functionality is predicated on strings being correct.

The problem with strings...

The PrototypedBehaviour - https://github.com/guy-murphy/inversion-dev/blob/master/Inversion.Process/Behaviour/PrototypedBehaviour.cs - base class is absurdly powerful. With it you can define a mixed bag of conditions and configuration, everything that the behaviour needs to determine whether its Condition should trigger its Action, and then everything else that an Action needs to fulfil its functionality, all in one place.

Driving the behaviour from what is essentially a bag of dictionaries is incredibly flexible, but comes with some risk; notably, that the various stanzas could easily contain a typo which could radically change the meaning of the configuration.

As well as changing behaviour configuration to use statically typed items where possible (for Flags, store names, Settings etc), I decided to make mini-factories for Conditions. I believe the result aids readability and also makes it less likely to make a mistake.

Improving the behaviour definition

So, with some expansion and a little helper class to sort out the Conditions, this turns the following:

new LoadCustomerBehaviour("load-data",
    new Configuration.Builder
    {
        {"config", "param", "customer" },
        {"config", "output-key", "customer" },
        {"context", "type", "customer", "System.Int32" }
    }),

new LoadCustomerByNameBehaviour("load-data",
    new Configuration.Builder
    {
        {"config", "param", "customer" },
        {"config", "output-key", "customer" },
        {"control-state", "excludes", "customer" },
        {"context", "has", "customer"}
    }),
    ...

into this:

new LoadCustomerBehaviour(
    respondsTo: Events.Lifecycle.LoadData,
    config: new Configuration.Builder
    {
        Conditions.ContextTyped(name: "customer", type: typeof(System.Int32)),
        {"config", "param", "customer" },
        {"config", "output-key", "customer" }
    }),

new LoadCustomerByNameBehaviour(
    respondsTo: Events.Lifecycle.LoadData,
    config: new Configuration.Builder
    {
        Conditions.ControlStateExcludes(name: "customer"),
        Conditions.ContextHas(name: "customer"),
        {"config", "param", "customer" },
        {"config", "output-key", "customer" }
    }),
    ...

And that reads a lot better to me, especially when using named parameters explicitly where possible. Guy Murphy once described how much he liked the named parameters in C#, saying that they "look like a little DSL", which is definitely part of the charm for me too, aside from the obvious advantages for disambiguation and reducing mistakes.

Conditions helper

The Conditions helper class itself is simply a generator of Inversion.Process.Configuration.Element instances, which effectively marshal the NamedCases found in Inversion.Process.Behaviour.Prototype - https://github.com/guy-murphy/inversion-dev/blob/master/Inversion.Process/Behaviour/Prototype.cs and Inversion.Extensibility - https://github.com/inversion-org/inversion-extensibility/blob/dotnetcore/Inversion.Extensibility/Extensibility/Prototypes.cs.

A full example can be found at https://gist.github.com/fractos/48bd53501a5a42a8dbe2beec73fea6bd

public static class Conditions
    {
      ...
        public static IConfigurationElement ControlStateExcludes(string name)
        {
            return new Inversion.Process.Configuration.Element(
                ordinal: 0,
                frame: "control-state",
                slot: "excludes",
                name: name,
                value: String.Empty
            );
        }

        public static IConfigurationElement ContextHas(string name)
        {
            return new Inversion.Process.Configuration.Element(
                ordinal: 0,
                frame: "context",
                slot: "has",
                name: name,
                value: String.Empty
            );
        }

        public static IConfigurationElement ContextExcludes(string name)
        {
            return new Inversion.Process.Configuration.Element(
                ordinal: 0,
                frame: "context",
                slot: "excludes",
                name: name,
                value: String.Empty
            );
        }

        public static IConfigurationElement ContextTyped(string name, Type type)
        {
            return new Inversion.Process.Configuration.Element(
                ordinal: 0,
                frame: "context",
                slot: "type",
                name: name,
                value: type.FullName);
        }
        ...

By explicitly generating the conditional stanzas using helper functions, I believe it becomes much more obvious about the intention of the behaviour configuration, and much less prone to simple typos.

The next step for making the configuration water-tight is to start using statically-typed names for Control State and Context parameter names, which I'll experiment with shortly, to see if they are a good fit.