Monday 23 March 2015

Using the Model-Store pattern in Inversion (part 1)

This post is the first in a series about using a Model-Store pattern in the Inversion behavioural micro-framework for .Net (see: https://github.com/guy-murphy/inversion-dev).

Model-Store Pattern

This is a general pattern than involves representing domain data as immutable models, and allowing the segregation of reading and writing of data in a manner that enables CQRS-like behaviour and optimisation in an application stack.

The key components of the pattern are domain model objects that exhibit an internal builder class that is used to mutate an existing object or create a new one (for this pattern's purposes, these are effectively the same thing), and a generalised store interface that is friendly towards service location, service injection and segregation. The store interface also enforces separation of concerns, enabling the storage methods and caching strategies to be transparent to the application.

Models

I despair when joining a project and discovering that they are three foot deep in AutoMapper entrails and everything has a DTO equivalent for talking to a Web Service somewhere, or some kind of perverse partial class used as a View Model. Why create something that can't be used everywhere? Why can't we all just get along and respect each other as who we are?

The day I started to keep my data objects free of extraneous crap was a good day.

Just a POCO ...

Assuming that we have a User entity in our domain, our POCO for this might look like the following:


Taking control of changes

However, this can be changed by anybody, so in order to put that under control, we can make the properties read-only and only settable from the object's constructor.


This uses the ability to set a public property to only have a private setter, thereby cutting down on some code real-estate and making sure code external to the object instance cannot modify the value of a property.

Immutability

However, the next step would be to enforce this policy for code internal to the object instance as well.


Extra constructor added that takes its values from an existing User object. This is mostly for completeness.

This is now more like a truly immutable data object. The instance is guaranteed by the CLR to have unmodified fields after initialisation.

Mutation

When working with objects, at some point we will need to represent a change in value. This is known as mutation and it entails a little bit of extra internal structure to support it. The advantage of enforcing this really shows when dealing with multi-threaded applications, where a thread can guarantee that an object pointed to by specific reference will not change during the lifetime of that instance. Any change to the object creates a new instance. Using immutable objects in multi-threaded applications can lead to entire swathes of management code and concerns falling away.

Two components are needed to support mutation. First, an internal Builder class, and second, a Mutate method.

The internal Builder class

The Builder class is contained within the domain object class, as below. It contains publicly modifiable versions of the properties belonging to the immutable outer class.

It represents a mutable version of the data. This can be used and accessed by external code to prepare new or changed data. 


The ToModel method puts the Builder object's values into a new instance of a User object which has immutable properties.

The FromModel method puts a User object's values into a Builder object which has mutable properties.

The Mutate method

This method is responsible for facilitating a change to an immutable object by accepting a change that is represented by an anonymous method, and returning a new instance of the object that contains the changed data.


It creates a new instance of this model's Builder class, initialised with the current immutable instance's data. Then, it applies the passed anonymous method, or mutator, to the builder.

The mutator looks something like this:


The result of the mutation is returned as a User object, and normally this would cause a casting exception as Mutate is defined as returning a User object, but the addition of the following two methods allow us to override the implicit casting behaviour of the immutable User class and the mutable Builder class in a granular fashion:


Therefore, when the Mutate method returns a Builder object as a User, the implicit operator for User that takes a Builder as an input instead returns the result of the Builder's ToModel method - i.e. the immutable version of the Builder's data.

Similarly, when some code casts from a User object to a Builder object, then the implicit operator for Builder will be called, returning the result of new Builder(model), where model is the User object being cast.

Default values for fields

To provide fields with default values, they should be applied to the Builder object, as instantiating the immutable object requires either an existing User object or a full set of values for the properties.

One way of doing this is to change the parameterless constructor in the Builder class so that it contains the initialisation of properties with default values.



Another way would be to provide a private backing-field for a property that is being given a default value in the Builder object:


Dictionaries and Lists

There are existing implementations of immutable classes of Dictionary and List that may be used in the immutable outer class, but they require a little bit of initialisation and translation during construction and conversion. 

Suppose we want to add a Metadata collection to our User object which will be represented by a string,string dictionary. To do this, we specify an ImmutableDictionary in the outer class:


And the public, read-only property:


The two constructors of the outer class need some modification. The first constructor needs to be passed a dictionary of metadata to instantiate its immutable data from:


The second constructor is passed a complete User object, so the signature doesn't change but the body will have to take its metadata from there:


In the Builder class, the metadata is represented by just a normal dictionary:


And that will need initialising in the parameterless constructor:


The other constructors and methods adjust to accommodate:


For ImmutableList, the pattern is just the same.

Also, for the record, I hate using var, but the alternative didn't format very well. Weak excuse, I know.

Immutables, Immutables, Immutables, all the way down

Now that we have the User object with its immutability and Builder class, supposing we implemented its Metadata property as another immutable class instead of just an ImmutableDictionary.


Importantly, the Builder's Metadata property is of type UserMetadata.Builder. That type choice plus the implicit cast overrides (assuming there is one in the UserMetadata class as well) allows a chaining of the mutable versions of the objects, which means that within a Mutate method called on the User object, the mutable version of the Metadata is in scope and will be converted back into an immutable version when the result of Mutate is cast back to a User (internally it will chain the calls to ToModel).


Implementing the IData interface

Finally, in order to make this generally useful within the Inversion ecosystem, we make the object implement the Inversion.IData interface:


Implementing this interface allows Inversion to include the object in a ViewStep model which can be processed by the render pipeline. It's not a hard requirement for an object used in an Inversion application to implement IData, but it does enable the object to be represented easily by XML/Json in a way that easily interfaces with the framework.


The casting to IData in the Data method makes it explicitly clear to the compiler that we want the implementation of the IData's Data method within the Metadata object instance.

The full versions of the User and UserMetadata classes can be found here:

https://gist.github.com/fractos/6c7f53dd88aeb4c00b5f

Next part ... Stores

Coming soon.
Update: Fixed the source code for the Mutate function.

No comments:

Post a Comment