RSS

Wyższy poziom abstrakcji, część II

Liczba odsłon: 329

Wczoraj roz­po­cząłem cykl ar­ty­ku­łów do­ty­czą­cych zwięk­sza­nia po­zio­mu ab­strak­cji w pro­gra­mo­wa­niu na przy­kła­dzie ję­zy­ka Java. W pierw­szej częś­ci przed­sta­wi­łem spo­sób na zmniej­sze­nie na­kła­du pra­cy po­trzeb­nej do za­im­ple­men­to­wa­nia me­to­dy toString(). Dzisiaj czas na bar­dziej ra­dy­kal­ne po­dejś­cie.

Adnota­cje

Wersja 1.5 ję­zy­ka Java wpro­wa­dzi­ła me­cha­nizm ad­no­tac­ji (ang. an­no­ta­tion), za po­mo­cą któ­re­go moż­li­we sta­ło się ozna­cza­nie ele­men­tów ję­zy­ka ta­kich jak kla­sy, po­la, me­to­dy czy zmien­ne. Adnota­cje sa­me z sie­bie nie wpły­wa­ją na spo­sób ge­ne­ro­wa­nia ko­du, mo­gą być jed­nak ana­li­zo­wa­ne al­bo na eta­pie kom­pi­la­cji, al­bo w cza­sie dzia­ła­nia pro­gra­mu w ce­lu pa­ra­met­ry­zo­wa­nia dzia­łań.

Adnota­cje szyb­ko zdo­by­ły ser­ca pro­gra­mis­tów ję­zy­ka Java. Przed ich po­ja­wie­niem się, kon­fi­gu­rac­ja klas by­ła opi­sy­wa­na w zbio­rach XML do­łą­cza­nych do apli­kac­ji. Ich mo­dy­fi­ko­wa­nie nie jest naj­wy­god­niej­sze, a za­war­tość nie pod­le­ga we­ry­fi­ka­cji skład­nio­wej i me­ry­to­rycz­nej w cza­sie kom­pi­la­cji. Co gor­sza, bar­dzo łat­wo jest do­pro­wa­dzić do bra­ku spój­no­ści mię­dzy mo­dy­fi­ko­wa­ny­mi kla­sa­mi i od­po­wia­da­ją­cy­mi im me­ta­da­ny­mi XML. Efekty ob­ja­wia­ją się w ta­kim przy­pad­ku je­dy­nie nie­pra­wid­ło­wym dzia­ła­niem.

Adnota­cje są ele­men­tem ję­zy­ka, za­tem pod­le­ga­ją we­ry­fi­ka­cji skład­nio­wej. Najważniej­sze jest jed­nak, że umiesz­cza się je w tekś­cie źród­ło­wym pro­gra­mu, w bez­po­śred­niej bli­sko­ści ele­men­tów, któ­rych do­ty­czą. Dzięki te­mu du­żo trud­niej jest o po­mył­kę.

Mechanizm ad­no­tac­ji moż­na wy­ko­rzys­tać, by unik­nąć im­ple­men­to­wa­nia w po­szcze­gól­nych kla­sach stan­dar­do­wych, me­cha­nicz­nie pi­sa­nych me­tod, ta­kich jak toString(), hashCode() czy equals(). W ten spo­sób pro­gra­mis­ta mo­że prze­nieść się na wyż­szy po­ziom ab­strak­cji i osiąg­nąć dwa ce­le: zmniej­szyć na­kład swo­jej pra­cy i zwięk­szyć ja­kość opro­gra­mo­wa­nia.

Adnota­cje a toString

Przyj­mij­my, że za po­mo­cą na­stę­pu­ją­co zde­fi­nio­wa­nej ad­no­tac­ji:

@Target({ ElementType.TYPE, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface InToString {
}

bę­dzie­my ozna­czać po­la, któ­re ma­ją być uwzględ­nia­ne w wy­ni­ku zwra­ca­nym przez me­to­dę toString(). Dla ułat­wie­nia przyj­mij­my rów­nież, że ta sa­ma ad­no­tac­ja przy­pi­sa­na do kla­sy spo­wo­du­je, że wszyst­kie po­la da­nej kla­sy zo­sta­ną włą­czo­ne do wy­ni­ku. Te dwa za­ło­że­nia da­ją nam bar­dzo du­żą swo­bo­dę: w przy­pad­ku pros­tych klas-en­cji mo­że­my jed­ną ad­no­tac­ją uwzględ­nić wszyst­kie po­la, a w bar­dziej skom­pli­ko­wa­nych przy­pad­kach ma­my moż­li­wość do­wol­ne­go wy­bie­ra­nia pól.

Pozosta­je te­raz na­pi­sać me­to­dę, któ­ra wy­ko­rzy­sta me­cha­nizm re­flek­sji i prze­gląd­nie wszyst­kie po­la wska­za­ne­go obiek­tu pod ką­tem wy­stę­po­wa­nia ad­no­tac­ji @InToString:

public final class DataStructure {

    private final Object object;
    private final Class cls;

    public DataStructure(final Object object) {
        this.object = object;
        cls = object.getClass();
    }

    @Override
    public String toString() {
        final StringBuilder buffer = new StringBuilder(cls.getSimpleName());
        final boolean includeAllFields = cls.isAnnotationPresent(InToString.class);
        buffer.append('{');
        boolean firstField = true;
        String name;
        Object value;
        for (final Field field : cls.getDeclaredFields()) {
            if (includeAllFields || field.isAnnotationPresent(InToString.class)) {
                field.setAccessible(true);
                try {
                    name = field.getName();
                    value = field.get(object);
                    if (firstField) {
                        firstField = false;
                    } else {
                        buffer.append(", ");
                    }
                    buffer.append(name).append('=').append(value);
                } catch (final IllegalAccessException ignore) {}
            }
        }
        buffer.append('}');
        return buffer.toString();
    }

}

W tym mo­men­cie wy­star­czy za­pi­sać w swo­jej kla­sie me­to­dę toString() skła­da­ją­cą się z jed­ne­go wier­sza ko­du:

class Simple {

    @InToString
    private int value;

    private double hidden;

    public Simple(final int value, final double hidden) {
        this.value = value;
        this.hidden = hidden;
    }

    @Override
    public String toString() {
        return new DataStructure(this).toString();
    }

}

by wy­wo­ła­nie me­to­dy toString() spo­wo­do­wa­ło zwró­ce­nie właś­ci­we­go na­pi­su, pre­zen­tu­ją­ce­go za­war­tość po­la value, ale nie ukry­te­go po­la hidden.

Co zy­sku­je­my

Takie roz­wią­za­nie za­cho­wu­je wszyst­kie za­le­ty me­to­dy wy­ko­rzy­stu­ją­cej bi­blio­te­kę zew­nętrz­ną, opi­sa­ną w po­przed­niej częś­ci cyk­lu. Ma też jed­ną no­wą, bar­dzo istot­ną: trud­no jest po­mi­nąć jed­no z pól. Ponie­waż ad­no­ta­cje są umiesz­czo­ne bez­po­śred­nio przy po­lach, a po­la kla­sy za­zwy­czaj gro­ma­dzi się w jed­nym miej­scu, łat­wo za­uwa­żyć, że no­wo two­rzo­ne po­le po­win­no być opi­sa­ne ad­no­tac­ją. Jeżeli zaś – po­win­no to sta­no­wić wręcz re­gu­łę – ad­no­tac­ja bę­dzie do­ty­czy­ła ca­łej kla­sy, nie ma fi­zycz­nej moż­li­woś­ci po­mi­nię­cia któ­re­goś z pól.

Co da­lej

Można jesz­cze bar­dziej zwięk­szyć po­ziom ab­strak­cji, choć wy­ma­ga to już ko­rzy­sta­nia z tech­nik umoż­li­wia­ją­cych wpły­wa­nie na pro­ces kom­pi­la­cji. Na przy­kład, bi­blio­te­ka Lombok po­zwa­la po­mi­nąć krok two­rze­nia me­to­dy toString(): włą­cza się ona w pro­ces kom­pi­la­cji i auto­ma­tycz­nie syn­te­zu­je po­trzeb­ne me­to­dy. W ta­kim przy­pad­ku ad­no­tac­ja @ToString przy­łą­czo­na do kla­sy to wszyst­ko, cze­go trze­ba, by wy­wo­ła­nie toString() dla obiek­tów tej kla­sy da­wa­ło ocze­ki­wa­ny wy­nik.

Należy jed­nak pa­mię­tać, że zwięk­sza­nie po­zio­mu ab­strak­cji mo­że po­wo­do­wać zmniej­sze­nie szyb­koś­ci dzia­ła­nia pro­gra­mu. W szcze­gól­noś­ci, ko­rzy­sta­nie z me­cha­niz­mu re­flek­sji mo­że mieć ne­ga­tyw­ny wpływ na wy­daj­ność. O tym, jak du­ży jest ten wpływ i jak moż­na go ogra­ni­czyć, na­pi­szę w ko­lej­nej częś­ci cyk­lu.