This post explores creating a domain model in F#. It continues the series functional domain project which targets creating a basic asset management API.
Domain Requirements
These are the requirements (I’ve made up) for our asset management system.
- An asset is a thing that the organization owns that has some capital value
- An asset may contain another asset (e.g. a park contains a playground that contains a slide)
- An asset is based on a template to ensure similar assets store the same information
- A template contains custom fields which have a type (text, numeric, date)
- An asset stores the values for the fields in its template
- An asset may have a maintenance program. This specifies what work is carried out at certain intervals. For these requirements, an asset may only have a single maintenance program.
- Any maintenance carried out must be recorded.
First Model
The following is a first pass at a model expressing these requirements.
module DomainTypes /// MaintenanceProgram must be immutable in data store so that maintenance records referencing it have accurate information. type MaintenanceProgram = { Id: System.Guid Summary: string Period: System.TimeSpan Details: string } type MaintenanceRecord = { Id: System.Guid // One of MaintenanceProgramId and Summary must be set MaintenanceProgramId: System.Guid option Summary: string option DateComplete: System.DateTime Details: string Cost: decimal } type FieldValue = | StringField of string | DateField of System.DateTime | NumericField of float type FieldDefinition = { Id: System.Guid Name: string Field: FieldValue } type Field = { Definition: FieldDefinition Value: FieldValue } type Template = { Id: System.Guid Name: string Fields: FieldDefinition list // I considered making MaintenanceProgram compositional, but that // might be limiting for organizations that want a simple set // of scheduled maintenance periods without specifics on the type of maintenance MaintenanceProgramId: System.Guid } type Asset = { Id: System.Guid Name: string Commissioned: System.DateTime Cost: decimal Fields: Field list TemplateId: System.Guid Subassets: Asset list }
F# records and discriminated unions provide a very natural way to express a domain model. In this model the majority of information is stored as records, with discriminated unions used to indicate and control the type of value stored in a field.
This model also makes the difference between composition and reference relationships clear. Composition implies the target instance will die with the source instance, whereas in a reference relationship the lifetime of the target is independent of the source. The Asset type illustrates this: the field values make no sense if there is no asset so they are compositional, and therefore we include the Field type within the Fields property of the Asset; whereas the template will be used by many assets so it is referenced by id.
Interestingly (at least to me), one could argue a sub-asset, i.e. a component of a system, could be reused in another asset when its current parent asset is decommissioned. This model is forbidding this by including subassets in the Asset rather than referencing them.
Refining Intent
At this point I wanted to enforce my constraint on the MaintenanceRecord, and via google, ran into the excellent series on F# for fun and profit, Designing with types
Firstly, several fields represent the same concept, but there is no way of identifying that. If the field type is replaced by a single case union type then that relationship becomes clearer. For instance:
type MaintenanceSummary = MaintenanceSummary of string type MaintenanceProgram = { Summary: MaintenanceSummary ... } type MaintenanceRecord = { Summary = MaintenanceSummary option ... }
Next, as noted in comments, we want to ensure one of MaintenanceProgramId and Summary is set in MaintenanceRecord. We can use a discriminated union to enforce this by replacing
type MaintenanceRecord = { MaintenanceProgramId: System.Guid option Summary: MaintenanceSummary option ... }
with
type MaintenanceRecordSummary = | MaintenanceProgramId of System.Guid | Summary of MaintenanceSummary type MaintenanceRecord = { Summary: MaintenanceRecordSummary .... }
This is as far as I’m going to take it for now as my overall knowledge of F# is still pretty limited, but I expect that as I implement the surrounding layers I’ll better understand the purpose of more of the advice within designing with types likely implement them.