1

Below are oversimplified examples, but I wonder which route to take what is the best practice when designing API, the simple or the more complex and robust which is better?

  1. This looks good and goes along with scala ADT concepts but I fear that it would become overly complex and the clients using my API will a little hate me for overcomplicating them with too many objects to think what to pass to my interface.

    trait ProcessUpdate {
      def name: String
      def id: String
      def message: String
    }
    
    case class SuccessProcessUpdate(name: String, id: String, message: String) extends ProcessUpdate
    case class FailureProcessUpdate(name: String, id: String, message: String, e: Exception) ProcessUpdate
    
    def success(successUpdate: SuccessProcessUpdate)
    def fail(failUpdate: FailureProcessUpdate)
    
  2. This is simple, I think the ones using this API will like me better, but not sure if it's robust enough.

    trait ProcessUpdate {
      def name: String
      def id: String
      def message: String
      def e: Option[Exception]
    } 
    
    case class ProcessUpdateInfo(name: String, id: String, message: String, e: Option[Exception])
    
    def update(processUpdate: ProcessUpdate) 
    

What do you think? is there a right answer? this example was oversimplified the more complex the thing the harder the question.

Jas
  • 507
  • 3
  • 13
  • 1
    which way turned more convenient to you when you [tried using it in unit tests](https://softwareengineering.stackexchange.com/a/182329/31260)? – gnat Jul 20 '17 at 07:27
  • I tried only the (1) approach. It was ok, it wasn't too simple but also was not too complicated, it was ok, not super convenient. Then I had second thoughts maybe I should update the interface to the (2) approach but only thoughts, i didn't change it and test. So I wanted to ask if there is an already known best practice because this looks like a general question, there are these 2 options which i'm sure people already maybe come to conclusions about which way is better? – Jas Jul 20 '17 at 07:29
  • What's the purpose of the case class and functions? In (1) you have two case classes extending the trait, but are you even going to use the trait elsewhere? In (2) you're using the trait in `update`, but you only have one case class. In both examples it seems like the trait is completely unnecessary. – Samuel Jul 20 '17 at 08:38
  • 1
    @gnat I tried now unit testing for both, and it's guiding me thorough what design to choose, so I think that is the solution, simply write unit tests and see which works better and is more convenient can you post it as an answer? – Jas Jul 20 '17 at 11:58
  • 1
    correct, this is the solution. By now you probably already got an idea of how much more convenient it is compared to asking someone else to evaluate if your API is good to use. An important complementary technique is analyzing [tag:test-coverage] because it makes it easy to ensure that you didn't miss something important. Just keep in mind that 100% coverage which is frequently a goal is not your priority here - you just check whether tests cover usage of parts of API that you believe are important. (not posting as an answer here because I have already done that in linked question) – gnat Jul 20 '17 at 12:17
  • 1
    as [@gnat said] it's a duplicate of (https://softwareengineering.stackexchange.com/questions/182306/why-is-it-often-said-that-the-test-cases-need-to-be-made-before-we-start-coding/182329#182329) – Jas Jul 20 '17 at 13:24

1 Answers1

0

Without knowing any more about the purpose of your program or the use cases, I usually follow an implementation pattern for ADT like so:

sealed trait ProcessUpdate

final case class SuccessProcessUpdate(
  name: String, 
  id: String, 
  message: String
) extends ProcessUpdate

final case class FailureProcessUpdate(
  name: String, 
  id: String, 
  message: String, 
  exception: Exception
b) extends ProcessUpdate

def update(processUpdate: ProcessUpdate) = processUpdate match {
  case SuccessProcessUpdate(name, id, message) => // do something
  case FailureProcessUpdate(name, id, message, exception) => // do something else
}

You could specify fields in ProcessUpdate, but by using sealed traits, you don't really need to specify that in the trait, and each case class that extends ProcessUpdate can look completely different. You can deal with each specific implementation by using pattern matching as shown above. If you don't need as much complexity, then I would forgo the trait entirely and implement it like so:

final case class ProcessUpdate(
  name: String, 
  id: String, 
  message: String, 
  exception: Option[Exception] = None // use default to allow shorter syntax
)

def update(processUpdate: ProcessUpdate) = processUpdate.exception.fold {
  // success case
} { exception =>
  // failure case
}
Samuel
  • 9,137
  • 1
  • 25
  • 42