How to separate business logic from representation?

Viewed 169

Assuming I have the following class:

data class Email(val id:String,
    val from:String,
    val to:String,
    val cc: List<String> =listOf(),
    val bcc: List<String> = listOf(),
    val title: String?="",
    val message:String?="",
    val evaluation:Int?=0)

I have this data in different formats, json, txt, csv etc.

I want to make an importer for each format. The first option would be to add as many companion objects as data formats. The problem is that the API of the class will be cluttered with unnecessary details especially if the importers will do some pre-processing things before finally returning email objects.

In other word I want to separate the business logic from the representation.

  1. Do I really need to do it? I came from other languages and best practices may change from one language to another.

  2. If so, what is the best way to do it. I would like a link to some implementations in well-known libraries.

Thanks.

1 Answers
  1. Do I really need to do it? I came from other languages and best practices may change from one language to another.

I think this comes down to preference. IMO it can be a good idea to not include serialization related information in your domain models. The reasons being:

  • It doesn't really belong there. As you said in your question, adding all this mapping logic clutters the Email class.
  • Changing your CSV or JSON parser might force you to also change the mapping logic in your Email class, making your code more coupled.

A clean way to do this is to use extension functions. You can choose to make use of them locally or to put them inside the companion object. The latter option would still clutter your data class with mapping logic.

So if you have for example a CsvEmailImporter class, then you could write a private function that converts a line in the CSV file into an Email object:

    class CsvEmailImporter : EmailImporter {
      override fun import(source: InputStream): List<Email> {
        val emails = source.bufferedReader()
          .lineSequence()
          .drop(1) // don't include the csv header
          .map { it.toEmail() }
          .toList()
        return emails
      }

      private fun String.toEmail(): Email {
        // map a line in the CSV file to an Email object
      }
    }

Notice how in this example our mapping function is private to the CsvEmailImporter. This means we didn't have to modify the Email class itself. It also means that you can't access or see it from anywhere else.

  1. If so, what is the best way to do it. I would like a link to some implementations in well-known libraries.

I don't know if there's a "best way" here but I have found this example from a popular manga reader app. In this case there's a top-level function to convert Manga objects to MangaInfo objects. This function is accessible from the outside and is similar to putting it inside the companion object.

I would suggest trying out different approaches to see what works best for you and your codebase.

Related