I have been doing some introspection on the way I write code to find ways that I need to improve. I consider this a task that one must do periodically so that we keep organized. There is a very, very simple problem that occurs in every application I know:
How to return the results of an operation to the user?
I've seen many implementations. Some return strings, some throw exceptions, some use out parameters, reuse the domain classes and have extra properties in there, etc. There is a myriad of ways of accomplishing this. This is the one I use.
I don't like throwing exceptions. There are certainly cases where you have no choice, but I always avoid that. Throughout my architectures there is a single prevalent type that hasn't changed for years now, and I consider that a sign of stability. It is so simple, yet so useful everywhere. The name may shock you, take a look:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Result | |
{ | |
public Result(string code = null, string message = null, bool success = false); | |
public bool Success { get; set; } | |
public string Code { get; set; } | |
public string Message { get; set; } | |
} | |
public class Result<T> : Result | |
{ | |
public Result(string code = null, string message = null, T obj = default, bool success = false); | |
public T Object { get; set; } | |
} |
Yes, this is it. Take a moment to compose yourself.
Mind you, this is used everywhere, in every layer. We are talking about Web APIs, Client Applications, Console Middlewares, the internal and external layers of every god damn piece of code I put my hands on.
Here are some very, very interesting properties of having it:
- Consistency - If it is common practice to use a single, stable type, everyone knows implicitly what they are working with, what are the use cases and it just feels more comfortable.
- Predictability - If you have multiple developers working on different tasks, they will probably have the need to return results of some shape. You don't need to argue the shape of the results, this is it.
- Abstraction - Even if we have developers at different layers of the stack, by using a single well-defined abstraction to return results we are making our lives easier. The approach is simply the same in backend, frontend, etc.
How does it feel to actually use this?
Let's define some very very simple methods and see how this looks:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public List<Result> CreateInvoices(Customer customer, List<Invoice> invoices) | |
{ | |
var customerResult = ValidateCustomer(customer); | |
if (customerResult.Success == false) | |
return new List<Result>() { customerResult }; | |
var invoicesResults = ValidateInvoices(invoices); | |
//We want these for the user | |
if (invoicesResults.Any(x=> x.Success == false)) | |
return invoicesResults.Select(x=> (Result)x).ToList(); | |
return new List<Result> {new Result("OK", "Operation succeeded", true)}; | |
} | |
private Result ValidateCustomer(Customer c) | |
{ | |
if (string.IsNullOrWhiteSpace(c.Name)) | |
return new Result("E1", "Customer name cannot be empty!"); | |
else | |
return new Result("OK", "", true); | |
} | |
private List<Result<Invoice>> ValidateInvoices(List<Invoice> invoices) | |
{ | |
List<Result<Invoice>> results = new List<Result<Invoice>>(); | |
if (invoices == null || invoices.Count == 0) | |
{ | |
return new List<Result<Invoice>>() { new Result<Invoice>("E3", "No invoices provided!"))}; | |
} | |
foreach (var invoice in invoices) | |
{ | |
if (invoice.IDCustomer == null) | |
results.Add(new Result<Invoice>("E2", "Invoice must have a customer!", invoice)); | |
//..... | |
} | |
return results; | |
} |
Dealing with ugliness
It is notable that we spend all the time dealing with the Result type, returning it, creating it, etc.I take some extra steps to clean up the code.
First of all, define some conventions that apply everywhere.
- An empty list means an operation has succeeded. It should be the same as a list with only successful results (assuming the Object does not carry important information in such cases).
- We never return null from a method that returns a Result or List<Result>.
- We try to model exceptions we can deal with as Result instances. A database connection exception will be caught, transformed to a Result and then returned.
Then, to deal with the code clutter we start by defining static instances of Result. An obvious instance that is used everywhere is the OK result:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class Results | |
{ | |
public static Result Ok{get;set;} = new Result("OK", "Operation succeeded", true); | |
public static Result Customer_EmptyName {get;set;}= new Result("E1", "Customer name cannot be empty!"); | |
public static Result Invoice_MustHaveCustomer {get;set;}= new Result("E2", "Invoice must have a customer!"); | |
public static Result No_Invoices {get;set;} = new Result<Invoice>("E3", "No invoices provided!") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class ExtensionMethods | |
{ | |
public static List<Result<T>> AsList<T>(this Result<T> r) => new List<Result<T>> { r }; | |
public static List<Result> AsList(this Result r) => new List<Result> { r }; | |
public static Result AsGeneric<T>(this Result<T> r)=> r; | |
public static List<Result> AsGeneric<T>(this List<Result<T>> r)=> r.Select(x=> (Result)x).ToList(); | |
public static List<Result> AsGeneric<T>(this IEnumerable<Result<T>> rs) => rs.Select(x=> x.AsGeneric()).ToList(); | |
public static Result<T> Of<T>(this Result r, T obj = default) => new Result<T>(r.Code, r.Message, obj, r.Success); | |
public static List<Result> IfAnyInvalidOr(this IEnumerable<Result> rs, List<Result> alternative) | |
{ | |
var tmp = rs.Where(x=> x.Success == false); | |
if (tmp.Any()) | |
return rs.ToList(); | |
else | |
return alternative; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public List<Result> CreateInvoices(Customer customer, List<Invoice> invoices) | |
{ | |
var customerResult = ValidateCustomer(customer); | |
if (customerResult.Success == false) | |
return Results.Ok.AsList(); | |
return ValidateInvoices(invoices).AsGeneric().IfAnyInvalidOr(Results.Ok.AsList()); | |
} | |
Result ValidateCustomer(Customer c) | |
{ | |
if (string.IsNullOrWhiteSpace(c.Name)) | |
return Results.Customer_EmptyName;; | |
else | |
return Results.Ok; | |
} | |
List<Result<Invoice>> ValidateInvoices(List<Invoice> invoices) | |
{ | |
List<Result<Invoice>> results = new List<Result<Invoice>>(); | |
if (invoices == null || invoices.Count == 0) | |
{ | |
return Results.No_Invoices.Of<Invoice>().AsList(); | |
} | |
foreach (var invoice in invoices) | |
{ | |
if (invoice.IDCustomer == null) | |
results.Add(Results.Invoice_MustHaveCustomer.Of(invoice)); | |
//..... | |
} | |
return results; | |
} |
The main highlights are the lack of duplicated strings and the absence of initial clutter.
Some of the methods take inspiration from the F# Option module, which has very cool functions allowing you to manipulate the values with ease in a fluent way.
What is your way of dealing with this problem?
Update: here is a follow-up on this post with enhancements on the approach and the feedback that I gathered
Comments
Post a Comment