ComiaTip Periytys ja generics GUI-ohjelmoinnissa II


Generics, enum ja periyttäminen käyttöliittymän ohjelmoinnissa Osa 2

Kirjoittanut Hannu Julkaistu 9.3.2009

Osassa 1 esittelin periyttämisen, generisien ja enumeraation käyttöä geneerisen ja helposti ylläpidettävän TaulukkoModel luokan toteuttamisessa. Tässä osassa laajennan samoja tekniikoita yksittäisiä käyttöliittymäkomponentteja pidemmälle.

Ikkunaksi JDialog vai JFrame

Java Swingissä kaksi yleisintä ikkunakomponenttia ovat JDialog ja JFrame. Molemmilla on pitkälti sama rajapinta, mutta eri käyttötarkoitukset ja periytyminen eri haaroista. JFrame vastaa normaalia ohjelman pääikkunaa ja käyttöjärjestelmä antaa sille paikan käynnissä olevien ohjelmien listassa (esim. Windowsissa TaskBar). JDialog taas on dialogi ikkuna eli sitä ei näytetä ohjelmien listassa ja se voi olla modaalinen.

Ongelmalliseksi näiden kahden pienet erot tulevat silloin kun käyttöliittymään tehdään muutoksia ja aikaisemmin framena toteutettu ikkuna pitäisi muuttaa dialogiksi tai kun saman ikkuman pitäisi pystyä toimimaan molemmissa rooleissa (tai kun täsmälleen samaa toiminnallisuutta tarvitaan ohjelmassa muuallakin). Graafisella käyttöliitymän rakennustyökalulla saman ikkunan tekisi tietysti äkkiä kerran dialogina, kerran framena ja saman toiminnallisuuden muualle niin monta kertaa kuin tarvitaan. Jossain vaiheessa toiminnasta kuitenkin löytyy bugeja tai käyttöliittymästä parantamisen varaa ja sitten joudutaan samoja muutoksia tekemään moneen paikkaan. Tätä ongemaa ei pysty suoraan periyttämisellä tai generisillä ratkaisemaan, mutta vastaavalla filosifialla kyllä.

Ikkunaan tuleva käyttöliittymä kannnattaa tehdä riippumattomaksi siitä kontekstista (JFramessa, JDialogissa vai osana muuta käyttöliittymää) jossa sitä käytetään. Swingissä tämä aikaansaadaan luomalla ikkunaan tuleva käyttöliittymä JPanel:lin sisälle. Kun käyttöliittymä halutaan näyttää JFrame ikkunassa, luodaan vain JFrame ja asetetaan sille luotu käyttöpiittymä paneeli contentPaneksi. Kun tarvitaan JDialog, luodaan JDialog ikkuna ja asetetaan sama käyttöliittymä panel sen contextPaneksi.

public static void showLoginFrame(){ JFrame dialog = new JFrame(); JPanel content; content = new LoginPanel(); dialog.setContentPane(content); dialog.setResizable(false); dialog.pack(); dialog.setVisible(true); }

Ylläolevassa esimerkkikoodissa näytetään login ikkuna JFramena (käyttäjälle mukavampaa aina kun kyseessä on ohjelman ainoa ikkuna, JDialog vain hautautuisi muiden ikkunoiden alle). LoginPanel luokka sisältää kaiken ohjelmaan kirjautumiseen liittyvän käyttöliittymätoiminnallisuuden. Kun ikkunasta tarvitaankin JDialog tyyppinen ikkuna (esim. modaalinen ja ei TaskBarissa näkyvä), tehdään sille ylläolevan kaltainen näyttömetodi, jossa JFrame on korvattu JDialog luokalla.

Don't repeat yourself(DRY) periaatteen mukaan koodatessa tulisi suuri hinku tehdä erillinen metodi, jossa tehdään kaikki molemmille ikkunatyypeille tarvittavat kutsut (esimerkkimetodissa kaikki ensimmäistä riviä lukuunottamatta). Mutta se ei onnistu. JDialog ja JFrame ovat periytymishierarkiassa korkeintaan pikkuserkkuja, joilla vain sattuu olemaan saman nimisiä metodeja, joten niinkin yksinkertainen toimenpide kuin ikkunan otsikon asettaminen täytyy tehdä molemmille erikseen.

Ikkunoiden sisällön määrittäminen setContentPane metodilla on ideana hyvin samanlainen kuin luokan käsittelemien muuttujien tyypin määritteleminen generisillä. Toteutustekniikka vain on eri, mutta uudelleenkäytettävyyttä eli geneerisyyttä tavoitellaan molemmissa.


Geneerinen Metakomponentti

Ylihieno nimitys sille että käyttöliittymä komponenetti voi koostua monesta muusta käyttöliittymäkomponentista ja olla uudelleenkäytettävä. Viittasin edellä sellaiseen mahdollisuuteen, että dialogiin tulevaa käyttöliittymää saatettaisiin tarvita muualla ohjelmassa "upotettuna" osaksi muuta käyttöliittymää. Jos dialogissa käytetty käyttöliittymä on toteutettu itsenäiseksi osaksi oman JPanellinsa sisälle, sen voi surutta liittää minne tahansa ohjelman käyttöliittymässä. Esimerkiksi Netbeansin GuiBuilderilla näitä omatekoisia metakomponentteja voi lisäilla kuten mitä tahansa valmiita käyttöliittymäkomponentteja.

Mikään ei tietenkään myöskään estä java generiksien ja periyttämisen käyttöä metakomponenteille. Allaoleva listalta toiselle valinta wizard on toteutettu tässä ja osassa 1 esitellyllä geneerisillä taulukoilla. Jokainen wizardin sivu sisältää logiikan siirrellä erityyppisiä oliota taulukosta toiseen, mutta wizard sivu ja TableModellit on toteutettu vain kerran generiksiä käyttäen.

generic wizard

public class PartSelectionPanel<TYYPPI extends Osa> extends PartSelectionPanelTemplate{

Luokka PartSelectionPanelTemplate on GuiBuilderilla tehty JPanel joka sisältää kuvassa näkyvät << ja >> napit ja JScrollPanet joissa taulukot ovat. PartSelectionPanel on geneerinen luokka joka rakentajassaan luo sille annetun TYYPPI olion mukaiset taulukko ja TableModel oliot generikseillä toteutetuista luokista, ja asettaa talukko oliot niille varattuihin Scrollpane komponentteihin.

Osassa 1 käytin enum tyyppiä yksiselitteisenä kuvaajana taulukon sarakkeelle ja sisällytin siihen sarakkeen nimen. Samaa tekniikkaa voi yhtä hyvin hyödyntää geneerisille metakomponenteille. Jos esimerkki wizardissa haluttaisiin muuttaa ikkunan otsikkoa vastaamaan sivun sisältöä ja sisällyttää kaikkiin sivuihin yksilöllinen lyhyt ohjeteksti tms. pieniä eroja, niin enum sopisi hyvin näiden erojen säilytyspaikaksi.

enum WizardPage{ //oikeasti tietysti käytettäisiin tässä property key //stringejä tyyliin "wizard.select.spare.parts" VARAOSA_PAGE("Valitse varaosat","Näitä varastossa"), TILAUSOSA_PAGE("Tilattavat osat", "Näitä tilauksesta"); String windowTitle; String helpLabelText; WizardPage(String title,String helpText){ windowTitle=title; helpLabelText=helpText; } } public class PartSelectionPanel<TYYPPI extends Osa> extends PartSelectionPanelTemplate{ public PartSelectionPanel(WizardPage pageDetils){ //label teksti enumista paikalleen yläluokassa //olevaan labeliin asetaOhjeTeksti(pageDetauls.helpLabelText); } ....

Samalla tavalla kuin osan 1 TableModel esiemerkissä, wizardiin tulevien sivujen määrästä riippumatta, mitään arvoa ei voi unohtaa päivittää. PartSelectionPanelia ei voi tehdä asettamatta ohjetekstiä paikoilleen ja tekemättä sivulle WizardPage enumeraattoria, jota puolestaan ei voi tehdä antamatta sille otsikko ja ohje tekstiä. Wizard sivujen kääntely saa jäädä tämän vinkin ulkopuolelle, mutta tapahtuisi helposti vähän samalla tavalla kuin TableModellin sarakkeiden hallinta.

Miksi näin abstractiin toteutukseen pitäisi vaivautua?

  • Ylläpidettävyys. Kun bugeja löytyy, ne tarvitsee korjata vain yhteen paikkaan.
  • Johdonmukaisuus. Useampi saman asian toteutus ei koskaan pysy kaikissa samanlaisena kovin kauaa ja lopputuloksena on N kappaletta erilaisia toteutuksia ja N lievästi erilaista käyttöliittymää. Periyttäminen pakottaa toiminnan ja layoutin pysymään samana.