wtorek, 19 czerwca 2012

Value Object a prawidłowa implementacja


Z racji wszechobecnej mody na DDD, obiekty typu Value Object (VO) powstają jak grzyby po deszczu. Zaimplementowanie takiego obiektu wydaje się być trywialne. Jednak bazując na własnych doświadczeniach widziałem/zrobiłem wiele różnej jakości VO i dopiero teraz chyba mam pełny pogląd w tej sprawie. Przykładem niech będzie VO Email, służący do opakowania stringowej reprezentacji adresu email. Implementacja będzie bazowała na Hibernate, ale zaprezentowane rozwiazania są wspólne do większości frameworków od persystencji.
@ValueObject
@Access(AccessType.FIELD)
public class Email implements Serializable{
 private static final long serialVersionUID = -6632187560263057672L;
 @Basic
 private String value;


 /**
  * na potrzeby hibenrate prywatny konstruktor
  */
 @SuppressWarnings("unused")
 private Email() {}

 /**
  * @param email - poprawy adres email
  * @throws IllegalArgumentException jeśli adres email jest niepoprawny
  */
 public Email(String email) {
  if (!EmailValidator.getInstance().isValid(email)) {
   throw new IllegalArgumentException("podałeś nieprawidłowy format adresu email");
  }
  this.value = email;
 }

 @Override
 public boolean equals(Object obj) {
  if (obj == this)
   return true;
  if (!(obj instanceof Email))
   return true;
  Email email = (Email) obj;
  return new EqualsBuilder().append(value, email.value).isEquals();
 }

 @Override
 public int hashCode() {
  return new HashCodeBuilder().append(value).toHashCode();
 }

 @Override
 public String toString() {
  return value;
 }

 public String getValue() {
  return value;
 }
}
Na pierwszy rzut oka nic wielce okrywczego w tym kodzie nie ma. Mimo wszystko warto opisać kilka kluczowych aspektów. Po pierwsze warto zrobić sobie adnotację „marker” do oznaczania wszystkich VO. Łatwiej je wtedy chociażby wyszukiwać w projekcie.
/**
 * stereotyp do oznacznia klass o charakterze ValueObject, 
 *  - ich identyczność bazuje na nie na Id jak w przypadku encji, tylko na ich stanie
 *  - najczęśniej powinny być niemodyfikowalne
 *  - enkapsulują pola o podobnej semantyce,
 * @author aludwiko
 */
public @interface ValueObject {}
Kolejna adnotacja @Access służy do nadpisania po czym mapowane są pola. Niezależnie od tego czy klasa bazowa np. User, mapowana jest z wykorzystaniem getterów czy bezpośrednio pól, Email zawsze będzie mapowany bezpośrednio do pola. Dzięki temu nie musimy tworzyć settera i gettera, co poprawia nam enkapsulację. W moim przypadku getter został, ale jest on po prostu przydatny.

Z racji tego, że encje przeważnie implementują interfejs Serializable, od razu dodajemy go do VO, na pewno nic złego z tego nie wyniknie.

Konstruktor domyślny (wymagany przez Hibernata) został zaimplementowany jako prywatny, aby nikogo nie kusiło tworzenie „pustych” Emaili. Publiczny konstruktor gwarantuje nam, że nigdy utworzymy „błędnego” obiektu Email, co jest jedną z cech dobrego VO, tzn jego stan nie powinien być nieprawidłowy.

Taki konstruktor i wspomniany wcześniej brak settera powodują że Email spełnia kolejną ważna cechę VO, jest niemodyfikowalny. Jest to bardzo ważna cecha w kontekście VO. Equals i hashCode dopełniają minimalną implementację.

Ok, mamy VO, pytanie co dalej z nim zrobić. Jeśli chodzi o warstwę widoku, to należy stworzyć odpowiedni Converter w JSF, PropertyEditor w starym Springu, lub jakikolwiek inny mechanizm, który z wartości inputa utworzy nam obiekt Email. Z racji tego że nie mamy settera nie bindujemy na widoku wartości bezpośrednio do value
<h:inputtext id="email" value="#{user.email.value}">
tylko do samego email'a
<h:inputtext id="email" value="#{user.email}">
Przykładowa implementacji w springu:
public class EmailPropertyEditor extends PropertyEditorSupport {

 @Override
 public void setAsText(String text) throws IllegalArgumentException {
  if (StringUtils.isNotBlank(text)) {
   Email email = new Email(text);
   setValue(email);
  }
  else {
   setValue(null);
  }
 }

 @Override
 public String getAsText() {
  Email email = (Email) getValue();
  if (email != null) {
   return email.getValue();
  }
  else {
   return "";
  }
 }
}
W tym momencie mamy już obiekt z wypełnionym Email'em, teraz warto powiedzieć Hibernatowi jak ma zapisywać taki obiekt z Email'em do bazy. Są dwa główne podejścia, albo idziemy w kierunku @Embeddable i @Embedded albo Hibernate UserType.

Pierwsze podejście jest już proste i prawdopodobnie każdy miał z nim styczność. Chociaż np. dyskusja czy każdy obiekt @Embeddable jest automatycznie VO, nadal trwa i właściwie jestem po obu stronach w tym konflikcie.

Drugie podejście, choć trochę bardziej skomplikowane, daje ciekawy efekt w postaci możliwości tworzenia bardziej obiektowych zapytań, takich jak np:
Email email = new Email("[email protected]");
Criteria criteria = session.createCriteria(User.class).add(eq("email", email));
lub jako HQL
"from User WHERE email = :someEmail"
W tym przypadku musimy napisać własną implementację typu pola w klasie, tak aby hibernate wiedział jak zapisywać Emaila. W większości wypadków wystarczy zaimplementowanie interfejsu UserType. Jednak w tym przypadku chciałbym mieć możliwość tworzenia zapytań z wykorzystaniem LIKE:
Criteria criteria = session.createCriteria(User.class).add(ilike("email.value", "op.pl"));
Dostęp do pól VO w zapytaniach jest możliwy, ale musimy zaimplementować CompositeUserType, przykład implementacji:
/**
 * implementacja UserType'a dla VO [email protected] Email}
 * @author aludwiko
 */
public class EmailCompositeType implements CompositeUserType {

 /**
  * ORDER IS IMPORTANT! it must match the order the columns are defined in the property mapping
  */
 @Override
 public String[] getPropertyNames() {
  return new String[] { "value" };
 }
 @Override
 public Type[] getPropertyTypes() {
  return new Type[] { Hibernate.STRING };
 }
 @Override
 public Object getPropertyValue(Object component, int property) throws HibernateException {
  if (component == null) {
   return null;
  }
  Email email = (Email) component;
  switch (property) {
   case 0: {
    return email.getValue();
   }
   default: {
    throw new HibernateException("Invalid property index [" + property + "] for Email");
   }
  }
 }
 @Override
 public void setPropertyValue(Object component, int property, Object value) throws HibernateException {
  throw new UnsupportedOperationException();
 }
 @Override
 public Class returnedClass() {
  return Email.class;
 }
 @Override
 public boolean equals(Object x, Object y) throws HibernateException {
  if (x == y) {
   return true;
  }
  if (x == null || y == null) {
   return false;
  }
  Email emailX = (Email) x;
  Email emailY = (Email) y;
  return emailX.equals(emailY);
 }
 @Override
 public int hashCode(Object x) throws HibernateException {
  return x.hashCode();
 }
 @Override
 public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
  String name = names[0];
  Object emailString = Hibernate.STRING.nullSafeGet(rs, name);
  if (emailString == null) {
   return null;
  }
  return new Email((String) emailString);
 }
 @Override
 public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
  if (value == null) {
   Hibernate.STRING.nullSafeSet(st, null, index);
  }
  else {
   Hibernate.STRING.nullSafeSet(st, ((Email) value).toString(), index);
  }
 }
 @Override
 public Object deepCopy(Object value) throws HibernateException {
  return value;
 }
 @Override
 public boolean isMutable() {
  return true;
 }
 @Override
 public Serializable disassemble(Object value, SessionImplementor session) throws HibernateException {
  return (Serializable) value;
 }
 @Override
 public Object assemble(Serializable cached, SessionImplementor session, Object owner) throws HibernateException {
  return cached;
 }
 @Override
 public Object replace(Object original, Object target, SessionImplementor session, Object owner) throws HibernateException {
  return original;
 }
}
I samo wykorzystanie:
@Entity
public class User {
        //...
 @Column
 @Type(type = "package.EmailCompositeType")
 public Email getEmail() {
  return email;
 }
 public void setEmail(Email email) {
  this.email = email;
 }
        //...
}
Mam nadzieję, że temat został opisany kompleksowo i da się to przeczytać w skończonym czasie. Gdyby ktoś miał coś ciekawego do dodania, to zachęcam do komentowania.

2 komentarze:

  1. Bardzo ciekawy post, niby rzeczy dosc proste, ale dowiedziałem sie paru ciekawych rzeczy :-)

    OdpowiedzUsuń
  2. Prawidłowy value object - >Wszystkie konstruktory powinny być prywatne, tworzenie obiektu tylko poprzes statyczną funkcję, zazwyyczaj of(), Klasa powinn być immutable .

    OdpowiedzUsuń