Generics, enum ja periyttäminen käyttöliittymän ohjelmoinnissa Osa 1
Kirjoittanut Hannu Julkaistu 2.3.2009
Käyttöliittymän koodimäärä kasvaa herkästi suureksi ja kätevillä graafisen käyttöliittymän kehitystyökaluilla tulee helposti tehtyä moneen kertaan sellaista minkä uudelleenkäyttö olisi itsestään selvää "käsin" kirjoitetussa koodissa. Tämä vinkki esittelee toiston välttöä Java Swing taulukkomodelleissa, mutta samoja tekniikoita voi soveltaa muihinkin komponentteihin.
Jos generics ei ole ennestään tuttu, voit lukea niistä tästä vinkistä
Swing taulukkokomponentti JTable ottaa näyttämänsä tiedot ja taulukon rakenteen TableModel luokan oliolta. Kun tarvitaan taulukko esimerkiksi näyttämään listaus asiakkaista, niin TableModel voitaisiin toteuttaa näyttämään asiakkaan nimi ja asiakasnumero sarakkeet ja antamaan vastaavat tiedot listasta asiakasolioita.
Silloin kun listattavana on vain yhden tyypin oliota TableModellin voi surutta toteuttaa sellaisenaan, mutta kun tarvitaan taulukkoja listaamaan monia saman rajapinnan toteuttavia oliota kannattaa harkita geneeristä toteutusta. Otetaan esimerkiksi ohjelma, joka tarvitsee taulukkoja monista erityyppisistä varaosista, jotka toteuttavat saman rajapinnan antamaan osan nimi, varastokoodi ja hinta.
Yksi tapa välttää tekemästä yksilöllisiä toteutuksia kaikille osatyypeille olisi toteuttaa TableModel joka käsittelee kaikille osille yhteisen yläluokan olioita, mutta silloin TableModellin käyttö vaatisi tarpeettoman paljon tyyppimuutoksia (cast) ja olisi muutenkin hankalaa. Genericsiä käyttäen saat huomattavasti kätevämmin käytettävän toteutuksen.
Genericsiä käyttäen TableModel toteutetaan käyttämällä käsiteltävän tyypin nimen tilalla omavalintaista nimeä ja kertomalla minkä rajapinnan TableModel voi olettaa tyyppinä käytettävien olioiden toteuttavan. Alla olevassa esimerkissä tyypin nimenä on TYYPPI ja TYYPPI:nä käytettäviltä oliolta vaadittava rajapinta on MyytavaOsa.
public class GeneerinenOsaTableModel<TYYPPI extends MyyTavaOsa> extends AbstractTableModel{ List<TYYPPI> osat = new ArrayList<TYYPPI>(); public TYYPPI annaOsaRivilta(int riviNumero){ return osat.get(riviNumero); } public void int annaVarastokoodiOsalleRivilla(int riviNumero){ return osat.get(riviNumero).varastokoodi(); } //muut metodit jätetty pois }
GeneerinenOsaTableModel luokkaa voidaan nyt käyttää mille tahansa taulukolle, jossa halutaan listata MyytavaOsa luokasta periytettyjä oliota.
GeneerinenOsaTableModel autonOsaModel = new GeneerinenOsaTableModel<AutonOsa>(); GeneerinenOsaTableModel tietokoneenOsaModel = new GeneerinenOsaTableModel<TietokoneenOsa>();
Geneerisen toteutuksen voi tietysti periyttää jos erityyppisten osien listaukselle halutaan jotakin toisistaan poikkeavaa toimintaa. Esimerkiksi alla oleva AutonOsaModel antaakin varastokoodina merkkikohtaisen koodin, jonka palauttavaa metodia ei muilla osatyypeillä tarvitse olla.
public class AutonOsaModel extends GeneerinenOsaTableModel<AutonOsa>{ //GeneerinenOsaTableModel toiminnan lisäksi tai sijasta //tarvittavan toiminan toteutus @Override public void int annaVarastokoodiOsalleRivilla (int riviNumero){ return osat.get(riviNumero).merkkikohtainenKoodi(); } }
Geneeristä toteutusta ja periytystä voi vastustaa se että aliluluokkien toiminnassa tarvitaan sellaisia muutoksia joiden tekemiseen metodien uudelleen määrittely (@Override) on riittämätön tai vaatii rasittavan määrän koodia. Tämän ongelman ratkaisuun auttaa yläluokan toteutus ajatuksella ja enum.
Esimerkkinä käytetystä osaluettelosta voisi joissakin tilanteissa olla hyödyllistä piilottaa käyttötilanteessa tarpeettomia sarakkeita. Java Swingin taulukoista kuitenkin puuttuu valmis toiminto jolla tämän saisi aikaan (saattaa olla mukana uusimmassa versiossa). Sarakkeiden piilottamislogiikan toteuttaminen tulee herkästi turhan hankalaksi koska JTable käsittelee TableModel:lia rivi ja sarakenumeroiden kautta.
Tässä tapauksessa Javan monikäyttöinen enum tulee hyvään tarpeeseen. Javassa enum ei ole pelkästään tyyppi joka voi saada vain luetellut arvot, vaan luetellut arvoit voivat sisältää arvolle kuuluvaa tietoa. Alla on määritelty enum tyyppi joka kuvaa varaosa taulukon sarakkeita. Enumille annettu rakentaja ja muuttuja mahdollistavat sarakkeen nimen sisällyttämisen sen enum arvoon. Vastaavaa ei voi tehdä esimerkiksi C++ kielen enum tyypillä.
public enum OsaTaulukkoSarake{ OSAN_NIMI("Nimi"), VARASTOKOODI("Koodi"), HINTA("Hinta"); private String sarakeNimi; OsaTaulukkoSarake(String sarakeNimi){ this.sarakeNimi=sarakeNimi; } public String annaSarakeNimi(){ return sakeNimi; } }
Oikeassa käytössä sarakkeen nimiä ei tietenkään kovakoodattaisi, vaan ne ladattaisiin properties tiedostosta, ja tässä nimen tilalla käytettäisiin property nimeä esim. osataulukko.sarake.nimi.
Enumilla määritellyt sarakkeet helpottavat koodin ymmärtämistä ja muutosten tekoa, mutta erityisen hyödylliseksi ne tulevat kun sarakkeita täytyy voida piilottaa. Näkyvissä olevat enumeilla määritellyt sarakkeet on käytännöllistä kuvata lista jäsenmuuttujalla. Esimerkiksi näin:
List<OsaTaulukkoSarake> sarakkeet = new ArrayList<OsaTaulukkoSarake>();
Näkyviin haluttavat sarakkeet lisätään sarakelistaan rakentajassa tai toteuttamalla metodeja listan muokkaamiseen jos näkyvissä olevia sarakkeita halutaan muuttaa dynaamisesti. Alla oleva asetaNakyviksi metodi käyttää harvemmin nähtyä syntaksia, joka ei rajoita montako parametria metodille annetaan.
public class GeneerinenOsaTableModel<TYYPPI extends MyyTavaOsa> extends AbstractTableModel{ List<TYYPPI> osat = new ArrayList<TYYPPI>(); public GeneerinenOsaTableModel(){ sarakkeet.add(OsaTaulukkoSarake.OSAN_NIMI); sarakkeet.add(OsaTaulukkoSarake.HINTA); } public void asetaNakyviksi( OsaTaulukkoSarake... nakyvatSarakkeet){ sarakkeet.clear(); for (OsaTaulukkoSarake sarake: nakyvatSarakkeet) sarakkeet.add(sarake); //tämä kutsu kertoo taulukolle että taulukon //rakenne on muuttunut fireTableStructureChanged(); }
Ensimmäinen tämän tekniikan hyöty näkyy tässä TableModellin välttämättömässä metodissa. Sarakkeiden määrän kertovaa arvoa ei tarvitse erikseen muistaa päivittää eikä se voi unohtua väärään arvoon.
@Override public int getColumnCount() { return sarakkeet.size(); }
Seuraava hyöty näkyy TableModellin tarvitsemassa getValueAt metodissa, joka palauttaa taulukon soluun tulevan arvon solun rivin ja sarake numeron perusteella. Taulukossa jossa sarakkeita voi olla piilotettuna sarakkeen järjestysnumero on kuitenkin hankala käsite. Aina on tietysti mahdollista laskea mitä saraketta numerolla tarkoitetaan, mutta siitä tulee helposti virheherkkää. Kun näkyvissä olevat sarakkeet ovat listassa niin tiedetään aina täydellä varmuudella mitä sarakenumerolla tarkoitetaan, kun sarakenumero "muutetaan" listan kautta sarakkeen määritteleväksi enum tyypiksi. Metodien setValueAt ja isCellEditable toteutukset olisivat hyvin saman tapaiset.
@Override public Object getValueAt(int rivi, int sarakeNumero) { switch(sarakkeet.get(sarakeNumero)){ case OSAN_NIMI: return annaNimiOsalleRivilla(rivi); case HINTA: return annaHintaOsalleRivilla(rivi); case VARASTOKOODI: return annaVarastokoodiOsalleRivilla(rivi); } throw new IndexOutOfBoundsException(); }
Alla oleva TableModellin pakollinen metodi kertoo taulukolle sarakkeen otsikkotekstin. Koska tämä tieto määriteltiin heti OsaTaulukkoSarake enumeraattorissa, saadaan se suoraan sarakelistasta. Paitsi että metodista tuli yksinkertainen, siitä tuli lähes idioottivarma päivitettävä. OsaTaulukkoSarake enumeraattoriin ei voi lisätä uutta saraketyyppiä ilman että sarakkeelle antaa nimen.
@Override public String getColumnName(int sarakeNumero){ return sarakkeet.get(sarakeNumero).annaSarakeNimi(); }
Tässä esimerkissä yhdellä enumeraatio määrityksellä ja yhdellä lista oliolla saatiin aikaan taulukkomodel joka on joustava, helppo ymmärtää, helppo ylläpitää ja sisältää paljon vähemmän mahodllisuuksia bugeille ja vähemmän koodia kuin vastaavan toteuttaminen esimerkiksi määrittelemällä vakioita kuten:
private final int OSA_NIMI_SARAKE=1; private final String OSA_NIMI_SARAKE_NIMI="Osan nimi"; jne. jne.
Osassa 2 pureudumme tässä esiteltyjen tekniikoiden hyötykäyttöön suuremmassa mittakaavassa tekemällä geneerisiä "metakomponentteja".



