21 июл. 2010 г.

пару слов о наследниках и чтении из БД

Сегодня на работе проводил маленький ликбез по поводу граблей на которые сам уже наступил... возможно кому-то пригодится. Итак предположим у нас есть класс C1:
public class C1{
  public String field1 = "default1";
  public String field2 = "default2";
  public String field3 = "default3";
}
Предположим нам нужно данный класс заполнить из БД или еще из каких либо внешних источников, обычно это выносят в отдельный метод (что логично), итого получаем некий метод, подобный этому (данный пример я часто встречал как в литературе, так и на сайтах в "солюшенах"):
public C1 getItemFromResultSet(ResultSet rs) throws Exception{
  C1 item = new C1();
  item.field1="from_db_1";
  item.field2="from_db_2";
  item.field3="from_db_3";
  return item;
}
соответственно при использовании у нас получается что-то вроде такого:
public C1[] getAll() throws Exception{
  /* 
   * "select * from table_name order by field1"
   */
  ResultSet rs = null; //null что б обвязку не писать
  rs.last();
  C1[] items = new C1[rs.getRow()];
  rs.beforeFirst();
  while (rs.next()){
    items[rs.getRow()-1]=getItemFromResultSet(rs);
  }
  return items;
}
public C1 getByID(long id) throws Exception{
  /*
   * "select * from table_name where id='"+id+"'"
   */
  ResultSet rs = null;
  if (!rs.next()){
    throw new Exception("Запись не найдена");
  }
  return getItemFromResultSet(rs);
}
Вроде бы все хорошо и все замечательно, но предположим что у нас появляется некий наследник от C1 у которого есть несколько доп. полей. Скажем такой:
public class C2 extends C1{
  public String name2 = "default 2";
}
И теперь скажем нам нужен метод, который вернет по номеру C2 класс, но в нем всего-лишь навсего нужно добавит 1 доп. поле. Первое что приходит на ум написать:
public C2 getByIDEx(long id) throws Exception{
  /*
   * "select *, 
   * (select count(*) from table2) as cnt 
   * from table_name where id='"+id+"'"
   */
  ResultSet rs = null;
//if (!rs.next()){
//  throw new Exception("Запись не найдена");
//}
  C2 item = (C2)getItemFromResultSet(rs);
  //item.name2 = rs.getString("cnt");
  item.name2 = "from_db_cnt";
  return item;
}
Но на практике при попытке такое вызвать мы получим:
Exception in thread "main" java.lang.ClassCastException: ua.lg.moon.Start$C1 cannot be cast to ua.lg.moon.Start$C2
    at ua.lg.moon.Start.getByIDEx(Start.java:54)
    at ua.lg.moon.Start.<init>(Start.java:60)
    at ua.lg.moon.Start.main(Start.java:63)
Текст ошибки нам явно сообщает о том что у нас есть элемент класса C1, а мы его пытаемся преобразовать в класс C2. Что в принципе логично. Т.е. сама JVM не дает нам совершить ошибку. Какие варианты в такой ситуации? Первое - создать еще один метод, который будет полной копией getItemFromResultSet, но при этом будет возвращать C2 элемент, но при таком подходе, при появлении C3 у нас еще один метод появится, затем еще один и т.д. и т.п. Логичнее все-таки подумать... следующее решение:
public void copyFromResultSet(ResultSet rs, C1 item) throws Exception{
  item.field1="from_db_1";
  item.field2="from_db_2";
  item.field3="from_db_3";
}
В данном случае, мы уже не привязаны к конкретному классу, т.к. мы его не создаем. Единственно что нам нужно, это что бы в качестве параметра item у нас был любой наследник C1 или соответственно сам элемент C1. Но на практике у нас уже есть множество мест, где используется предыдущий вариант. Может есть смысл как-то изменить старый вариант с возможностью использования наследников? В моем случае получился такой вариант:
public C1 getItemFromResultSet(ResultSet rs, C1 item) throws Exception{
  item.field1="from_db_1";
  item.field2="from_db_2";
  item.field3="from_db_3";
  return item;
}
public C1 getItemFromResultSet(ResultSet rs) throws Exception{
  return getItemFromResultSet(rs, new C1());
}
в данном случае у нас в старом использовании все останется как и было, нам не нужно будет переписывать тонны кода, но для решения с классом C2 нам достаточно написать такой метод:
public C2 getByIDEx(long id) throws Exception{
  /*
   * "select *, 
   * (select count(*) from table2) as cnt 
   * from table_name where id='"+id+"'"
   */
  ResultSet rs = null;
//if (!rs.next()){
//  throw new Exception("Запись не найдена");
//}
  C2 item = getItemFromResultSet(rs, new C2());
  //item.name2 = rs.getString("cnt");
  item.name2 = "from_db_cnt";
  return item;
}
теперь при появлении наследников C3 или любого другого, нам достаточно только писать методы аналогичные getByIDEx в которых для заполнения полей класса C1 мы используем один и тот-же метод, а затем "дополняем" его отличающимися данным. И как следствие при появлении нового поля в родительском классе C1 нам новое поле достаточно прописать только в методе getItemFromResultSet.

Комментариев нет: