Modeling With Sum Types Example
Expanding on the business rules section in classes we’ll model a more complex case where we have mutually exclusive results with different data per type. Basically the result is one and only one of 4 possible types, and each type has different data.
Our example will be authorizing a credit card using an external service. Remote service calls can really benefit from this approach, because they always have a failure state that probably should be handled specially, and it’s really easy for a client to miss that because it looks like a normal function call.
Our Business Rules:
- If authorization is denied, display a message to the user and let them retry.
- If cvv check fails, display a message to the user and let them retry, but also log the attempt, and void the transaction.
- If the service times out, or is otherwise unavailable, finish the order without cc auth.
- If authorization is successful, process order with auth info.
How can we implement this? One option is to use the service directly, and then massage the results in a sort of ad-hoc way. The service call we will use is:
AuthResponse GetAuth(AuthRequest request);
Dump It In Main!
So one option, in our controller when we need to auth a credit card we call this service:
var response = _service.GetAuth(request);
if (response.Status.ToLower() != "approved") {
var message = "ResponseMessage: " + response.Message;
return RedirectToAction("Billing", new ValidationFailure(message));
}
//etc...
What are the problems we’ll have with this?
How do we test this chunk of code? Fire up the website, add to cart, login/enter address, enter other billing info, figure out what to tell the service to get it to return denied, etc.
How do we test the service down case? Same as above, but put a breakpoint in there somewhere and force it to timeout? The feedback loop there could be better.
We’re also pretty exposed to the internals of the service. We have access and are coupled to everthing in the Response class and RequestClass. Some encapsulation would be helpful here.
What happens when finish all that, and then realize a REST API or nightly process also needs to support these business rules?
I’d be worried this would end up buggy, slow to implement, and slow to modify.
As a Library?
First step is to take this logic, and implement it as a library (ui agnostic). Instead of taking action as part of the calculation, we’ll separate results (approved, denied, etc) from what we do with those results (RedirectToAction, json return object, etc). We need a structure to handle our results.
We know we have 4 possibilities, so maybe an enum?
public enum ResultType {
Approved,
Denied,
CvvFailed,
Unavailable
}
We know that we have a message if auth is denied, and auth data if it’s approved, so we need more:
class AuthResultWithEnum {
public enum ResultType {
Approved,
Denied,
CvvFailed,
Unavailable
}
public ResultType Result;
public XElement AuthNode;
public string DeniedMessage;
}
This can be made to work, but what problems are there?
It doesn’t really match our business requirements:
var r = new AuthResultWithEnum() {
Result = AuthResultWithEnum.ResultType.Approved,
DeniedMessage = "too poor!"
};
It doesn’t really help clients know what’s going on:
if (r.Result == AuthResultWithEnum.ResultType.Denied) {
RedirectToAction("Billing", r.DeniedMessage);
}
Here the client has to know to grab DeniedMessage when it failed, but there’s nothing to really help knowing how to handle success, or that CvvFailed should be handled too, etc.
So here, we’ve maybe made it more reusable, but it’s actually more complex, and the code in the client looks pretty similar to our “just dump it in main” solution. It can be tested easier, but our client code is still pretty error prone, which isn’t any easier to test than before, so this is a marginal win at best.
Domain Modeling
If we make our data structure match our business rules, things get much simpler.
The auth node goes with Approved only:
public sealed class Success : AuthResult {
public readonly long AuthCode;
public Success(long authCode) {
AuthCode = authCode;
}
}
The failure message is only when denied:
public sealed class Denied : AuthResult {
public readonly string Message;
public Denied(string message) {
this.Message = message;
}
}
We have 2 more cases, and our result is 1 and only 1 of these types, so we’ll use subclasses:
public abstract class AuthResult {
public sealed class Denied : AuthResult {
public readonly string Message;
public Denied(string message) {
this.Message = message;
}
}
public sealed class InvalidCvv : AuthResult {}
public sealed class UnAvailable : AuthResult {}
public sealed class Success : AuthResult {
public readonly long AuthCode;
public Success(long authCode) {
AuthCode = authCode;
}
}
}
Now we can’t make the same mistakes as before. To represent Success, we must pass it an auth node, and can’t pass it a failure message. As a client though, this is still a little awkward:
AuthResult result = authService.TryAuth(2357, 4.00m);
//now what?
//result.???
We can’t do anything useful with an AuthResult, we need the actual type, so we’d need to cast it:
AuthResult result = authService.TryAuth(2357, 4.00m);
if (result is AuthResult.Success) {
var authData = ((AuthResult.Success) result).AuthCode;
}
This works, but again leaves the client pretty cold. Client still needs to know the different types to cast to, and to knowing to cast is pretty non-obvious. To solve that, we’ll add a Match function to the parent class that does the appropriate cast. The client is expected to pass in a function that handles each case.
public T Match<T>(
Func<Denied, T> aDenied,
Func<InvalidCvv, T> aInvalidCvv,
Func<Success, T> aSuccess,
Func<UnAvailable, T> aUnavailable) {
if (this is Denied) {
return aDenied((Denied)this);
}
if (this is InvalidCvv) {
return aInvalidCvv((InvalidCvv)this);
}
if (this is UnAvailable) {
return aUnavailable((UnAvailable)this);
}
if (this is Success) {
return aSuccess((Success)this);
}
throw new InvalidOperationException("unexpected");
}
This looks complex and scary, and it’s a pain to type. However, every one of them is exactly the same, and maybe C# 7 will actually get pattern matching and then we can get the same effect without the boilerplate. Beyond the rough syntax, what’s going on is pretty simple. Given an AuthResult (which is one of the subclasses) it will pick the subclass it actually is, and run the passed in function. Perhaps easier to see with an example.
return authResult.Match<ActionResult>(
denied => {
var v = new ValidationSummaryModel("Payment", "Please review your billing information.");
return EditBilling(v);
},
invalidCc => {
var v = new ValidationSummaryModel("Payment", "Please review your cvv information.");
return EditBilling(v);
},
success => {
Process(success.AuthCode);
return RedirectToAction("Receipt", "Checkout"); },
unavailable => {
Process();
return RedirectToAction("Receipt", "Checkout"); }
);
This is nice because the client can tell from the match signature what the various possible cases are, and the compiler helps the client do the right thing. If something changes, our compiler will help us make sure we change all clients appropriately.
Conclusion
Matching the domain with types is useful, but C# doesn’t have a built in OR mechanism, eg a value that’s Success OR Denied OR DeniedForSomeOtherReason. We can still model problems that way, but it’s a fair amount of boilerplate. It’s frequently worth it, and definitely a pattern worth knowing about because it’s not exactly intuitive.
More reading: http://bugsquash.blogspot.com/2012/01/encoding-algebraic-data-types-in-c.html and https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable is great on the purpose and benefits of this.