Na początek napiszę, że problemu jako takiego w zasadzie nie mam, gdyż problem postawiony w temacie realizuję. Główna rzecz, o którą się tutaj zgłaszam, to porady bardziej doświadczonych, gdyż nie do końca mam przekonanie że moje rozwiązanie jest dobre. Post będzie długi, więc mam nadzieję, że komuś starczy cierpliwości, żeby to przeczytać ;)
Na start przyjmijmy, że mam klasę „Model”, która posiada pole: private long number; + setter/getter.
Potrzebuję zapisać stan obiektu tej klasy na dysku w celu późniejszego przywrócenia go w programie. W początkowej nauce Javy rozwiązaniem przeznaczonym do tego wydawała się być serializacja, jednak po zapoznaniu się z różnymi materiałami wniosek był taki, że nie powinno się jej używać do „długotrwałego” przechowywania stanu obiektów. Bardziej ma służyć jedynie do przesyłania obiektów, gdyż nie ma gwarancji, że w kolejnej aktualizacji Javy taka klasa nie zmieni swojego serialVersionUID i przestanie być możliwa do wczytania.
Dalszy tok rozumowania doprowadza mnie do zapotrzebowania na metody:
public static void save(Model model, File file);
public static Model load(File file);
Ponieważ nie chcę obsługi zapisu/odczytu trzymać bezpośrednio w modelu, zazwyczaj powstaje osobna, abstrakcyjna klasa IOModel w jakimś pakiecie io i w niej tworzę powyższe metody. W metodzie save() rozbijam całą logikę zapisywanej klasy, aż dojdę do typów prostych i je kolejno zapisuje do pliku. W tej samej kolejności następuje odczyt i odtwarzanie obiektu w metodzie load().
Chwilowo problem rozwiązany, jednak z czasem powstaje kolejny:
Podczas tworzenia programu i często także już po wydaniu pierwszych testowych wersji kod ewoluuje dalej. Jedne dane przybywają, inne są modyfikowane i trzeba wprowadzić także zmiany w zapisie/odczycie. Przyjmijmy, że klasa Model zyskała nowe pole: private String text;. Następuje utrata kompatybilności wstecznej jak w przypadku serializable (wiem, że przy dodawaniu właściwości nie ma potrzeby zmiany UID, ale upraszczam na potrzeby przykładu). Sytuacja nie do zaakceptowania, gdyż nie chcę, aby pierwsi użytkownicy testujący aplikację tracili dane.
Rozwiązanie, które stosuję, to dodanie w klasie IOModel:
private static final int VERSION = 1;
Metoda save() pierwsze, co zapisuje do pliku, to wartość VERSION, metoda load() tylko wczytuje wartość VERSION z pliku i na jej podstawie wywołuje którąś z metod prywatnych typu loadVersion1(InputObjectStream in), loadVersion2(InputObjectStream in) itd.
Za każdym razem, gdy następuje zmiana formatu danych, zwiększam wartość VERSION, aktualizuję metodę save() aby odzwierciedlała nowy stan zapisywanego obiektu, i tworzę kolejną metodę loadVersionX(), która obsługuje odczyt w danej wersji.
Zaletą rozwiązania jest to, że program potrafi wczytać wszystkie wcześniejsze wersje danych (zakładając, że nowo dodane właściwości modelu są prawidłowe w swoich wartościach domyślnych).
Aktualnie rozwiązuje to wszystkie moje problemy z przechowywaniem danych, ale...
-
Wymaga to tworzenia dosyć sporej ilości kodu dla każdej klasy, dla której potrzebuję możliwości zapisu/odczytu szczególnie, gdy klasa jest rozbudowana i posiada sporo właściwości często w postaci nie tylko typów prostych ale także kolejnych obiektów. Dlatego serializable wydawał się tak atrakcyjnym rozwiązaniem.
-
Gdy następuje zmiana wersji pliku podyktowana zmianami w modelu, kod rozrasta się w brzydki sposób. Muszę w zasadzie skopiować całą metodę loadVersionX, by utworzyć z niej metodę loadVersionX+1 z niewielkimi zmianami. Daleko temu do zasady DRY.
Pytanie końcowe.
Chodzi mi głównie o ocenę, czy takie rozwiązanie jest dobre/akceptowalne, czy może jednak zabrnąłem w złą uliczkę i ktoś oświeci mnie, jak można to zrobić dużo prościej, wygodniej i profesjonalniej.
Poniżej kod obydwu omawianych w poście klas. Obydwie klasy są już po dodaniu nowego pola do klasy Model. Komentarze w klasie IOModel pokazują, które fragmenty kodu musiałem zmodyfikować, aby przejść z VERSION 1 do 2.
Model.java:
public class Model
{
private long number;
private String text;
public Model()
{
number = System.currentTimeMillis();
text = String.valueOf(number);
}
public long getNumber()
{
return number;
}
public String getText()
{
return text;
}
public void setNumber(long value)
{
number = value;
}
public void setText(String value)
{
text = value;
}
}
IOModel.java:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class IOModel
{
private static final int VERSION = 2; //Zmiana numeru wersji na ++VERSION
public static void save(Model model, File file)
throws FileNotFoundException, IOException
{
ObjectOutputStream out = null;
try
{
out = new ObjectOutputStream(new FileOutputStream(file));
out.writeInt(VERSION);
out.writeLong(model.getNumber());
out.writeObject(model.getText()); //nowa wlasciwosc obiektu uwzgledniona w nowym formacie zapisu
//zapis zawsze wystarczy tylko w najnowszej wersji, wiec przynajmniej tu w odroznieniu od wczytywania
//nie ma potrzeby tworzyc wielu wersji zapisu
}
finally
{
if (out != null)
out.close();
}
}
public static Model load(File file)
throws FileNotFoundException, IOException
{
ObjectInputStream in = null;
try
{
in = new ObjectInputStream(new FileInputStream(file));
int fileVersion = in.readInt();
switch (fileVersion)
{
case 2:
return loadVersion2(in); //dopisany nowy blok case wywolujacy wczytywanie nowej wersji pliku
case 1:
return loadVersion1(in);
default:
throw new IOException("Unknown file version");
}
}
catch (ClassNotFoundException e)
{
throw new IOException("Unknown class type", e);
}
finally
{
if (in != null)
in.close();
}
}
private static Model loadVersion1(ObjectInputStream in)
throws FileNotFoundException, IOException
{
Model model = new Model();
model.setNumber(in.readLong());
return model;
}
//nowa metoda wczytywania danych utworzona przez skopiowanie poprzedniej i dodanie wczytywania nowych danych
private static Model loadVersion2(ObjectInputStream in)
throws ClassNotFoundException, FileNotFoundException, IOException
{
Model model = new Model();
model.setNumber(in.readLong());
model.setText((String)in.readObject()); //dopisana linia po skopiowaniu metody loadVersion1()
return model;
}
}