avatarGabriel Shanahan

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

5751

Abstract

  <figure id="1297">
        <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%2FzYNsKVtkn&amp;display_name=Kotlin+Playground&amp;url=https%3A%2F%2Fpl.kotl.in%2FzYNsKVtkn&amp;image=https%3A%2F%2Fplay.kotlinlang.org%2Fassets%2Fog-image.png&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=kotl" allowfullscreen="" frameborder="0" height="300" width="800">
          </div>
        </div>
    </figure></iframe></div></div></figure><p id="fd6f">Nothing fancy going on. As is customary, each method checks for various preconditions before doing its thing, and apart from using return values instead of exceptions, it probably looks pretty much like most service layers you’ve encountered.</p><p id="eacf">Now take a step back and take a good hard look at what we wrote. Do you like it? Because <b>I do not</b>!</p><ul><li>There are more lines spent checking for preconditions in each method than there is actual business logic</li><li>We have to keep checking for the same preconditions over and over again, and things that are obviously implied by the business context are not guaranteed. For example, if a <code>User</code> is in a <code>VERIFICATION_PENDING</code> state, he must have a <code>name</code> and an <code>address</code>, but <b>this not guaranteed by the type</b>. This means that even when we <i>know</i> that the <code>name</code> and <code>address</code> should be there, they’re still as nullable as ever, which leads to a whole bunch of checks that seem redundant, but have to be there.</li><li>One of the reasons why these checks have to be there is because there’s no guarantee someone won’t send in an object that breaks these assumptions — it can happen, and with this design, there’s no way to prevent this from happening.</li><li>When that happens, the error will only get caught at runtime, and runtime errors are <a href="https://readmedium.com/preface-a65cb535d122#2662">our worst enemy</a>. This can happen either as a result of an honest mistake, but also, as we’ve mentioned repeatedly, as a consequence of the fact that 10 years from now, nobody on the team will have been there when this code was written and these assumptions will long be forgotten</li><li>The assumptions are not <a href="https://readmedium.com/preface-a65cb535d122#39fa">communicated by the code</a>. Doc comments don’t cut it, because they’re not enforced — there’s no way to guarantee that a doc comment is accurate, and stays accurate. On a long enough time scale, <a href="https://www.codeproject.com/Articles/872073/Code-Comments-are-Lies">comments always lie</a>.</li><li>The <a href="https://en.wikipedia.org/wiki/Defensive_programming">defensive programming</a> techniques that this leads to, where each method needs to start with a gazillion checks that all sorts of preconditions are not broken, fill the codebase with clutter and make code difficult to read and understand. They also needlessly increase <a href="https://en.wikipedia.org/wiki/Cyclomatic_complexity">cyclomatic complexity</a>, which in turns makes code more difficult to test (more paths through the code means more paths we need to test).</li><li>The further in any such workflow we are, the more preconditions we need to check, because we usually need to repeat all the checks that were already done 10 times in the previous steps + add new checks for the current step — this can be seen in the <code>markAsX</code> methods above. Even in simple code as this, things get unwieldy fast, and we all know that it will be much worse in reality.</li><li>Let’s face it — almost nobody actually does all those checks. What usually happens in the real world is that some checks are indeed made at the beginning of a method, but not all of them, and the rest are just assumed to be true, because we’re human, there’s no time, etc. Hopefully, there is a test suite to back these assumptions up, but there often simply isn’t, for the same reasons. And even when there is, as we’ve seen, a green test suit is no guarantee that there are no problems with the code base.</li></ul><p id="40e0">In other words, this whole situations is a ticking bomb of silent errors and regressions just waiting to blow up, and we can’t help but ask ourselves: can we do better?</p><p id="06ab">It turns out we can.</p><h2 id="1f0e">States and Structure as Separate Types</h2><p id="e463">The solution is exactly the same as in the previous chapter —<b> represent those states using types</b>.</p>
    <figure id="acfc">
        <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%2FnpoVHw-4C&amp;display_name=Kotlin+Playground&amp;url=https%3A%2F%2Fpl.kotl.in%2FnpoVHw-4C&amp;image=https%3A%2F%2Fplay.kotlinlang.org%2Fassets%2Fog-image.png&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=kotl" allowfullscreen="" frameborder="0" height="300" width="800">
          </div>
        </div>
    </figure></iframe></div></div></figure><p id="1ce4">Let’s take a look at everything that’s changed:</p><ul><li>We no longer need a <code>state</code> property — the state is encoded in the type.</li><li>The existence or non-existence of a property, and the conditions under which it does, is explicitly communicated and enforced. You <i>cannot</i> create a <code>VerifiedUser</code> without specifying the <code>validatedAt</code> property. If something is nullable, it means it is <i>not required</i>, as opposed to 

Options

“might be required at some unknown point down the road if some unknown conditions are met”.</li><li>In the various <code>Result</code> classes, we can be explicit about the fact that on success, it produces the next state (<code>PendingUser</code> or <code>VerifiedUser</code>) and on failure, it doesn’t change the state.</li><li>No need to check the state in <code>prepareForVerification</code> — guaranteed to be correct, always.</li><li>No need to check anything at all in <code>markAsVerified</code> and <code>markAsFailed</code>— everything is guaranteed to be there, always.</li><li>Cyclomatic complexity is down, which means less time writing tests — at least 7 tests are no longer needed to achieve the same coverage.</li><li>All assumptions are explicitly communicated and enforced. You simply cannot send a <code>NewUser</code> into <code>markAsVerified</code>, and you never will be, not today, not tomorrow, and not 10 years from now when you’re no longer around. If someone makes a mistake, the code won’t compile, and they will know exactly what needs to be fixed.</li></ul><p id="defd">So, by replacing a single class with an interface and 3 simple classes, we prevented a whole bunch of errors from <i>ever</i> happening, made the code shorter (this will be especially pronounced in real world scenarios, where service code comfortably outnumbers “data” code), more concise, easier to read, understand and maintain, saved ourselves from writing several tests, made the code self-documenting, saved ourselves from having to do null-checks before we access properties and saved ourselves from having to explicitly manage the users state. What’s not to love?</p><h2 id="0115">Going Further</h2><p id="a229">There’s even more you could do. For example, you could realize that the situation with the nullable <code>id</code> property is exactly the same one — the <code>id</code> can only be null when you’re first creating the object, but can never be <code>null</code> once the object is saved — in other words, you have a <code>PersistedUser</code>, and an <code>UnpersistedUser</code>.</p><p id="63c9">Right off the bat, this opens up new doors for you. For example, imagine having to retrieve all the posts of a <code>User</code>. Naturally, that only makes sense for persisted users, since users that aren’t saved to the database can’t post. This will manifest itself in a practical issue as well, because you will have to send the <code>id</code> into some sort of repository that will fetch posts related to that user <code>id</code>, but if <code>id</code> is nullable, you’ll have to deal with the null variant somehow, just in case someone somewhere down the road decides to be clever and actually send in a <code>User</code> that wasn’t saved yet. But split the <code>User</code> into a <code>Persisted</code> and <code>Unpersisted</code> variant, and suddenly you’re <i>guaranteed</i> that this can’t happen.</p><h2 id="5994">Connection to Sealed Hierarchies</h2><p id="83cc">Interestingly enough, if you really think about it, most of the benefits stated here are actually not dependent on the concept of sealed hierarchies at all — everything would work out exactly the same if <code>sealed interface User</code> was just an <code>interface User</code>.</p><p id="e9ec">There are two reasons why I’m mentioning it in this context:</p><ul><li>it’s a direct extension of the technique described in the previous chapter, which <i>is</i> dependent on sealed hierarchies</li><li>because even though it’s not strictly necessary to use a sealed hierarchy for the example we demonstrated, this is definitely a use-case that should be modeled using a sealed hierarchy. The set of “user states” should definitely be fixed at compile time, and should not be extensible by a 3rd party library, no more than an enum could. This is not only an issue of <a href="https://readmedium.com/preface-a65cb535d122#f495"><i>function</i>, but of <i>form</i> as well</a> — we’re <i>communicating</i> intent, specifically that this is intended to be a fixed thing.</li></ul><p id="6ad8">To recap, we just learned a technique of modeling that makes our code safer, more expressive and easier to maintain. These concepts actually transcend Kotlin — I’ve mentioned the excellent <a href="https://fsharpforfunandprofit.com/series/designing-with-types/">F# for Fun and Profit series</a> earlier, and you can read about essentially the same concept <a href="https://www.47deg.com/blog/functional-domain-modeling/">here</a>, <a href="https://arrow-kt.io/docs/patterns/error_handling/">here</a>, <a href="https://livebook.manning.com/book/functional-and-reactive-domain-modeling">here</a> and all over the place. You can use exactly the same principles in Java, today — it’s just a little more cumbersome to write out all those classes.</p><p id="cddc">However, as always, there are tradeoffs to using this approach to modeling, which I’ll be discussing in the next article.</p><p id="c99c">Go back to <a href="https://readmedium.com/sealed-hierarchies-strongly-typed-illegal-states-3d3c50c9a77b">Modeling Illegal States</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/modeling-states-and-structure-considerations-7d2bd16ccf98">Modeling States and Structure: Considerations</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 States and Structure

The benefits of using types to represent the states and shapes of your entities, and how it makes your code safer, more expressive and easier to maintain.

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

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.

In the previous chapter, we discussed how sealed hierarchies could be used to model illegal states (such as invalid data), thereby recovering the ability to reason locally, achieve linear code flow, prevent certain types of errors from ever happening, increase readability and maintainability, and communicate and enforce assumptions explicitly.

All this was achieved by a seemingly trivial modification — using explicit datatypes to model illegal states, instead of throwing exceptions.

This allowed us to turn this:

into this:

In this article, I’d like to take things a step further, and show you how you can do this for any states, not just illegal ones, and how, again, this can be tremendously beneficial for your code.

Let’s imagine an app that requires users to go through a verification process before they can use all of its features. A user can register with only their e-mail and password, and be restricted to basic features until they’re verified. To start the verification process, they also need to input their name and address, and apply for verification. Verification is done by a human, who does some manual checks, and if everything checks out, marks a user as verified. In other words, the user can be in three basic states: NEW, VERIFICATION_PENDING, VERIFIED.

Traditional way

Let’s take a look at how we would usually model such a user:

At first glance, this seems absolutely fine. Some of those things are nullable because they are not always present. One might even go as far as to say that it’s nice that we see immediately which ones they are, a clear improvement over Java. So what’s the problem?

Don’t worry, we’ll get there. But to do that, we have to actually use this data structure first. So let’s write some simple service logic which advances a user through the individual states, and since we already learned about representing illegal states as explicit classes from the last chapter, we’ll take care to do that as well.

Nothing fancy going on. As is customary, each method checks for various preconditions before doing its thing, and apart from using return values instead of exceptions, it probably looks pretty much like most service layers you’ve encountered.

Now take a step back and take a good hard look at what we wrote. Do you like it? Because I do not!

  • There are more lines spent checking for preconditions in each method than there is actual business logic
  • We have to keep checking for the same preconditions over and over again, and things that are obviously implied by the business context are not guaranteed. For example, if a User is in a VERIFICATION_PENDING state, he must have a name and an address, but this not guaranteed by the type. This means that even when we know that the name and address should be there, they’re still as nullable as ever, which leads to a whole bunch of checks that seem redundant, but have to be there.
  • One of the reasons why these checks have to be there is because there’s no guarantee someone won’t send in an object that breaks these assumptions — it can happen, and with this design, there’s no way to prevent this from happening.
  • When that happens, the error will only get caught at runtime, and runtime errors are our worst enemy. This can happen either as a result of an honest mistake, but also, as we’ve mentioned repeatedly, as a consequence of the fact that 10 years from now, nobody on the team will have been there when this code was written and these assumptions will long be forgotten
  • The assumptions are not communicated by the code. Doc comments don’t cut it, because they’re not enforced — there’s no way to guarantee that a doc comment is accurate, and stays accurate. On a long enough time scale, comments always lie.
  • The defensive programming techniques that this leads to, where each method needs to start with a gazillion checks that all sorts of preconditions are not broken, fill the codebase with clutter and make code difficult to read and understand. They also needlessly increase cyclomatic complexity, which in turns makes code more difficult to test (more paths through the code means more paths we need to test).
  • The further in any such workflow we are, the more preconditions we need to check, because we usually need to repeat all the checks that were already done 10 times in the previous steps + add new checks for the current step — this can be seen in the markAsX methods above. Even in simple code as this, things get unwieldy fast, and we all know that it will be much worse in reality.
  • Let’s face it — almost nobody actually does all those checks. What usually happens in the real world is that some checks are indeed made at the beginning of a method, but not all of them, and the rest are just assumed to be true, because we’re human, there’s no time, etc. Hopefully, there is a test suite to back these assumptions up, but there often simply isn’t, for the same reasons. And even when there is, as we’ve seen, a green test suit is no guarantee that there are no problems with the code base.

In other words, this whole situations is a ticking bomb of silent errors and regressions just waiting to blow up, and we can’t help but ask ourselves: can we do better?

It turns out we can.

States and Structure as Separate Types

The solution is exactly the same as in the previous chapter — represent those states using types.

Let’s take a look at everything that’s changed:

  • We no longer need a state property — the state is encoded in the type.
  • The existence or non-existence of a property, and the conditions under which it does, is explicitly communicated and enforced. You cannot create a VerifiedUser without specifying the validatedAt property. If something is nullable, it means it is not required, as opposed to “might be required at some unknown point down the road if some unknown conditions are met”.
  • In the various Result classes, we can be explicit about the fact that on success, it produces the next state (PendingUser or VerifiedUser) and on failure, it doesn’t change the state.
  • No need to check the state in prepareForVerification — guaranteed to be correct, always.
  • No need to check anything at all in markAsVerified and markAsFailed— everything is guaranteed to be there, always.
  • Cyclomatic complexity is down, which means less time writing tests — at least 7 tests are no longer needed to achieve the same coverage.
  • All assumptions are explicitly communicated and enforced. You simply cannot send a NewUser into markAsVerified, and you never will be, not today, not tomorrow, and not 10 years from now when you’re no longer around. If someone makes a mistake, the code won’t compile, and they will know exactly what needs to be fixed.

So, by replacing a single class with an interface and 3 simple classes, we prevented a whole bunch of errors from ever happening, made the code shorter (this will be especially pronounced in real world scenarios, where service code comfortably outnumbers “data” code), more concise, easier to read, understand and maintain, saved ourselves from writing several tests, made the code self-documenting, saved ourselves from having to do null-checks before we access properties and saved ourselves from having to explicitly manage the users state. What’s not to love?

Going Further

There’s even more you could do. For example, you could realize that the situation with the nullable id property is exactly the same one — the id can only be null when you’re first creating the object, but can never be null once the object is saved — in other words, you have a PersistedUser, and an UnpersistedUser.

Right off the bat, this opens up new doors for you. For example, imagine having to retrieve all the posts of a User. Naturally, that only makes sense for persisted users, since users that aren’t saved to the database can’t post. This will manifest itself in a practical issue as well, because you will have to send the id into some sort of repository that will fetch posts related to that user id, but if id is nullable, you’ll have to deal with the null variant somehow, just in case someone somewhere down the road decides to be clever and actually send in a User that wasn’t saved yet. But split the User into a Persisted and Unpersisted variant, and suddenly you’re guaranteed that this can’t happen.

Connection to Sealed Hierarchies

Interestingly enough, if you really think about it, most of the benefits stated here are actually not dependent on the concept of sealed hierarchies at all — everything would work out exactly the same if sealed interface User was just an interface User.

There are two reasons why I’m mentioning it in this context:

  • it’s a direct extension of the technique described in the previous chapter, which is dependent on sealed hierarchies
  • because even though it’s not strictly necessary to use a sealed hierarchy for the example we demonstrated, this is definitely a use-case that should be modeled using a sealed hierarchy. The set of “user states” should definitely be fixed at compile time, and should not be extensible by a 3rd party library, no more than an enum could. This is not only an issue of function, but of form as well — we’re communicating intent, specifically that this is intended to be a fixed thing.

To recap, we just learned a technique of modeling that makes our code safer, more expressive and easier to maintain. These concepts actually transcend Kotlin — I’ve mentioned the excellent F# for Fun and Profit series earlier, and you can read about essentially the same concept here, here, here and all over the place. You can use exactly the same principles in Java, today — it’s just a little more cumbersome to write out all those classes.

However, as always, there are tradeoffs to using this approach to modeling, which I’ll be discussing in the next article.

Go back to Modeling Illegal States, jump to the Table of Contents, or continue to Modeling States and Structure: Considerations.

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