Сегодня на работе проводил маленький ликбез по поводу граблей на которые сам уже наступил... возможно кому-то пригодится.
Итак предположим у нас есть класс 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.