Java – Is it anti-pattern to have inheritence in a dto

datadtoinheritancejava

Are data transfer objects or POJOs meant to be final or can they be extended and create hierarchies for them?
It is not clear to me if such a value class is properly designed only as a final class and what are some good examples/designs of a hierarchy of DTO classes?

Best Answer

Update: Rewrote code in Java since this question is tagged java but this applies to any object oriented language.


If using inheritance is a means of reducing code duplication, I'm a little reluctant to have DTOs inherit from anything at all. Inheritance implies an is-a relationship between child class and the parent.

Using audit fields as an example, would you say the following statement is true?

Person is a BaseAuditFields.

It sounds kind of funny to me when I read it out loud. Inheritance in DTOs becomes problematic, because you introduce coupling between classes where each class usually represents a concrete idea or use case. DTOs sharing a common parent class implies the class hierarchy will evolve at the same time, and for the same reasons. This makes these classes harder to change without having a ripple effect cascading to classes where the new fields might not be applicable.

You'll notice this happen more often for DTOs that serialize and deserialize data from sources you have no control over, like a web service owned by another company or team.

That being said, I've found cases where processing a DTO can be abstracted away. Audit fields are a good example. In these cases I've found interfaces to be a much more robust solution than a concrete parent class, for the simple fact a DTO can inherit from multiple interfaces. Even then I keep the interface as tightly focused as I can and only define the minimal methods to retrieve or modify a small subset of fields:

public interface Auditable {
    int getCreatorId();
    void setCreatorId(int creatorId);
    Integer getUpdaterId();
    void setUpdaterId(Integer updaterId);
}

public class Person implements Auditable {
    ...
}

Now if we go back to our is-a test:

Person is an Auditable

The statement makes more sense. With the use of interfaces it becomes easier to utilize polymorphism in the classes that process DTOs. Now you are free to add and remove interfaces as you see fit to aid in processing the DTOs without affecting the serialization and deserialization to and from their source format. Because interfaces are being used you can modify the classes to reflect structural changes in the data, and have additional methods that make the DTO more palatable to the classes processing them.

Going back to audit fields, you have one web service call a property creatorId and another one calls it createUserId. If both DTOs inherit from the same interface, then processing both of them becomes easier.

Let's say you have a sales staff web service. By coincidence the property names in the XML response match the Auditable interface, so nothing extra is needed in this class:

public class SalesStaffWebServicePerson implements Auditable {
    private int creatorId;
    private Date createDate;
    private Integer updaterId;
    private Date updatedDate;

    public int getCreatorId() {
        return creatorId;
    }

    public void setCreatorId(int creatorId) {
        this.creatorId = creatorId;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public Integer getUpdaterId() {
        return updaterId;
    }

    public void setUpdaterId(Integer updaterId) {
        this.updaterId = updaterId;
    }

    public Date getUpdatedDate() {
        return updatedDate;
    }

    public void setUpdatedDate(Date updatedDate) {
        this.updatedDate = updatedDate;
    }
}

The customer web service returns a JSON response and two fields are named differently than what is defined in the Auditable interface, so we add a couple of getters and setters to this DTO in order to make it compatible:

public class CustomerWebServicePerson implements Auditable {
    private int createUserId;
    private Date createDate;
    private Integer updateUserId;
    private Date updatedDate;

    // Getters/setters mapped over from customer web service JSON response

    public int getCreateUserId() {
        return createUserId;
    }

    public void setCreateUserId(int createUserId) {
        this.createUserId = createUserId;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public Integer getUpdateUserId() {
        return updateUserId;
    }

    public void setUpdateUserId(Integer updateUserId) {
        this.updateUserId = updateUserId;
    }

    public Date getUpdatedDate() {
        return updatedDate;
    }

    public void setUpdatedDate(Date updatedDate) {
        this.updatedDate = updatedDate;
    }

    // Getters/setters to support Auditable interface

    public int getCreatorId() {
        return createUserId;
    }

    public void setCreatorId(int createUserId) {
        this.createUserId = createUserId;
    }

    public Integer getUpdaterId() {
        return updateUserId;
    }

    public void setUpdaterId(Integer updateUserId) {
        this.updateUserId = updateUserId;
    }
}

Sure there might be duplicated getters and setters, but behind the scenes they are working with the same fields so everyone is happy.