Question

I have a Message class that represent some data sent by a smartwatch. A Message has a header (sender, length...) and a type; it can be a location update, an alarm message... There are about thirty different types of messages.

Based on the type, the message should have a specific payload attached to it. For example, a message of type "UD" should have a payload containing latitude and longitude fields.

I created the Message class like so:

// Simplified
class Message extends ValueObject {
  serial: Serial
  length: number
  payload: Payload
}

class Payload {
  static create(type: MessageType, payload: any): Payload {
    switch (type) {
      case MessageType.UD:
        return UDPayload.create(payload)
      case MessageType.LK:
        return LKPayload.create(payload)
      ...
    }
  }
}

class UDPayload extends Payload {
  location: Location

  static create(props: any): UDPayload {
    return new UDPayload({location})
  }
}

Now, my question is: who should instantiate all these value objects? For example, UDPayload.create() should take a Location object as a parameter, should Payload.create() construct it? Because the one constructing the Payload doesn't know about the format of the actual object.

Where should the validation occur (empty fields for example)?

Finally, how to handle persistance concerns? That is, when reading the data, I basically have to instantiate all the underlying value objects before constructing the actual payload.

Was it helpful?

Solution

There a couple of ways to model this. Importantly, you need to understand that the nature of the problem at hand requires some sort of discrimination somewhere. Really, all we can do here is move this discrimination (and validation) around:

Option 1

Using a PayloadFactory object (much like you have above) where the caller invokes a static create(MessageType, any): Payload method.

This represents pushing the discrimination as low as possible (hiding it from the caller). Whether the above method includes the validation or you push the validation even further down into something like UDPayload.create(any) is not entirely relevant. That said, I would try to keep your concrete Payload objects as clean as possible (so no factory methods).

  • Pro: This is useful if you expect many callers to this method because you can reuse the discrimination

  • Con: In the process you lose information. Now the caller will not know the concrete type of the payload returned (i.e. Payload instead of UDPayload).

Option 2

We move the discrimination up to the caller and invoke factory methods like: PayloadFactory.createUDPayload(any): UDPayload and have the factory method do the validation.

This is a little better. Now the caller will know what type of payload they are working with and can act accordingly.

  • Pro: Now the caller knows the concrete type of Payload.

  • Con: It makes the process less portable because the discrimination is not reusable. I know what you are thinking, "What if I just wrap the discrimination in something like Message.create(...) to make it reusable?". You then run into the same problem as option 1 where you have a Message but you don't know the concrete type of the Payload.

Option 3

Instead of handling a Message holding any Payload using a single endpoint, we instead move the discrimination to the caller of the caller (which is the sender - so the watch) and move handling to many endpoints.

The watch knows what kind of message it's about to send no? The discrimination has already been done for you. Each endpoint can simply focus on validation and handling.

  • Pro: Caller knows the concrete type of Payload. Also maximizes portability because each endpoint could simply invoke the appropriate factory method (e.g. PayloadFactory.createUDPayload(any): UDPayload).

  • Con: Not possible unless you control the watch. Requires adding a lot of endpoints/handlers.

Now we just have to ask ourselves some questions. First, what is the utility of the Payload base-class (and as a corollary the Message base-class)? Is it useful to know you have a Payload but not the concrete type? If so, go with option 1 and consider removing the concrete Payload types altogether. If this endpoint doesn't care what kind of Payload the Message is carrying, that is it is not going to be discriminated somewhere deeper in your call stack, don't bother with creating all of the extra noise.

If you handler does care about the concrete type then we should consider 2 or 3. They are about equal in my book. Option 2 means moving the switch up to your handler (so now the discrimination cannot be reused), while option 3 requires adding a whole slew of endpoints (and is also likely not possible).

Licensed under: CC-BY-SA with attribution
scroll top