0

I have an Order entity:

public class Order {
  private String name;
  private Status status;//enum
  //other fields, getters,setters
 }

Status is the enum with a lot of values(let's imagine 30 statuses). I created OrderDto and OrderMapper. I want to convert my order to OrderDto. What should I do with the Status enum?

Should I create the StatusDto enum and Status converter/mapper or use the enum from domain Order or return Status.FOO.name() as String?

   public class OrderDto {
      private String name;
      private ??? status;//enum or String or another(dto) enum?
    }
Pavel Petrashov
  • 1,073
  • 1
  • 15
  • 34

2 Answers2

2

If you use the Status enum in your OrderDto you couple it with your enitity. As a result you can easily change the API to clients if you make changes your entity layer.

Let's assume the OrderDto is serialized using java object serialization to send it to a client. If the client deserializes it it needs the classes of the entity layer. This is usually not what you want.

If you serialize the OrderDto via JSON, then the name of the enum is usually used. This means that if you make a change to the Status enum in the entity layer. It immediately changes the JSON that is send to the client.

I would decouple it with another StatusDto enum and map it in the transport layer (where the DTO resides) using a Map. This would also make sense if the entity Status is more fine-grained as the StatusDto, E.g.

 public class StatusMapper {

     private Map<Status, StatusDto> toDto = new HashMap<>();

     public StatusMapper(){
         toDto.put(Status.PLACED, StatusDto.IN_PROCESS);
         toDto.put(Status.CONFIRMED, StatusDto.IN_PROCESS);
         toDto.put(Status.SHIPPED, StatusDto.IN_DELIVERY);
     }

     public StatusDto map(Status status){
        StatusDto dto = toDto.get(status)
        if(dto == null){
           // default status or exception ?
        }
        return dto;
     }
 }

This mapping is easy to test. When using a switch instead of a Map it's hard to test the default case.

EDIT

That's a lot of overhead compared to a simple switch statement. Why should the default be hard to test?

First I want to say that the following example is based on Java. Maybe there are languages that don't have that problem.

Let's assume you have defined the following enum:

public enum Status {
    S1, S2, S3;
}

Often when you implement a mapping you have a one to one relationship between the source and the target values. Thus a mapping using a switch would look like this:

public String map(Status status) {
   switch(status) {
     case S1: return "State1";
     case S2: return "State2";
     case S3: return "State3";
     default: throw new IllegalArgumentException("Unknown status");
   }
}

Now try to write a test that covers the default case.

  • You can't pass a Status enum that will execute the default case, because all enum values are covered by their cases. It is the nature of the default case that it is only executed when a new enum is added. But you can not add a new enum just for testing purposes. Subclassing is not allowed. An Enum is final. The default case is often added as a hint for a developer to remember that there is some work to do if a new enum is added.

  • You can't pass null since it leads to a NullPointerException (in Java), because the switch uses the ordinal value of the enum. You can see this in the bytecode. Status.ordinal() is used in switch statements

With the Map approach I can pass null. Since the map has no mapping null is returned and thus the 'default' is covered. Sure it is not exactly the same as if an unknown enum would be passed, but for me it is the best compromise to test it.

René Link
  • 48,224
  • 13
  • 108
  • 140
  • Great example! Thank you! I don't understand why I didn't think of it (map using) – Pavel Petrashov Mar 10 '21 at 14:26
  • That's a lot of overhead compared to a simple `switch` statement. Why should the default be hard to test? – Benjamin M Mar 10 '21 at 15:02
  • @BenjaminM if you map all values with a switch statement the default case usually covers the case when a new unknown enum value is introduced. But how can you test it until it is introduced. I agree that it is a bit more code. – René Link Mar 10 '21 at 19:58
  • I don't understand. Your `map` method does exactly the same as `switch` case. If you ask for an enum that you didn't cover you enter `if(dto == null)` and can handle it. When using `switch` you enter `default` and can handle it. Can you provide an example that shows the difference / benefits? – Benjamin M Mar 10 '21 at 20:09
0

The usual answer: It depends.

You can use the name() or toString() method of your Enum.

You can provide an additional "translation" inside the Enum like:

public enum Status {
    S1("translation1"),
    S2("translation2"),
    S3("translation3");

    private final String translation;       

    private Status(String t) {
        translation = t;
    }

    ...

}

and then use the "translation" value you provided there inside your mapper.

Or you can use an external class with a switch statement that does the job:

public static String translate(Status status) {
  switch(status) {
  case S1: return "foo";
  case S2: return "bar";
  default: throw IllegalArgumentException();
  }
}

You could even put this method inside your Enum if you like, but then it's kinda coupled again.

The more flexibility and decoupling you want, the more code you have to write. But you can also start small (for example using name()) and later on convert your code to use switch if you need it.

Pros/cons:

  • name() and toString() don't allow much flexibility, because you're restricted to the characters Java allows for Enums.

  • providing a translation using the Enum constructor lets you provide any string you like

  • using an external switch provides decoupling and most flexibility: You can for example have multiple different mappers for the same Enum, or you can restrict the mapping by throwing an Exception for certain values, etc.

Benjamin M
  • 23,599
  • 32
  • 121
  • 201