View objects, as I see them, are a kind of DTOs without the burden of DTOs.
While DTOs represent and duplicate domain objects data, View objects simply represent data requested by a view and extracted from one or more domain objects.
What's more important, they cannot be used as replacements of domain objects, because they are immutable containers of simple data.
So, their use is (and must be) restricted to those scenarios where you need read only report-style views and fast performances.
What about View objects construction?
Like discussed in this post from the Domain-Driven Design group, there are different possibilities.
I think the best one is to use the Builder pattern:
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
You can use the Builder pattern for implementing build methods that directly access the data store for constructing different parts of the View object.
This gives you the following advantages:
- You can separate the process of building your View object in different steps, and use only the steps you need for a particular view page, without even having to construct the full View object.
- You can have a generic ObjectAViewBuilder with generic buildPartA() and buildPartB() steps, and then having subclasses specify what data you actually need for every given step, i.e. depending on a particular view page.
This gives you better performances and higher flexibility.
Let us go now with a simple example.
Say we have two domain objects: Employee and Office (I'm not going to show interfaces here because they are of no interest in our context).
@Entity()
@Table(name="Employee")
@Proxy(proxyClass=Employee.class)
public class EmployeeImpl implements Employee {
@Id()
private int id;
@Version()
private int version;
@Column(unique=true)
private String matriculationCode;
private String firstname;
private String surname;
@ManyToOne(targetEntity=OfficeImpl.class)
private Office office;
protected EmployeeImpl() {}
public EmployeeImpl(String matriculationCode) {
this.matriculationCode = matriculationCode;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public String getMatriculationCode() {
return matriculationCode;
}
public Office getOffice() {
return this.office;
}
public void setOffice(Office office) {
this.office = office;
}
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Employee)) return false;
Employee other = (Employee) obj;
return new EqualsBuilder().append(this.getMatriculationCode(), other.getMatriculationCode()).isEquals();
}
public int hashCode() {
return new HashCodeBuilder().append(this.getMatriculationCode()).toHashCode();
}
}
@Entity()
@Table(name="Office")
@Proxy(proxyClass=Office.class)
public class OfficeImpl implements Office {
@Id()
private int id;
@Version()
private int version;
@Column(unique=true)
private String officeId;
private String name;
protected OfficeImpl() {}
public OfficeImpl(String officeId, String name) {
this.officeId = officeId;
this.name = name;
}
public String getName() {
return name;
}
public String getOfficeId() {
return officeId;
}
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Office)) return false;
Office other = (Office) obj;
return new EqualsBuilder().append(this.getOfficeId(), other.getOfficeId()).isEquals();
}
public int hashCode() {
return new HashCodeBuilder().append(this.getOfficeId()).toHashCode();
}
}
The two domain entities are annotated with EJB3 annotations, so they are persistent objects.
Now, your application needs some report-style view listing a few employees data, but you don't want to get the full Employee object graph: so you create a marker EmployeeView interface for representing employee related views, and an extended interface for showing just some simple data from an employee and its related office, the SimpleEmployeeView (with related implementation):
public interface EmployeeView extends Serializable {
}
public interface SimpleEmployeeView extends EmployeeView {
public String getMatriculationCode();
public String getFirstname();
public String getSurname();
public String getOfficeName();
}
public class SimpleEmployeeViewImpl implements SimpleEmployeeView {
private String matriculationCode;
private String firstname;
private String surname;
private String officeName;
public String getMatriculationCode() {
return matriculationCode;
}
public void setMatriculationCode(String matriculationCode) {
this.matriculationCode = matriculationCode;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public String getOfficeName() {
return officeName;
}
public void setOfficeName(String officeName) {
this.officeName = officeName;
}
}
Please note: the view implementation has setters method, while the interface has not. This is because the view (hence its interface) has to be immutable, but setter methods must be used by the builder for constructing the View object.
Once you have some View object, you need a Builder!
So here is the generic Builder interface for constructing generic EmployeeView objects (EmployeeViewBuilder), and its concrete implementation (SimpleEmployeeViewBuilder):
public interface EmployeeViewBuilder {
public void buildEmployee(String matriculationCode);
public void buildEmployeeOffice();
public EmployeeView getEmployeeView();
}
public class SimpleEmployeeViewBuilder implements EmployeeViewBuilder {
private HibernateTemplate hibernateTemplate;
private SimpleEmployeeViewImpl view = new SimpleEmployeeViewImpl();
public void buildEmployee(String matriculationCode) {
List<Object[]> result = hibernateTemplate.find("select e.matriculationCode, e.firstname, e.surname from com.blogspot.sbtourist.domain.EmployeeImpl e where e.matriculationCode = ?", matriculationCode);
for (Object[] values : result) {
view.setMatriculationCode((String) values[0]);
view.setFirstname((String) values[1]);
view.setSurname((String) values[2]);
}
}
public void buildEmployeeOffice() {
if (view.getMatriculationCode() == null) {
throw new IllegalStateException("First call buildEmployee() method!");
}
List<String> result = hibernateTemplate.find("select e.office.name from com.blogspot.sbtourist.domain.EmployeeImpl e where e.matriculationCode = ?", view.getMatriculationCode());
for (String value : result) {
view.setOfficeName(value);
}
}
public SimpleEmployeeView getEmployeeView() {
return this.view;
}
public HibernateTemplate getHibernateTemplate() {
return hibernateTemplate;
}
public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
this.hibernateTemplate = hibernateTemplate;
}
}
As you can see, SimpleEmployeeViewBuilder builds each part of the View object directly using data access API (in our example, Hibernate API), retrieving only the data it needs.
And, if you have a different employee view page, just implement a different EmployeeView with related EmployeeViewBuilder!
Finally, your Builder objects will have to be used in the Service layer, into view specific methods:
public EmployeeView getEmployeeWithOfficeView(String employeeMatriculationCode) {
EmployeeViewBuilder builder = new SimpleEmployeeViewBuilder();
builder.buildEmployee(employeeMatriculationCode);
builder.buildEmployeeOffice();
return builder.getEmployeeView();
}
I'd like to know your opinion, every feedback is welcome.
Happy building!
Updates
07/25/2006 : Improved example code (thanks to Christofer Jennings).