APIs for Critical Systems
Using Fluent Interfaces to enforce Workflow
In 2000, a software system used to plan radiation treatment inadvertently caused the death of 5 patients through radiation overdose. The cause was found to be poor software design and human error. As software engineers, it is vital to always remember:
In engineering, failure equals death. - James Cameron
In web applications, we often talk about building in layers of security to avoid data breaches. In critical systems, we need to consider layers of safety. All components, hardware and software, will require multiple fail-safes to avoid injury. When it comes to software, a safe system would:
- Adhere to the requirements
- Avoid impossible states
Adhering to the requirements involves a lot of communication and discussion to ensure all stakeholders understand the risks involved and the safe guards to prevent injury. Avoiding impossible states is building the software in a way to avoid unsafe operation. One technique to achieve this is to design a Fluent interface.
Fluent interface
A fluent interface is an interface that enables method chaining. That means methods can be called successively just by adding a dot. The end goal is that it's suppose to make code more readable and easier to understand. However, an alternative goal is to make a domain-specific language (aka DSL). By designing the API to address the business requirements, developers and product owners can reason about the code without needing to understand the low-level details.
Consider the following example for tallying a food bill:
public double GetTotal(Food[] foodItems) {
var total = 0;
foreach (var item in foodItems) {
total += item.Price;
}
return total * 1.13;
}
This example is fairly trivial and any junior developer would understand the code, but it still requires some thinking to understand the intent. Imagine if we created extension methods to calculate the gross total and apply tax, then a DSL could be designed to achieve the following:
public double GetTotal(Food[] foodItems) {
return foodItems
.CalculateGrossTotal()
.ApplySalesTax(Province.Ontario);
}
Subjectively, this is much easier to reason about. For what its worth,
fluent interfaces are focused on communicating intent.
Fluent Interfaces and your IDE
One of the coolest things about a Fluent Interface, is that the IDE can be used as a tool to guide developers. In the video above, we can see that once we have the foodItems
, the IDE will suggest possible valid method calls. When the gross total has been calculated, the only thing a developer can do is:
- Get the gross total.
- Apply the sales tax according to province.
With a procedural approach, applying the sale tax according to province was implicit. Current and future developers have to know that multiplying the total by 1.13 is the way that the sales tax is calculated for the province of Ontario. With the fluent interface, the major difference is that the code becomes declarative. Developers only need to consider what needs to be done instead of how it is done. Furthermore, since the workflow is baked into the interface, a developer cannot calculate the sales tax without first calculating the gross total.
Tradeoffs
Some people think fluent interfaces are evil and some just bad. This is because some tradeoffs have to be made. However, when safety is required, the tradeoffs might be worth it. The general idea is to design the interfaces to be optimized for safety and readability, not developer convenience. If the code is used in an unsafe way, it should not compile. In general, the tradeoffs are:
- More time is required for design
- "Ugly" code
- Creates coupling
For the first tradeoff, Rich Hickey alludes, we should be spending more time with design instead of banging away on the keyboard. Bugs are much cheaper to fix at design time than in production, especially with critical systems.
Fixing a bug after it has killed a patient is much worse than spending several weeks hashing out an interface.
The second tradeoff is more subjective, but given all things being equal, I would rather have a clean interface with an ugly implementation, than a poor interface with a clean implementation. The reasoning is that with a clean interface, the implementation can be refactored in the future with minimal impact. With a poor interface, a refactor would impact all users of the interface.
The third tradeoff is that fluent interfaces often create a certain amount of coupling. However, in this case we want to couple the API with the workflow. What that means is that if the workflow changes, it might require significant work. However, for critical systems, changes to workflow usually have enough impact to warrant significant work in both software design and user process. This coupling is usually the reason why some people think fluent interfaces are bad, but the point people are actually trying to make is that fluent interfaces shouldn't be used everywhere.
Fluent interfaces are not evil or bad, using them in the wrong use case is.
Designing Fluent Interfaces
Let's see how the food total fluent interface could be implemented. To set the record straight, this example is a trivial problem and probably might not warrant a DSL. However, it would be too complex to demonstrate creating a Fluent interface to administer radioactive treatment.
For this example, let's assume all monetary values are in Canadian dollars and that we want to optimize for understanding. To create a fluent interface, we should consider the workflow first. When enforcing a workflow, you can think of it as creating a data pipeline, but using the types to enforce flow. In this case, our data flow can be thought of as a transformation of:
food items => gross total => net total
To enforce this flow, we will need a separate interface for each step. The idea is to chain the methods in such a way that the output of one leads to the input of another. Using the type system, we can enforce what methods can be called and when.
// I really wish .NET had the equivalent of Java JSR 354.
public interface IMoney {
decimal Value { get; }
RegionInfo Region { get; }
}
public interface IFood {
IMoney Price { get; }
}
public interface IGrossTotal {
IMoney Total { get; }
}
public interface INetTotal {
IMoney Total { get; }
}
Aren't IGrossTotal
and INetTotal
the same thing?
It may seem unnecessary to define IGrossTotal
and INetTotal
because they appear identical, but from a domain perspective they are two completely different concepts. The gross total represents the total before tax and the net total represents the total after tax. This allows both the business and developer to be explicit about a total. In this case, we remove ambiguity to allow for maximum communication at the cost of an extra interface.
Chaining the Workflow
With the interfaces available, we can chain the function calls through extension methods or using regular object oriented programming. CalculateGross
operates on IFood
items and returns an IGrossTotal
. ApplySalesTax
operates on IGrossTotal
to produce a INetTotal
. This ensures that CalculateGross
must be called before ApplySalesTax
.
public static class SalesExtensions
{
public static IGrossTotal CalculateGross(this IEnumerable<IFood> foodItems)
{
...
}
public static INetTotal ApplySalesTax(this IGrossTotal grossTotal, Province province)
{
...
}
}
Further restrictions could be embedded in the code by making the visibility of all concrete implementations as internal. In this case, since IGrossTotal
and INetTotal
are intermediary types, any concrete implementation should be made internal to avoid circumventing the workflow. It's not totally bullet proof because it still can't guard against someone from hacking in their own implementation, but hopefully, the code review process can catch that.
I highly recommend trying to implement this on your own. It should be noted, that this does add a lot more code due to all the interfaces. My final implementation that supported all Canadian provinces ended up being around 100 lines of code. Interestingly, the amount of logic remained the same, but the amount of data types increased. So from a maintainability point of view, the cost is for maintaining more explicit types, but when it comes to optimizing for safety, it may well be worth the cost.
Programming with Seat Belts and Air Bags
For my current project, the API ensures that any developer must work with the system within its boundaries. Consider the following:
var service = new MotorService();
service.SelectMotor(Motor.A);
service.CalibrateMotor();
service.ConfirmCalibration();
service.StartMotor();
var result = service.ApplyAdjustment(new Adjustment(15));
LogResult(result);
service.StopMotor();
This code might seem familiar, but the challenge is that the service
needs to hold the state. Run time checks need to be done to ensure the workflow. For example, logic needs to be added to ConfirmCalibration
to ensure that that the motor has been calibrated. If a developer accidentally calls ConfirmCalibration
before CalibrateMotor
, then that could put the service
in a bad state. This adds complexity and the potential for bugs. State management should never be under estimated. It is hard!
Using a fluent interface, we can achieve the following:
var service = new MotorService();
service
.SelectMotor(Motor.A)
.CalibrateMotor()
.ConfirmCalibration()
.StartMotor()
.ApplyAdjustment(new Adjustment(15))
.Then(LogResult)
.StopMotor();
In this case, state management is not needed because each step returns a new type. A developer cannot call ConfirmCalibration
without first calling CalibrateMotor
. Similarly, the more critical step is that the motor cannot be started without the confirmation. This API is design to optimize for safety and avoid developers from accidentally introducing bugs.
This API took several weeks to flesh out with several meetings with stakeholders and team members. Sequence and state diagrams were done to visually identify any potential safety issues. After all that planning, it took about a day to implement. This is the cost of a fluent interface. Less hands on keyboards, more minds at work.
For fluent interfaces, collaboration == communication > coding
Conclusion
Fluent interfaces is one tool that can be used to create stable and safe software. It is not a silver bullet, but is worth considering when safety and readability are paramount. I recommend trying it out, but before using it in production, work with your team to see if it fits the use case. I also highly recommend reading and understanding the arguments for and against it. It is always vital to understand the tradeoffs and use cases. At the end of the day, knowing how to create Fluent Interfaces is just another tool in the developer's toolkit - neither good nor bad.
References and Resources
- Design, Composition, and Performance by Rich Hickey
- Domain Modeling Made Functional by Scott Wlaschin
- Fluent Interfaces by Erik Schierboom
- Fluent Interfaces Are Bad for Maintainability by Yegor Bugayenko
- Fluent Interfaces are Evil by Marco Pivetta
- When Software Kills by Keri Savoca
- Header image by Scouten, CC BY-SA 3.0, via Wikimedia Commons