1

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 Answers1

1
  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.

Selim
  • 1,064
  • 11
  • 23
  • Thanks for your detailed answer! I am interested in knowing about your arguments on separating serialisation from the domain model. Why do you think this is a good idea? – Queen of Spades Apr 18 '21 at 15:44
  • @QueenofSpades I should have included some reasons in my original answer. I've updated it and included two reasons for my opinion :-) – Selim Apr 18 '21 at 17:03
  • Let's consider the second argument. Why an importer could change? 1. Parsing the exact same data but in aa different way, let's say changing a `for` with a `loop`. For this kind of change we will only touch the companion object if it was in the `Email` class. – Queen of Spades Apr 18 '21 at 17:47
  • 2. The change is in the data itself, let's say now my files have new fields that I like to parse as well. For example, if my files has now an information about the `date` the email was sent then I will need to change both the parser and the `Email` class in both cases (whether the parser is independent or in a companion object) – Queen of Spades Apr 18 '21 at 17:49
  • I feel that I am missing something. Could you give a particular example that this kind of separation would benefit from? Thanks – Queen of Spades Apr 18 '21 at 17:50
  • For case 1, ask yourself whether you're ok with having to modify the `Email` class when you're adding YAML support in the future. Instead you could add a `YAMLImporter` while not touching the data class. What changed is not your `Email` class but the added support for YAML files. If you separate the two, your code changes will reflect your feature changes. Case 2 is unavoidable. If you add another property to `Email`, then your importers will have to change to support the new property. – Selim Apr 19 '21 at 07:53
  • Why considering touching the `Email` class as a bad thing? (other than the fact that the code change will not reflect the feature change, which is a good point) – Queen of Spades Apr 19 '21 at 09:03
  • Changing the `Email` class isn't necessarily bad but I don't think you should **have to** modify it when you add a new Importer. Keep in mind that [data classes are there to **hold data**](https://kotlinlang.org/docs/data-classes.html). You need to decide whether other code belongs in there or not. – Selim Apr 19 '21 at 11:51
  • Thanks, it's very clear. A final question, what is the benefit of using an interface (CSVImporter, YAMLImporter etc. will implement it as you did in your code) ? – Queen of Spades Apr 19 '21 at 15:56
  • If you have a common interface for CSV, YAML and JSON importers, then you can choose to hide them behind an interface. A class that wants to import `Emails` doesn't need to know what the type of importer is. It may only care about importing. See [here](https://stackoverflow.com/a/17686326/1868281) for a nice explanation. – Selim Apr 19 '21 at 16:04