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&display_name=Kotlin+Playground&url=https%3A%2F%2Fpl.kotl.in%2FzYNsKVtkn&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="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&display_name=Kotlin+Playground&url=https%3A%2F%2Fpl.kotl.in%2FnpoVHw-4C&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="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&utm_medium=GabrielShanahan&utm_campaign=KotlinPrimer&utm_content=join-our-team&utm_term=KotlinPrimer#pozice">Join me in Etnetera</a></figcaption></figure></article></body>