Skip to main content

Simplicity and Adapatability in interface design

How do you like your model and api? detailed, with separate classes or more uniform and lightweight with less classes, but with heavier objects, where some of the properties are optioal? Well, there is a delicate line in interface design between simplicity and adaptability, and it is up to you to decide also on what is a viable design - just enough design to satisfy your needs but keeping in mind you might need to extend it in future. If that wasn't clear or something let's step up for an example and see what I'm talking about. This is an open discussion, I like to hear thoughts and refine and tune my thinking, so I hope you have much to say about it. We are using scala language for compactness of example but this does not mean it's useful only for scala it's just an example.
Step 1 simple single data class
We start by exploring a simple design, we have some process and we want to report on it's progress, therefore our domain model will communicate that we are having a process together with it's relevant properties. We are following Domain Driven Design concept - the bounded context. In this first step we are going to create a single case class which would hold our properties for the process update.
case class ProcessUpdate(id: String, name: String, message: String, status: String) // This an update on proess with an id, yes we used string to report the status.
Step 2 extract interface
This is very limited, what we we want to extend that process update, what if we have more kind of process updates? let's start extending it by adding an interfae, which is in anyway a good practice.
trait ProcessUpdate {
  def name: String
  def id: String
  def message: String

case class ProcessUpdate(id: String, name: String, message: String, status: String)
Step 3 add support for errors
Now what if our client want's to update us about an error how would he do that, currently our interface no our case class supports, that, we have a few choices to make here, let's go with the naive solution, let's add support for reporting the exception (we are not using Failure because we utilize scala for the sake of shortness not it's specific classes)
so we add naively the error to the interfae and to the case class:
sealed trait ProcessUpdate {
  def name: String
  def id: String
  def message: String
  def error: Option[Exception] // We are exploring where we should add the error should we add it to one single interface and case class?

case class ProcessUpdate(id: String, name: String, message: String, status: String, error: Option[Exception]) extends ProcessUpdate

def reportProcessStatus(processUpdate: ProcessUpdate)
So now the user can provide the Exception if he had one or None in case he does not have.
Is that good?
Well, imagine we have more such cases, of optional fields, we are going to pile them up on our poor interface morever the client who uses our case class, would need to think for many params what should he pass, and whether it's ok or not that he passes None. This leads to a poor interface and mixed up users of our api.
Precaution: Note that although we try to stray away from this way of extending our domain, I will never tell you that this is 100% incorrect we should always consider each case on it's own.
Step 4 domain model on ADT
In this case we say: we have a general ProcessUpdate interface and we could utilize it in multiple ways, one is success and one is error, and only in the error case we would need to pass the exception. Now that comes with an additional case class we need to provide and additional mental stress on the client who needs to remember he needs to use one case class in one case and another in another case but we are reducing his mental stress by specifically specifying each such modeled class in our api, so he would know exactly what to pass.
sealed trait ProcessUpdate {
  def name: String
  def id: String
  def message: String

case class SuccessProcessUpdate(id: ExecutionIname: String, id: String, message: String) extends ProcessUpdate
case class FailureProcessUpdate(name: String, id: String, message: String, e: Exception) extends ProcessUpdate

def reportProcessSuccess(successUpdate: SuccessProcessUpdate) // success? pass success data.
def reportProcessFailed(failureProcessUpdate: FailureProcessUpdate) // it's now clear in fail we pass fail.
That't better. Now if we have a method to allow the user to report an error we would ask him to provide a FailureProcessUpdate and in this case we have no optional exception.
The question
The questionis ofcourse where do you put an end to this.  You might find yourself with an explosion of case classes for each case (here we just handled the error case).  The answer to that would be that you need to consider your options meaning, if you have an explosion, think about your design maybe those are different domain objects and traits alltogether and you should be combining them..
The key

Now I kept this to the end but this should be written at the beginning, I just didn't want to tell you the "secret" right from the start. The key to successfull design it to try the design yourself.  So it's almost a magic bullet, if you think which design should I use, like in our example, just write unit tests as first clients to both designs, and while you write your unit tests, you will see that one simply makes more sense! choose that design!! :)

If you want to get deeper on this topic and other design decisions there is an absolutely amazing book named Scala Design Patterns this is one of the best books I have found on scala, mainly taking many concepts and specifically discussing how we should approach them. In addition we are just touching upon this concept but I thought it's important to raise any design decision up, as trivial as it may be. Sometimes we just takes things for granted, I strongly promote we should always consider the different option and choose the right one consciously.


Popular posts from this blog

Dev OnCall Patterns

Introduction Being On-Call is not easy. So does writing software. Being On-Call is not just a magic solution, anyone who has been On-Call can tell you that, it's a stressful, you could be woken up at the middle of the night, and be undress stress, there are way's to mitigate that. White having software developers as On-Calls has its benefits, in order to preserve the benefits you should take special measurements in order to mitigate the stress and lack of sleep missing work-life balance that comes along with it. Many software developers can tell you that even if they were not being contacted the thought of being available 24/7 had its toll on them. But on the contrary a software developer who is an On-Call's gains many insights into troubleshooting, responsibility and deeper understanding of the code that he and his peers wrote. Being an On-Call all has become a natural part of software development. Please note I do not call software development software engineering b

Containers - Quick Low Level Guide

Containers Kernel, namespace, cgroups Kernel space and user space Before we actually get to explain containers let's define what is a kernel.  Because you know there is no such thing in reality as a kernel it's only how we name things, and different people name things differently. cgroups, namespaces, UFS We are going to discuss containers, cgroups, namespace, UFS, hypervisor, user space, kernetl space and more.   When we say "kernel" we mean this.  We have the hardware, this is not the kernel, now above the hardware we have a few layers of software, imagine now two boxes. User mode is all the application you run while the kernel is the lower level is all the virtual memory management scheduling, connection to hardware devices, network drivers, it's basically the abstraction on top of the hardware + the basic services which allow this. One box is closer to the hardware and contains a few layers, the second box sits on top of the kernel box and contains

Recursion Trees Primer

Recursion trees. Controlling the fundamentals stands at the cornerstone of controlling a topic.  In our case in order to be a good developer its not enough or even not at all important to control the latest Java/JavaScript/big data technology but what's really important is the basics.  And the basics in computer science are maths, stats, algorithms and computer structure. Steve wosniak the co-founder of apple said the same, what gave him his relative advantage was his deep understanding of programming and computer structure, this is what gave him the ability to create computer's which are less costly than the competitors (not that there were many) and by the way there were 3 founders to apple company one responsible for the technical side, one for the product and sales (Steve Jobs) and the third responsible for the company structure and growth, each of the three extremely important, it was not only the two Steve's but that's a topic for another episode. And with t