RSS

Wyższy poziom abstrakcji, część I

Liczba odsłon: 168

Nowo­czes­ne pro­gra­mo­wa­nie ba­zu­je na ciąg­łym pod­no­sze­niu po­zio­mu ab­strak­cji. Programis­ta nie po­wi­nien spę­dzać cza­su na im­ple­men­to­wa­niu wciąż tych sa­mych pod­sta­wo­wych me­cha­niz­mów i ope­rac­ji, lecz sku­piać się na opi­sy­wa­niu wy­glą­du i funkcjo­nal­noś­ci apli­kac­ji. W tym cyk­lu wpi­sów przed­sta­wię kil­ka prak­tycz­nych za­gad­nień zwią­za­nych ze zwięk­sza­niem po­zio­mu ab­strak­cji ko­du apli­kac­ji pi­sa­nych w ję­zy­ku Java.

Metoda toString

Standar­do­wa kla­sa ba­zo­wa Object de­fi­niu­je me­to­dę toString(), któ­rej za­da­niem jest seria­li­zo­wa­nie obiek­tu do pew­nej po­sta­ci tek­sto­wej. Nie jest po­wie­dzia­ne, ja­ka jest to po­stać i cza­sem kor­ci, by wy­ko­rzys­tać me­to­dę toString() do prze­twa­rza­nia obiek­tu na na­pis uży­wa­ny w war­stwie inter­fej­su użyt­kow­ni­ka.

Dobrze jest jed­nak się przed tym po­wstrzy­my­wać: uży­tecz­na, tek­sto­wa po­stać obiek­tu, któ­rej for­mat jest nie­zmien­ny i we­ry­fi­ko­wa­ny te­sta­mi jed­nost­ko­wy­mi, po­win­na być do­stęp­na po­przez od­dziel­ną, właś­ci­wie naz­wa­ną me­to­dę. Na przy­kład, kla­sa Person prze­cho­wu­ją­ca imię i naz­wis­ko oso­by, za­miast zwra­cać po­łą­czo­ne imię i naz­wis­ko za po­mo­cą me­to­dy toString(), po­win­na de­fi­nio­wać me­to­dę getFullName(). W ten spo­sób gdy po­ja­wi się ko­niecz­ność wy­bie­ra­nia, czy peł­na po­stać ma za­wie­rać naj­pierw imię, czy naj­pierw naz­wis­ko, bę­dzie­my mog­li do­dać pa­ra­metr (co jest nie­moż­li­we w przy­pad­ku toString()).

W ol­brzy­miej więk­szoś­ci przy­pad­ków wy­nik toString() mo­że wy­glą­dać na­stę­pu­ją­co:

Klasa{pole=wartość, pole=wartość, ...}
Klasa@adres[pole=wartość,pole=wartość,...]

Takie spo­so­by seria­li­zo­wa­nia obiek­tów przez toString() są ob­słu­gi­wa­ne mię­dzy in­ny­mi przez bi­blio­te­ki Google Guava oraz Apache Commons. Należy jed­nak pod­kre­ślić, że for­mat nie gra tak na­praw­dę ro­li. Dobrze, je­że­li jest spój­ny i czy­tel­ny, jed­nak pro­gram nie mo­że być nig­dy od nie­go za­leż­ny. Celem ist­nie­nia toString() jest bo­wiem je­dy­nie pre­zen­to­wa­nie sta­nu obiek­tu w śro­do­wis­ku uru­cho­mie­nio­wym, dzien­ni­kach zda­rzeń i w trak­cie diag­no­zo­wa­nia dzia­ła­nia apli­kac­ji.

Jak się za­pi­su­je toString

Najbardziej pry­mi­tyw­nym spo­so­bem za­im­ple­men­to­wa­nia me­to­dy toString() jest:

@Override
public String toString() {
   return "Klasa{pole1=" + pole1 + ", pole2=" + pole2 + "}";
}

Nigdy nie wol­no im­ple­men­to­wać tej me­to­dy w ten spo­sób. Przede wszyst­kim, je­że­li któ­re­kol­wiek po­le ma war­tość null, wy­wo­ła­nie toString() spo­wo­du­je po­ja­wie­nie się sytu­acji wy­jąt­ko­wej Null­Pointer­Exception. Po dru­gie, wy­daj­ność ta­kiej im­ple­men­tac­ji bę­dzie ża­łos­na z po­wo­du ciąg­łych, kas­ka­do­wych złą­czeń na­pi­sów i alo­ko­wa­nia mnóst­wa obiek­tów tym­cza­so­wych.

Nieco lep­szym roz­wią­za­niem jest sko­rzy­sta­nie z kla­sy String­Bu­il­der. We względ­nie „in­te­li­gent­ny” spo­sób uży­wa ona po­więk­sza­ją­ce­go się bu­fo­ra, za­peł­nia­jąc go złą­cza­nym tek­stem. Ponadto, me­to­da ap­pend() mo­że być wy­wo­ły­wa­na z ar­gu­men­tem null, co skut­ku­je do­łą­cze­niem do bu­do­wa­ne­go tek­stu fra­zy null:

@Override
public String toString() {
   return new StringBuilder("Klasa{pole1=").append(pole1)
                     .append(", pole2=").append(pole2)
                     .append('}').toString();
}

Dużo lep­sze jest jed­nak wy­ko­rzys­ta­nie go­to­wych roz­wią­zań, na przy­kład ta­kich jak ist­nie­ją­ce w bi­blio­te­ce Google Guava:

@Override
public String toString() {
   return MoreObjects.toStringHelper(this)
             .add("pole1", pole1)
             .add("pole2", pole2)
             .toString();
}

W ten spo­sób prze­cho­dzi­my na wyż­szy po­ziom ab­strak­cji. Zamiast im­ple­men­to­wać me­cha­nizm toString(), za­pi­su­je­my re­gu­ły, wed­ług któ­rych ma na­stą­pić kon­wer­sja. Mechanizm jest za­szy­ty w bi­blio­te­ce, cze­go po­zy­tyw­ny­mi skut­ka­mi są:

Co da­lej?

Użycie właś­ci­wej bi­blio­te­ki we wszyst­kich im­ple­men­tac­jach toString() to nie wszyst­ko, co moż­na zro­bić. Są co naj­mniej trzy ob­sza­ry, w któ­rych to roz­wią­za­nie jest nie­kom­plet­ne i nie­sa­tys­fak­cjo­nu­ją­ce.

Po pierw­sze, roz­pa­tru­jemy je­dy­nie me­to­dę toString(). Jest ona dość spe­cy­ficz­na: aku­rat w jej przy­pad­ku ist­nie­je kil­ka sze­ro­ko roz­pow­szech­nio­nych bi­blio­tek z właś­ci­wą im­ple­men­tac­ją. Każda kla­sa po­win­na jed­nak im­ple­men­to­wać rów­nież me­to­dy ta­kie jak equals()hashCode(). Poza tym, moż­na wy­myś­lać włas­ne me­to­dy, wy­ma­ga­ne w pew­nej ka­te­gorii klas, któ­rych im­ple­men­tac­ja jest pow­ta­rzal­na i me­cha­nicz­na i po­win­na być rea­li­zo­wa­na przez ja­kąś bi­blio­te­kę. Przykła­dem ta­kiej me­to­dy mo­że być assignFrom(), usta­la­ją­ca stan obiek­tu na pod­sta­wie in­ne­go obiek­tu (w szcze­gól­noś­ci tej sa­mej kla­sy).

Po dru­gie, mi­mo, że dzię­ki ze­wnętrz­nej bi­blio­te­ce za­pis me­cha­niz­mu zo­stał za­stą­pio­ny za­pi­sem re­guł, moż­na po­peł­nić błę­dy w tych re­gu­łach. W opi­sy­wa­nym przy­pad­ku pierw­sze, ja­kie na­su­wa­ją się na myśl, to:

Po trze­cie, wciąż trze­ba za­pi­sać me­to­dę toString(). Owszem, jest te­raz krót­sza i mniej pow­ta­rzal­na, jed­nak re­gu­ły są za bar­dzo od­dzie­lo­ne od ele­men­tów, któ­rych do­ty­czą.

Wszystkim tym trzem nie­do­ciąg­nię­ciom moż­na za­ra­dzić na kil­ka spo­so­bów. O tym jed­nak na­pi­szę w ko­lej­nych częś­ciach cyk­lu.