问题
I´m trying to build a type in F#, where when I get an object of that type I can be sure it´s in a valid state.
The type is called JobId
and it just holds a Guid
.
The business rule is: It must be a Guid - but no empty Guid.
I´ve already implemented the type in C# but now I would like to port it to a F# class library.
That´s the C# type:
public sealed class JobId
{
public string Value { get; }
private JobId(string value)
=> Value = value;
public static JobId Create()
=> new JobId(Guid.NewGuid().ToString("N"));
public static Option<JobId> Create(Guid id)
=> id == Guid.Empty
? None
: Some(new JobId(id.ToString("N"));
public static Option<JobId> Create(string id)
{
try
{
var guid = new Guid(id);
return Create(guid);
}
catch (FormatException)
{
return None;
}
}
}
So how do I build that in F#? Thanks!
Update 1:
I tried to implement it as discriminated union type like this:
type JobId =
| JobId of string
But the problem is, that I can´t define any business rules with that approach.
So the final question is: How to ensure that the string
in JobId
ist in a
certain format?
回答1:
Discriminated unions and F# records keep the internal representation public, so this only works in cases where all values of the internal representation are valid. If you need to define a primitive type that does some checks, then you need a type that hides its internals. In this particular case, I would just use a pretty much direct F# equivalent of your C# code:
type JobId private (id:string) =
member x.Value = id
static member Create() =
JobId(Guid.NewGuid().ToString("N"))
static member Create(id:Guid) =
if id = Guid.Empty then None
else Some(new JobId(id.ToString("N")))
static member Create(id:string) =
try JobId.Create(Guid(id))
with :? FormatException -> None
Note that there are two cases that you want to protect against - one is string
value that's not actually a Guid
and the other is an empty Guid
. You can use the type system to protect against the first case - just create a DU where the value is Guid
rather than string
!
type JobId =
| JobId of Guid
Alas, there is no way of ensuring that this guid is not empty. However, a nicer solution than the above might be to define NonEmptyGuid
(using a class like above) that represents only non-empty guids. Then your domain model could be:
type JobId =
| JobId of NonEmptyGuid
This would be especially nice if you were using NonEmptyGuid
elsewhere in your project.
回答2:
I've adapted Tomas' answer to use a DU instead of a class to preserve proper equality and comparison, allowing JobId
to work as expected as a grouping key, for example.
[<AutoOpen>]
module JobId =
open System
type JobId = private JobId of string with
static member Create() = JobId(Guid.NewGuid().ToString("N"))
static member Create(id:Guid) =
if id = Guid.Empty then None
else Some(JobId(id.ToString("N")))
static member Create(id:string) =
try JobId.Create(Guid(id))
with :? FormatException -> None
You have to put the type inside a module and then you can't access the DU constructor directly outside of that module:
JobId.Create (System.Guid.NewGuid()) // Some (JobId "1715d4ae776d441da357f0efb330be43")
JobId.Create System.Guid.Empty // None
JobId System.Guid.Empty // Compile error
来源:https://stackoverflow.com/questions/58974451/how-to-build-f-type-fulfilling-business-rules