small medium large xlarge

Generic-user-small
10 Sep 2017, 17:07
carlo launer (4 posts)

On the topic of enforcing constraints for domain primitives (Constrained Primitive Values/Integrity of Primitive Values), you said to make the constructor private and add a create function.

So I looked at my domain and I have the following types of restricted strings:

type String2 = private String2 of string  //2 chars max
type ShortCode = String2 
type String10 = private String15 of string
type LongCode = String15 
type String20 = private String20 of string 
type CategoryCode = String20 
type String40 = private String40 of string 
type CategoryName = String40
type String45 = private String45 of string type Title = String45 
type String255 = private String255 of string
type BriefDescription = String255 
type String1000 = private String1000 of string 
type LongDescription = String1000

note we talk in terms of codes; descriptions etc but I added the StringX alias for clarity.
Then, I thought I would be lazy/smart and make a reusable create function for this family of types. i.e, using generics:

 let createRestrictedString<'T> friendlyName maxLength str = 
        if String.length str > maxLength then
            Error (friendlyName+" must not be longer than "+maxLength.ToString()+" characters")
        else
            Ok( typeof<'T>.GetConstructor(System.Reflection.BindingFlags.Instance ||| System.Reflection.BindingFlags.NonPublic, null, [|typeof<string>|],null).Invoke [| str |]  :?> 'T)

and now we get simple useage:

    let createShortCode =  createRestrictedString<ShortCode> "ShortCode" 2 

and testing

let x = createShortCode "ab"
let y = createShortCode "cde"

we get

[<Struct>]
val x : Result<ShortCode,string> = Ok String2 "ab"
[<Struct>]
val y : Result<ShortCode,string> =
  Error "ShortCode must not be longer than 2 characters"

However this solution probably incurs some runtime cost due to reflection - is there a more elegant or performant solution using quotations perhaps?

Thank you, f# newbie here!

Generic-user-small
10 Sep 2017, 18:08
carlo launer (4 posts)

Also, are these createX methods intended to be used by the DB layer as well? I imagine that would be expensive to run the validations every time data is loaded from the database. In the earlier chapters you suggested all data outside the bounded context is suspect, but do you really, really mean that, should we not trust the persistence layer?

Generic-user-small
10 Sep 2017, 19:07
Scott Wlaschin (18 posts)

Good questions!

Re: having a more generic function for creating String20, String40, etc., yes that is totally possible, and that’s what I would do too. :) Rather than using reflection though, I would just pass in the constructor function that you need. For example, here are some generic helper functions for constrained types:

module ConstrainedType =

    /// Create a constrained string using the constructor provided
    /// Return Error if input is null. empty, or length > maxLen
    let createString name ctor maxLen str = 
        if String.IsNullOrEmpty(str) then
            let msg = sprintf "%s must not be null or empty" name 
            Error msg
        elif str.Length > maxLen then
            let msg = sprintf "%s must not be more than %i chars" name maxLen 
            Error msg 
        else
            Ok (ctor str)

    /// Create a constrained integer using the constructor provided
    /// Return Error if input is less than minVal or more than maxVal
    let createInt name ctor minVal maxVal i = 
        if i < minVal then
            let msg = sprintf "%s: Must not be less than %i" name minVal
            Error msg
        elif i > maxVal then
            let msg = sprintf "%s: Must not be greater than %i" name maxVal
            Error msg
        else
            Ok (ctor i)

    /// Create a constrained decimal using the constructor provided
    /// Return Error if input is less than minVal or more than maxVal
    let createDecimal name ctor minVal maxVal i = 
        if i < minVal then
            let msg = sprintf "%s: Must not be less than %M" name minVal
            Error msg
        elif i > maxVal then
            let msg = sprintf "%s: Must not be greater than %M" name maxVal
            Error msg
        else
            Ok (ctor i)

    /// Create a constrained string using the constructor provided
    /// Return Error if input is null. empty, or does not match the regex pattern
    let createLike name ctor pattern str = 
        if String.IsNullOrEmpty(str) then
            let msg = sprintf "%s: Must not be null or empty" name 
            Error msg
        elif System.Text.RegularExpressions.Regex.IsMatch(str,pattern) then
            Ok (ctor str)
        else
            let msg = sprintf "%s: '%s' must match the pattern '%s'" name str pattern
            Error msg 

And here is how they might be used

module String50 =

    /// Return the value inside a String50
    let value (String50 str) = str

    /// Create an String50 from a string
    /// Return Error if input is null. empty, or length > 50
    let create str = 
        ConstrainedType.createString "String50" String50 50 str

module WidgetCode =

    /// Return the string value inside a WidgetCode 
    let value (WidgetCode code) = code

    /// Create an WidgetCode from a string
    /// Return Error if input is null. empty, or not matching pattern
    let create code = 
        // The codes for Widgets start with a "W" and then four digits
        let pattern = "W\d{4}"
        ConstrainedType.createLike "WidgetCode" WidgetCode pattern code 

I’ll try to add these examples into the book.

Re: using validation in the DB layer, that depends on how much you trust the DB!

You could treat the DB as any other service (that is, outside the BC), and validate everything, or you could treat it as a private data store (inside the BC) and not validate. I don’t think there is one answer to fit all situations. If you can guarantee that you are the only system writing data to the data store, then you could probably trust it :)

The time taken for (CPU-bound) validation will be a lot less than (I/O bound) database access, so I wouldn’t use performance as a reason for not doing validation. I think the trust issue is the critical one.

Hope this helps,

Thanks!

Generic-user-small
11 Sep 2017, 08:49
carlo launer (4 posts)

Looking at your code there are a few patterns that weren’t immediately apparent to me as an f# newbie:
- although the type constructor for String2 is made private via
String2 = private String2 of string
it is nonetheless available inside a module named the same as the type.
- passing a private constructor to a function makes it available to a function that would not otherwise have access to it
- modules are lightweight enough that a proliferation of modules is not necessarily to be avoided
Thank you for the insights!

Generic-user-small
11 Sep 2017, 08:52
Scott Wlaschin (18 posts)

Good points – I’ll clarify this in the text. Thanks!

Generic-user-small
11 Sep 2017, 10:08
carlo launer (4 posts)

Also, initially I rebelled against your String.IsNullOrEmpty = Error pattern; that’s fine but empty strings are valid values in most of my objects, thought I; so I renamed your createString to createNonEmptyString and added my own createString with Ok “”

But then I realized you were correct:
the distinction with whether the user never entered a value or entered an empty string rarely matters and if needed can be explicitly modelled. Rather, empty strings should be treated the same as null, and explicitly allowed in the domain through an Option type – ie, a valid string primitive cannot be empty.

Generic-user-small
11 Sep 2017, 10:13
Scott Wlaschin (18 posts)

Yes, that’s a good point! As always, it depends on the domain, but generally empty strings mean missing data. :)

You must be logged in to comment