DTOs and The Many Subclasses Problem

Last week was exciting. We got to work on our first issues in the projects we were assigned to. As I mentioned in the previous post, I worked on Link project, specifically on the server side of the project.

The week started with a sprint meeting where a number of issues (tasks) were determined and I got three tasks. The tasks were easy, but I also had to get familiar with the code base.

I had three tasks, but one of the tasks was more interesting than the rest. In this post I will be talking about this issue using a simplified version of the problem.

The Problem

We’re building a web service that provides an API for report templates building. These templates can be composed of multiple questions. For brevity’s sake, we will assume that there’s only three types of such questions. A multiple choice question, a short text answer question, and a date specification question.

I assume you’re familiar with DTOs. If not, Mohammed Abbas wrote a nice and short post introducing the concept and describing their use. Our web service allows users to specify the format of a report and retrieve it. Using a typical architecture of DTOs, you may be tempted to create different DTO for each question type. You will also need to translate these DTOs into entities which can be stored into database directly, and also translate entities into DTOs.

// Data Transfer Objects: Used to recieve and send template data from service users
// These a separate copy of these classes are created for each endpoint or controller
data class TemplateDto(val questions: List<QuestionDto>)
data class QuestionDto(val label: String, val type: String)
data class McqDto(val choices: List<McqChoice>): QuestionDto(QuestionTypes.MCQ)
data class TextQuestionDto(): QuestionDto(QuestionTypes.TEXT)
data class DateQuestionDto(): QuestionDto(QuestionTypes.DATE)

// Data model
class Template(val questions: List<Question>) { ... }
class Question(val label: String, val type: String, val id: Int) { ... }
class Mcq(val choices: List<McqChoice>): Question(QuestionTypes.MCQ) { ... }
class TextQuestion(): Question(QuestionTypes.TEXT) { ... }
class DateQuestion(): Question(QuestionTypes.DATE) { ... }

// Methods to transform from DTO to a model class and vice versa
fun templateDtoToModel(templateDto: TemplateDto): Template
fun templateModelToDto(template: Template): TemplateDto

But in doing so adding new types of questions becomes a difficult task. You may consider eliminating DTOs and operating with the entity class directly, receiving data in the entity and storing the data in the database, but I should note to you that this is mostly a bad idea. Also these DTO classes need to be repeated for each route that have a template response. That’s because each route may include a different type of response and provide more or less info and metadata.

My Solution Proposal

Solving this problem turns out to be easy if you view the data in a Question as a black box to the route controller and model classes. The model of course needs to store such classes, and the route will need to serialize and deserialize these classes. But such tasks can be delegated to another classes that are unified for all controllers. You may think that we would lose the ability to add metadata or additional info to these Question “classes”, but we don’t, because through composition we still can achieve this, and let me show you how.

First we begin with the Question “classes”. These aren’t classes in the object oriented way but POOs (Plain Old Objects), they just hold data like a C data structure.

data class Question(val label: String, val type: String)
data class Mcq(val choices: List<McqChoice>): Question(QuestionTypes.MCQ)
data class TextQuestion(): Question(QuestionTypes.TEXT)
data class DateQuestion(): Question(QuestionTypes.DATE)

For sure, these just look like the DTOs, but DTOs can hold other info inside them if we wish, and those POOs hold only the data related to the Questions. For example, DTOs can hold an id field used for updating the template questions.

Back to our DTO classes, this time we will add an id field. The controller should update the Questions listed with the DTO

data class TemplateDto(val questions: List<QuestionDto>)
data class QuestionDto(val question: Question, val id: Int)

Notice how the QuestionDto holds a Question object reference. This way we can add more fields to these Questions without having to write the whole classes again.

Our model classes will do something similar.

class TemplateModel(val questions: List<QuestionModel>) { ... }
class QuestionModel(val question: Question, val id: Int) { ... }

At last, all that’s left are two (or more, it’s up to you) classes that are aware of these Question classes and how to serialize, deserialize, and store them in DB. There are many approaches to do this. Personally, I would be tempted to implement the visitor pattern on them, but I’m not sure yet of it being a good idea. Modeling this in DB requires having a table for Questions base data (not the QuestionModel) and another for the QuestionModel’s additional data.

And as always, thanks for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *