Neměnitelné třídy

Dnes bych chtěl psát o jedné technice, která se mi celkem osvědčuje – o neměnitelných třídách (immutable classes). Kdo četl knihu od Joshuy Blocha Java Efektivně, pro toho to nebude žádná novinka. Pro ty, kteří ji nečetli nebo si ji nepamatují připomenu.

Neměnitelná třída je taková třída třída, jejíž instance nejde změnit. Všechny informace v ní obsažené se poskytnou při jejím vytváření a jsou pak pevně dány po celou dobu jejího života.[Bloch]

Klasickými příklady jsou String, BigInteger a BigDecimal. Zamysleme se nad tím, co nám neměnitelné třídy přináší.

Vezměme třídu Address, která obsahuje informace o ulici, číslu a PSČ. Pokud je měnitelná, má příslušné get i set metody. Prostě třída, kterých vyrobíme denně několik. Měnitelným třídám bychom měli věnovat zvláštní péči. Měli bychom řešit co se stane, když je mění několik vláken najednou. Měli bychom vytvářet defenzivní kopie, když je někomu dáváme ke zpracování. Měli bychom hlídat jestli se nám nestane například něco takového:



1     Client client1 = new Client();
2     Client client2 = new Client();
3     Address address = new Address("Petrova","15","110 00");
4     client1.setAddress(address);
5     client2.setAddress(address);
6     
7     client1.getAddress().setNumber("16");

Tady jsme nechtěně přestěhovali i druhého klienta. Při použití OR mapovacích nástrojů musíme rovněž přemýšlet, jestli se nám provedené změny zapíší do databáze automaticky nebo jestli je musíme explicitně uložit.

Zkrátka a dobře, pokud bychom chtěli programovat pořádně, měli bychom kolem měnitelných tříd spoustu práce a přemýšlení. Protože ale přemýšlíme hrozně neradi, existuje pro nás snadné řešení. Jsou to právě neměnitelné třídy. Obvykle je realizuji tak, že jim jednoduše nedám žádné set metody a všechna potřebná data nastavím v konstruktoru. Pokud navíc třída obsahuje nějaká měnitelná data, musím si ještě vytvořit defenzivní kopii při jejich vložení a při jejich vracení.



01 public class Foo {
02   private final Date date;
03   private final List<String> list;
04   
05   public Foo(Date date, List<String> list) {
06     super();
07     this.date = new Date(date.getTime());
08     this.list = new ArrayList<String>(list);
09   }
10   
11   public Date getDate() {
12     return new Date(date.getTime());
13   }
14   public List<String> getList() {
15     return Collections.unmodifiableList(list);
16   }
17   
18 }

[Updated 27.8.2007 - Peter mě v komentáři správně upozornil, že jsem u polí zapomněl final]

Pokud bych na defenzivní kopírování zapomněl, mohl by mi někdo měnit data pod rukama a třída by nebyla neměnitelná. Pan Bloch ještě navíc radí zamezit zastiňování metod, aby mi někdo nemohl udělat měnitelného potomka neměnitelné třídy. Protože ale obvykle nepíší veřejná rozhraní, tak jsem většinou toto riziko nést.

Co mi neměnitelné třídy přinesou? Zjednoduší mi život. Nemusím řešit co se stane, když je změním. Nemusím řešit synchronizaci vláken. Jejich instance mohu bez obav sdílet. Dobře se mi z nich staví další objekty. Dobře se mi píše metoda equals a hashCode. Zkrátka a dobře programování s nimi je mnohem méně náchylné k chybám.

Samosebou to není žádný všelék. Některé třídy je často nutné udělat měnitelné. Z jejich povahy prostě plyne, že je nutné je měnit. Občas se mi stane, že tak zatvrzele trvám na tom, že chci udělat třídu neměnitelnou, až si tím zkazím celý návrh.

Specifickým případem je použití MVC frameworku. Pokud chci třídu plnit daty z UI formuláře, tak ji prostě set metody dát musím. Nicméně platí následující:

Třídy by měly být neměnitelné, pokud neexistuje velmi dobrý důvod k tomu, učinit je měnitelnými [Bloch]

11 Responses to “Neměnitelné třídy”

  1. Peter Says:

    Pokial vase instancne premenne nie su deklarovane ako final, potom nemate zarucenu viditelnost vasich dat z inych threadov ako z toho, v ktorom bola instancia vytvorena. Takze ich nemozete volne zdielat medzi threadmi. V pripade nemenitelnych tried je nastastie velmi jednoduche deklarovat instancne premenne ako final.

  2. Lukáš Křečan Says:

    Dík za upozornění, už jsem to opravil.

  3. Arnost Says:

    K těm deklaracím jako "final" - možná by stálo za to trochu víc rozvést, proč se to má deklarovat jako final. Mě by napadlo, že stačí jen neumožnit změny pomocí setter metod a je to. Ale asi je tam nějaký zádrhel...

    Je tohle ten důvod, proč deklarovat proměnné final? http://java.sun.com/docs/books/jls/third_edition/html/memory.html#66562

    Co by se vlastně stalo, když bych je tak nedeklaroval?

  4. Peter Says:

    Ano presne to je ten dovod (pamatovy model a jeho garancie). V diskusii na tej like co ste poslali je dokonca aj priklad s non-final premennou. Je tam uvedene: "However, f.y is not final; the reader() method is therefore not guaranteed to see the value 4 for it." (Teda, predpoklada sa, ze writer() bezi z jedneho threadu, reader() z druheho)

    Final zarucuje pri spravnom publikovani viditelnost hodnoty z ostatnych threadov bez potreby dalsej synchronizacie.

    Ak by ste final deklaracie vynechali, tak nemate ziadnu garanciu, ze hodnotu, ktoru zapisete do tej premennej, skutocne z ostatnych threadov uvidite. Namiesto toho by tam mohla byt defaultna hodnota (0/false/null).

  5. Lukáš Křečan Says:

    Peter má pravdu, zajímavý článek o tématu je i na http://today.java.net/pub/a/today/2004/04/13/JSR133.html Otázka je, jestli k synchronizaci stejně nebudu dělat na místě, kde neměnitelný objekt dávám k dispozici ostatním vláknům. (Například při přidání do nějakého sdíleného kontextu).

  6. benzin Says:

    No v knize o navrhovych vzorech, se tyhle nemenitelne tridy jmenuji hodnotove objekty. Protoze tento pattern se ve skutecnosti nevztahuje k tride, ale k jeji instanci, proto mi i nazev "nemenitelna trida" prijde takovy zavadejici, pac ta trida zmenit jde 😀 (uz jenom protoze ani jedna metode neni final, tudiz potomek ji muze uplne celou prekopat, nebo ji prekopete skrze proxy).

  7. kolisko Says:

    Neměnitelné třídy mam rád. Pokud mam ale graf objektů složitější, přijdou mi měnitelné třídy flexibilnější.

    Například: mam třídu Známka, která drží informaci o známce ve filatelistickém albu. Známka má atributy jako rok, stát, nominální hodnotu. Tyto atributy jsou vzásadě neměnný. Pak má ale Znamka ještě atribut hodnotaNaTrhu, která se mění. Pokud má třída známka metodu setTrzniCena, změnim cenu a je vyřešeno. Pokud je třída Známka neměnná, musim vytvořit novou instanci se správnou cenou, nějak vystopovat, kdo všechno drží odkaz na starou známku a nahradit ho odkazem na novou známku.

    Bohužel většina tříd, se kterými jsem se potkal, jsou právě toho typu, že se na ně odkazuje z více než jednoho místa. Upřímě mě kromě profáknutých příkladů jako adresa a datum nenapadají žádní vhodní kandidáti. Jaké jsou vše zkušenosti?

  8. lzap Says:

    Jedna z nejlepsich knizek, jakou jsem kdy cetl.

  9. benzin Says:

    4kolisko: To je jenom otazka toho jak se navyknete pracovat. Nejak jste k tomu objektu dospel. Nejspis podle nejakych kryteriji. Ano je jednodussi si ulozit do docasne probmene primo ten obekt, ale stejne ak je mozne dohledat pokazde objekt znovu. Zvysuje to rezii, ale podle zkusenosti mych i spousty jinych programatoru, to za ten prinos stoji.

  10. Jirka Says:

    Jaky vyznam ma v tomhle pripade super(); ?

  11. Lukáš Křečan Says:

    Žádný, Eclipse mi ho tam vygenerovalo a já ho zapomněl smazat. Někdo ho tam nechává, aby bylo na první pohled zřejmé, že mu ho tam kompilátor vygeneruje, já ho ale obvykle mažu.