Generally domain objects will be created as dumb data carriers with setters/geters without having any logic. But this will cause huge problem in long run.
If you build the domain objects with dumb setters and getters we will end up in writing null checks all over the places.
I bet many of us have seen the code snippets like:
User user = ....; if(user!=null) { String email = user.getEmail(); if(email != null && StringUtils.trimToNull(email) != null) { emailService.sendEmail(....); } else { throw new Exception("Email should not be null/blank"); } }
Here email address of User object should not be null at all(It could be a not null property in database).
But with dumb domain objects with only setters/getters we will end up writing code to check for nulls as mentioned above.
We can get rid of this null checks in all over the places we can use Builder pattern.
Assume we need to write a domain Object User with properties id, firstname, lastname, email, dob, phone.
Among them id, firstname, lastname, email properties are mandatory and should not be null or blank.
In this case we can write the User class using Builder pattern as follows:
package com.sivalabs.core.model; import java.util.Date; /** * @author Siva * */ public class User { private Integer id; private String firstname; private String lastname; private String email; private Date dob; private String phone; private User() { } private User(Integer id, String firstname, String lastname, String email) { this.id = id; this.firstname = firstname; this.lastname = lastname; this.email = email; } public static final User build(Integer id, String firstname, String lastname, String email) { if(id == null || id < 0){ throw new IllegalArgumentException("Id should not be null or negetive."); } if(firstname == null || firstname.trim().length()==0){ throw new IllegalArgumentException("firstname should not be null or blank."); } if(lastname == null || lastname.trim().length()==0){ throw new IllegalArgumentException("lastname should not be null or blank."); } if(email == null || email.trim().length()==0){ throw new IllegalArgumentException("email should not be null or blank."); } if(!email.contains("@")){ throw new IllegalArgumentException("Invalid email address."); } return new User(id,firstname, lastname, email); } public Integer getId() { return id; } public String getFirstname() { return firstname; } public String getLastname() { return lastname; } public String getEmail() { return email; } public Date getDob() { return new Date(dob.getTime()); } public User dob(Date dob) { this.dob = new Date(dob.getTime()); return this; } public String getPhone() { return phone; } public User phone(String phone) { this.phone = phone; return this; } }Following are the steps to build safe domain objects:
1. Make default constructor as private preventing others creating empty instances.
2. Create a private parametrized constructor with mandatory arguments only.
3. Provide a public static build() method taking mandatory arguments, validate them and then build the object using parametrized constructor.
4. Create setter methods (I have used Method chaining here) for optional properties.
With this procedure I need not check for nulls for the mandatory arguments becuase if I have a non-null user object means it contains valid values for mandatory properties.
Just in case, if there's a need arise to add some more parameters to the User object, then won't it be cumbersome to increase the number of parameters that are passed to the build method?
ReplyDeleteAlso, if the values are set by calling individual setter methods (ideally, shouldn't because of Builder pattern) - the validation will not happen, right!?
Hi Veeru,
DeleteRegarding calling individual setter methods, if you observe User class I din't provide setters for mandatory parameters, they can be set only through build() method.
About number of parameters, yes if there are more mandatory params the no of args to build() will be more. But i guess it is better than calling a sequence of setters which might give a chance to caller to miss some mandatory params.
-Siva
Your User object cannot be used in many of todays java ee, swing, javafx or a lot of those technologies because you don't follow the javabeans conventions. to use it anywhere, you will have to duplicate it again.
ReplyDeleteIts even made worse by the fact that the duplicate you will use for presentation purposes cannot be used for persistence in java ee contexts...
You have to rethink your design
cameo,
DeleteYou are right. Today's frameworks are all almost depends on setters/getters.
But as I explained with setters/getters we have a problem of checking the validity of object throughout the code.
In my case, these domain objects are not being instantiated by frameworks. These are fine-grained objects that will be instantiated only by me, like Money(represent a class to hold some money value as BigDecimal and provide various conversion methods), Address(a generic address class with various formatting methods) etc.
Intention is good to avoid inserting non valid user into database but as @cameo pointed out if your domain object is not compliant with java beans convention you will face lot of trouble to use open source libraries, we faced lot of problem while using dispalytag in jsp with object not following bean conventions, so certainly a trade off between this two.
ReplyDeleteYou should throw the most specific exception available, for example NullPointerException if something is null. Google Guava library has nice methods for parameter checking.
ReplyDeleteWhat I do (admitting that id and username are invariant and that email is not):
ReplyDeletepublic class User {
public User() {}
public User id(String id, String username) {
}
public User set(String email) {
}
public String getId() {...}
public String getUsername() {...}
public String getMail() {...}
}
Use bean validation jsr303!
ReplyDeleteI don't think jsr303 addresses the issue the OP is trying to solve - you still need to actively call validator.validate(object) - which could easily be forgotten by developers. Building the validation in to the domain object makes non-valid objects (theorectically) impossible.
ReplyDeleteI don't think that having validation logic in DTOs are good option (not always). I strongly believe that following single responsibility principle convert into long term benefits when product goes into maintenance mode. DTOs are for data representation at any point of time in application. If there is no information for certain fields in DTO, then let it be. If you need to validate the information before using it, build a separate validator for it.
ReplyDelete