avatarGabriel Shanahan

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

2797

Abstract

is long forgotten, and <code>format</code> gets called with a value that was not validated first</li><li>a new business requirement arises which requires different “levels” of validation. Again, <code>format</code> might expect those parts to be there from the beginning, and gets called with input that was validated in a different way than it expects.</li></ul><p id="3e74">A specific example of the latter case might be <code>CalculationResult = ClientData</code>, where we're validating that we have all the client's data. When we're first creating a client, we might only require a name and an e-mail, but once we’re at the point where we’re e.g. signing a contract, we also need an address and bank account. So we modify the <code>valid</code> function to only check the appropriate parts of <code>ClientData</code> based on the context and think we're done. However, we don't realize that <code>format</code> expects all the values to be there, and get a runtime error — the <a href="https://readmedium.com/preface-a65cb535d122#2662">worst kind of error</a>.</p><p id="2e81">Another problem is that we don’t know which “outputs” can actually be generated by this code. If everything goes well, we can see clearly what happens, but what if the result is not valid? An exception gets thrown, and that gets handled in one of the callers, i.e. someplace else. It’s not clear where that place is, how we get there from here (there are usually multiple callers) and what happens there. Things get even more complicated when using things like <a href="https://www.baeldung.com/exception-handling-for-rest-with-spring"><code>ControllerAdv>ice, Excepti</code>onHandler</a> and similar constructs (which is often the case in the real world). To be sure of what happens what an exception gets thrown, one must backtrack through all possible execution paths from the point the exception gets thrown, which is not a feasible approach. In other words, exceptions break <b>local reasoning</b>.</p><p id="3dae">In total, this means that there are (at least) two different places where responses get produced, which means (at least) two different places we need to be aware of, manage, maintain and keep in sync when making changes. Worst of all, we need to do all this manually — the compiler will not let us know if we change one, but forget to change the other.</p><p id="a998">It turns out that these problems have a solution. <b>The core idea is to represent <i>everything</i> (including error states) as data types</b>:</p> <figure id="075a"> <div> <div> <img class="ratio" src="http://placehold.it/16x9"> <iframe class="" src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fpl.kotl.in%2F-4ioREB4U&amp;display_name=Ko

Options

tlin+Playground&url=https%3A%2F%2Fpl.kotl.in%2F-4ioREB4U&image=https%3A%2F%2Fplay.kotlinlang.org%2Fassets%2Fog-image.png&key=a19fcc184b9711e1b4764040d3dc5c07&type=text%2Fhtml&schema=kotl" allowfullscreen="" frameborder="0" height="300" width="800"> </div> </div> </figure></iframe></div></div></figure><p id="1e38">Even in code as simple as this, this approach leads to a markedly better results.</p><p id="eb3d">The key benefits are:</p><ul><li>Illegal states (in fact all states) are explicit, and represented by types.</li><li>Function signatures <a href="https://readmedium.com/preface-a65cb535d122#39fa">communicate and enforce assumptions</a> (<code>format</code> requires its input to be validated first, and the type denotes the manner in which the validation is performed — you can differentiate between e.g. <code>PartialValidationResult</code> and <code>FullValidationResult</code>)</li><li>The data flow is completely linear. There are no branches, jumps, no catch blocks, no special situations, it’s just <code>calculateValue -> validate -> format -> sendResponse</code></li><li>The ability to reason locally is recovered. Formatting of all data is done in one place, for all scenarios. Again, no special situations, no alternative ways a response can get sent, no <code>ExceptionHandler</code>s, no <code>ControllerAdvice</code> etc.</li><li>Since we use sealed classes, whenever we add a new state (e.g. a <code>PartialValidationResult</code>), we are immediately told which parts of the code we need to adapt. We’ve completely removed a whole category of errors. Again.</li></ul><p id="6154">For more on this, I highly recommend reading <a href="https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/">this great article</a>. In fact, you should go ahead and read <a href="https://fsharpforfunandprofit.com/series/designing-with-types/">the entire series</a>, it will make you a better person.</p><p id="c99c">Go back to <a href="https://readmedium.com/sealed-hierarchies-preventing-runtime-errors-a72c4b0c295c">Safely Emulating Dynamic Dispatch</a>, jump to the <a href="https://readmedium.com/table-of-contents-c52573cfa291">Table of Contents</a>, or continue to <a href="https://readmedium.com/sealed-hierarchies-strongly-typed-domain-modeling-c8e2731ea181">Modeling States and Structure</a>.</p><figure id="8ecd"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*biBSB579iezsNvEQ_NMLBg.png"><figcaption><a href="https://www.etnetera.cz/prace-u-nas?utm_source=medium&amp;utm_medium=GabrielShanahan&amp;utm_campaign=KotlinPrimer&amp;utm_content=join-our-team&amp;utm_term=KotlinPrimer#pozice">Join me in Etnetera</a></figcaption></figure></article></body>

Modeling Illegal States

How to use sealed hierarchies to shift away from exceptions thereby preventing runtime errors, enforcing assumptions, recovering the ability to reason locally and achieving linear code flow.

— — — — — — — — — — — — — — —

THE CURRENT VERSION OF THIS ARTICLE IS PUBLISHED HERE.

— — — — — — — — — — — — — — —

Tags: #FUNDAMENTAL CONCEPT

This article is part of the Kotlin Primer, an opinionated guide to the Kotlin language, which is indented to help facilitate Kotlin adoption inside Java-centric organizations. It was originally written as an organizational learning resource for Etnetera a.s. and I would like to express my sincere gratitude for their support.

It is recommended to read the Introduction before moving on. Check out the Table of Contents for all articles.

Take a look at the following code, which is basically a template for every business rule ever written:

At first glance, there really doesn’t seem to be anything wrong with the way the code is written. However, on closer inspection, we might realize that there are in fact some potential problems.

For one, the format function may implicitly rely on the fact that value is valid. This could cause problems in two different scenarios:

  • 10 years from now, this requirement is long forgotten, and format gets called with a value that was not validated first
  • a new business requirement arises which requires different “levels” of validation. Again, format might expect those parts to be there from the beginning, and gets called with input that was validated in a different way than it expects.

A specific example of the latter case might be CalculationResult = ClientData, where we're validating that we have all the client's data. When we're first creating a client, we might only require a name and an e-mail, but once we’re at the point where we’re e.g. signing a contract, we also need an address and bank account. So we modify the valid function to only check the appropriate parts of ClientData based on the context and think we're done. However, we don't realize that format expects all the values to be there, and get a runtime error — the worst kind of error.

Another problem is that we don’t know which “outputs” can actually be generated by this code. If everything goes well, we can see clearly what happens, but what if the result is not valid? An exception gets thrown, and that gets handled in one of the callers, i.e. someplace else. It’s not clear where that place is, how we get there from here (there are usually multiple callers) and what happens there. Things get even more complicated when using things like ControllerAdv>ice, ExceptionHandler and similar constructs (which is often the case in the real world). To be sure of what happens what an exception gets thrown, one must backtrack through all possible execution paths from the point the exception gets thrown, which is not a feasible approach. In other words, exceptions break local reasoning.

In total, this means that there are (at least) two different places where responses get produced, which means (at least) two different places we need to be aware of, manage, maintain and keep in sync when making changes. Worst of all, we need to do all this manually — the compiler will not let us know if we change one, but forget to change the other.

It turns out that these problems have a solution. The core idea is to represent everything (including error states) as data types:

Even in code as simple as this, this approach leads to a markedly better results.

The key benefits are:

  • Illegal states (in fact all states) are explicit, and represented by types.
  • Function signatures communicate and enforce assumptions (format requires its input to be validated first, and the type denotes the manner in which the validation is performed — you can differentiate between e.g. PartialValidationResult and FullValidationResult)
  • The data flow is completely linear. There are no branches, jumps, no catch blocks, no special situations, it’s just calculateValue -> validate -> format -> sendResponse
  • The ability to reason locally is recovered. Formatting of all data is done in one place, for all scenarios. Again, no special situations, no alternative ways a response can get sent, no ExceptionHandlers, no ControllerAdvice etc.
  • Since we use sealed classes, whenever we add a new state (e.g. a PartialValidationResult), we are immediately told which parts of the code we need to adapt. We’ve completely removed a whole category of errors. Again.

For more on this, I highly recommend reading this great article. In fact, you should go ahead and read the entire series, it will make you a better person.

Go back to Safely Emulating Dynamic Dispatch, jump to the Table of Contents, or continue to Modeling States and Structure.

Join me in Etnetera
Kotlin
Java
Programming
Functional Programming
Object Oriented
Recommended from ReadMedium