I have a code sample like this:
type Db =
| Db of Map<string, string>
let get id db =
let (Db dict) = db
dict
|> Map.tryFind id
|> (fun x -> x, db)
let add id value db =
let (Db dict) = db
dict
|> Map.add id value
|> Db
|> (fun x -> (), x)
let remove id db =
let (Db dict) = db
dict
|> Map.remove id
|> Db
|> (fun x -> (), x)
type GetByIdCapability =
| GetByIdCapability of ? (Id -> Value * ?) ?
type AddValueCapability =
| AddValueCapability of ? (Id -> Value -> ?) ?
type RemoveByIdCapability =
| RemoveByIdCapability of ? (Id -> ?) ?
type CreateAddValueCapability =
| CreateAddValueCapability of (Db -> AddValueCapability)
type AddValueService =
{
AddValueCapability: AddValueCapability
}
let createAddValueService db (CreateAddValueCapability createAddValueCapabilityFunc) =
let addValueCapability = db |> createAddValueCapabilityFunc
{
AddValueCapability = addValueCapability
}
let addValue id value (addValueService: AddValueService) =
addValueService.AddValueCapability ?
What I want is to have AddValueService
which must have access only to "add value to Db" capability and has no access to other functions of Db inside service's logic. But at the same time I want to get updated instance of Db after calling service's logic. Is it possible to implement?
If I understand the question correctly, you can define your Db API in a separate module, and the capabilities like this:
type AddValueCapability =
private AddValueCapability of (string -> string -> Db -> unit * Db)
let addValueCapability = AddValueCapability add
let runAddValue (AddValueCapability f) = f
Notice that the AddValueCapability
type has a private
case constructor, which means that no code outside of the defining module can pattern-match on it.
The addValueCapability
value is the only way external client code can get a hold of a value of that type. You may or may not want to export that value as well, depending on your exact needs.
While external client code can't pattern-match on the AddValueCapability
case constructor, it can run the capability using the runAddValue
function.
Here's a simple example of a function that is defined outside of the Db module:
let useCapability (cap : AddValueCapability) db =
runAddValue cap "foo" "bar" db
It can run the capability, but not anything else if you also lock down the underlying functionality:
let private get id db =
let (Db dict) = db
dict
|> Map.tryFind id
|> (fun x -> x, db)
let private add id value db =
let (Db dict) = db
dict
|> Map.add id value
|> Db
|> (fun x -> (), x)
let private remove id db =
let (Db dict) = db
dict
|> Map.remove id
|> Db
|> (fun x -> (), x)
Here's an xUnit.net demo of it:
[<Fact>]
let Demo () =
let db = Map.empty |> Db
let _, actual = useCapability addValueCapability db
Assert.Equal (Map.ofList ["foo", "bar"] |> Db, actual)
You can create other capabilities in a similar fashion, and pass only those capabilities around that you want client code to have...
...or you could just define some single-method interfaces and pass those around to client code. That's how you'd usually do it in OOD.