Here's a useful and simple way to capitalize on Java interfaces. Interfaces have myriad uses in Java; this is only one.
Imagine you've got two entities with a small subset of fields in common that you need to write validators for. You don't want to disturb the perfection of the entity classes by subclassing, but you only want to write the validators once.
Gather the accessors for those common fields in one place, an interface. In this example, those fields would be stuff related to language and country settings, currency, etc.
public interface ElementValidation { public String getCurrency(); public void setCurrency( String currency ); public String getIpaddress(); public void setIpaddress( String ipaddress ); public String getLanguage(); public void setLanguage( String language ); public String getState(); public void setState( String state ); public String getCountry(); public void setCountry( String country ); public String getZipcode(); public void setZipcode( String zipcode ); }
Then, implement this interface in the consuming classes. If any of the fields are not actually in common, the accessors can be supplied to meet the contract, they just don't do anything. And, when validating, either the validator doesn't call the accessor because it's not validating that element, or it checks the returns so as not to blow chunks.
I use the @Override annotation, not because Java actually requires it, though it is "academically" proper, but to mark visually the accessors that are involved in this scheme.
public class Address implements Serializable, ElementValidation { private String oid; private String fullname; private String street1; private String street2; private String street3; private String city; private String state; private String country; private String zipcode; private String language; public String getOid() { return this.oid; } public void setOid( ObjectId oid ) { this.oid = oid; } public String getFullname() { return this.fullname; } public void setFullname( String fullname ) { this.fullname = fullname; } public String getStreet1() { return this.street1; } public void setStreet1( String street1 ) { this.street1 = street1; } public String getStreet2() { return this.street2; } public void setStreet2( String street2 ) { this.street2 = street2; } public String getStreet3() { return this.street3; } public void setStreet3( String street3 ) { this.street3 = street3; } public String getCity() { return this.city; } public void setCity( String city ) { this.city = city; } @Override public String getState() { return this.state; } @Override public void setState( String state ) { this.state = state; } @Override public String getCountry() { return this.country; } @Override public void setCountry( String country ) { this.country = country; } @Override public String getZipcode() { return this.zipcode; } @Override public void setZipcode( String zipcode ) { this.zipcode = zipcode; } @Override public String getLanguage() { return this.language; } @Override public void setLanguage( String language ) { this.language = language; } @Override public String getCurrency() { return null; } @Override public void setCurrency( String currency ) { } @Override public String getIpaddress() { return null; } @Override public void setIpaddress( String ipaddress ) { }
public class Payment implements Serializable, ElementValidation { private String oid; private String currency; private String ipaddress; private String language; private String street1; private String street2; private String street3; private String city; private String state; private String country; private String zipcode; private Integer cardtype; private String cardholder; private String cardnumber; private String expmonth; private String expyear; private String phone; public String getOid() { return this.oid; } public void setOid( ObjectId oid ) { this.oid = oid; } @Override public String getCurrency() { return this.currency; } @Override public void setCurrency( String currency ) { } @Override public String getIpaddress() { return ipaddress; } @Override public void setIpaddress( String ipaddress ) { } @Override public String getLanguage() { return this.language; } @Override public void setLanguage( String language ) { this.language = language; } public String getStreet1() { return this.street1; } public void setStreet1( String street1 ) { this.street1 = street1; } public String getStreet2() { return this.street2; } public void setStreet2( String street2 ) { this.street2 = street2; } public String getStreet3() { return this.street3; } public void setStreet3( String street3 ) { this.street3 = street3; } public String getCity() { return this.city; } public void setCity( String city ) { this.city = city; } @Override public String getState() { return this.state; } @Override public void setState( String state ) { this.state = state; } @Override public String getCountry() { return this.country; } @Override public void setCountry( String country ) { this.country = country; } @Override public String getZipcode() { return this.zipcode; } @Override public void setZipcode( String zipcode ) { this.zipcode = zipcode; } public Integer getCardtype() { return this.cardtype; } public void setCardtype( Integer cardtype ) { this.cardtype = cardtype; } public String getCardholder() { return cardholder; } public void setCardholder( String cardname ) { this.cardholder = cardname; } public String getCardnumber() { return cardnumber; } public void setCardnumber( String cardnumber ) { this.cardnumber = cardnumber; } public String getExpmonth() { return this.expmonth; } public void setExpmonth( String expmonth ) { this.expmonth = expmonth; } public String getExpyear() { return this.expyear; } public void setExpyear( String expyear ) { this.expyear = expyear; }
Now, the validators are a simple thing to write and all in one place. Here, also, are two validation methods used that aren't part of this scheme, but work for both types of entities. These methods are called from our business logic high above entities and DTOs.
There's some other stuff going on in here such as throwing an application exception in case of failure, but this is easily observed and separate from the point of this topic. In validation, there's a distinction between something missing and something invalid as reflected here.
The Validation calls go down to the DTO and database to look up an entity if necessary to see if it's there.
public class ElementValidator { private static int inspectCurrency( ElementValidation element, AppException e ) { int err; String currency = element.getCurrency(); if( currency == null ) { // in US we default this... element.setCurrency( EntityConstants.CURRENCY_DEFAULT ); return 0; } err = Validation.validateCurrency( currency, e ); if( err == 0 ) return 0; e.addInvalidField( "currency" ); return err; } private static int inspectLanguage( ElementValidation element, AppException e ) { int err; String language = element.getLanguage(); if( language == null ) { // in US we default this... element.setLanguage( EntityConstants.LANGUAGE_DEFAULT ); return 0; } language = language.toLowerCase(); err = Validation.validateLanguage( language, e ); if( err == 0 ) { element.setLanguage( language ); return 0; } e.addInvalidField( "language" ); return err; } private static int inspectCountry( ElementValidation element, AppException e ) { int err; String country = element.getCountry(); if( country == null ) { // in US we default this... element.setCountry( EntityConstants.COUNTRY_DEFAULT ); return 0; } country = country.toUpperCase(); err = Validation.validateCountry( country, e ); if( err == 0 ) { element.setCountry( country ); return 0; } e.addInvalidField( "country" ); return err; } private static int inspectState( ElementValidation element, AppException e ) { int err; String state = element.getState(); String country = element.getCountry(); if( state == null && Validation.needsState( country ) ) { e.addMissingField( "state" ); return 1; } state = state.toUpperCase(); err = Validation.validateState( state, e ); if( err == 0 ) { element.setState( state ); return 0; } e.addInvalidField( "state" ); return err; } private static int inspectIpaddress( ElementValidation element, AppException e ) { String ipaddress = element.getIpaddress(); if( StringUtil.isEmptyOrNullLiteral( ipaddress ) ) { e.addMissingField( "ipaddress" ); return 1; } try { new DottedQuad( ipaddress ); } catch( IllegalArgumentException e1 ) { e.addInvalidField( "ipaddress" ); return 1; } return 0; } private static int inspectName( String name, String fieldname, AppException e ) { if( StringUtil.isEmptyOrNullLiteral( name ) ) { e.addMissingField( fieldname ); return 1; } return 0; } private static int inspectDate( String date, int length, String fieldname, AppException e ) { if( StringUtil.isDecimal( date ) && date.length() == length ) return 0; e.addInvalidField( fieldname ); return 1; } }