RSS

Niemutowalność przez interfejsy: nie taka prosta, jak się wydaje

Liczba odsłon: 186

W wie­lu ję­zy­kach pro­gra­mo­wa­nia sto­su­je się kon­cep­cję obiek­tów nie­mu­to­wal­nych. Niemutowal­ność jest szcze­gól­nie przy­dat­na, gdy two­rzy się wie­lo­wąt­ko­we opro­gra­mo­wa­nie dzia­ła­ją­ce w śro­do­wis­ku za­rzą­dza­nym (ang. ma­na­ged), gdyż zwięk­sza wtór­ność wy­ko­rzys­ta­nia struk­tur da­nych przez re­fe­ren­cję i cał­ko­wi­cie lik­wi­du­je zja­wi­sko wy­ści­gów i po­trze­bę syn­chro­ni­za­cji wąt­ków.

Nie zaw­sze jed­nak nie­mu­to­wal­ność jest po­żą­da­na, a cza­sem wręcz nie moż­na jej sto­so­wać. Przykła­dem jest ję­zyk Java i pro­gra­my, w któ­rych sto­so­wa­ne są roz­wią­za­nia ba­zu­ją­ce na Java Beans, ta­kie jak JSF czy JPA. Można jed­nak po­łą­czyć kon­cep­cję mu­to­wal­noś­ci i nie­mu­to­wal­no­ści obiek­tów, two­rząc je­den z dwóch wa­rian­tów obiek­tu w za­leż­noś­ci od po­trzeb.

Jednym z roz­wią­zań jest za­sto­so­wa­nie inter­fej­sów. Wyobraź­my so­bie kla­sę re­pre­zen­tu­ją­cą umie­jęt­ność po­sta­ci w grze wraz z jej po­zio­mem w za­kre­sie od 0 do 10. Normalnie ta­ką struk­tu­rę da­nych przed­sta­wi­li­byś­my w po­sta­ci:

public final class Capability {

   public Capability(final @Nonnull String name, final int level) {
      setName(name);
      setLevel(level);
   }

   public @Nonnull String getName() {
      return name;
   }

   public void setName(final @Nonnull String name) {
      this.name = Preconditions.checkNotNull(name);
   }

   public int getLevel() {
      return level;
   }

   public void setLevel(final int level) {
      Preconditions.checkArgument(level >= 0 && level <= 10);
      this.level = (byte) level;
   }

   private @Nonnull String name;
   private byte level;

}

Przyj­mij­my jed­nak, że chcie­li­byś­my za­blo­ko­wać moż­li­wość zmia­ny sta­nu obiek­tu kla­sy Capabili­ty. W ję­zy­ku C++ po­zwa­la na to sło­wo klu­czo­we const, wy­mu­sza­ją­ce nie­mu­to­wal­ność. Język Java nie ma ta­kie­go me­cha­niz­mu: w za­mian mu­si­my zlik­wi­do­wać akce­so­ry mu­tu­ją­ce lub… unie­moż­li­wić do­stęp do nich:

public interface ImmutableCapability {
   
   @Nonnull String getName();

   int getLevel();

}

public interface MutableCapability extends ImmutableCapability {
   
   void setName(@Nonnull String name);

   void setLevel(int level);

}

public final class Capability implements MutableCapability {

   …
   …

}

Od te­go mo­men­tu je­że­li gdzie­kol­wiek prze­ka­że­my obiekt kla­sy Capabili­ty zrzu­to­wa­ny na ImmutableCapability, do­stęp­ne bę­dą tyl­ko akce­so­ry od­czy­tu­ją­ce. Wnętrze mo­du­łu pro­gra­mu mo­że za­tem przy­go­to­wać obiekt za­peł­nia­jąc go par­tia­mi, a na­stęp­nie prze­ka­zać w for­mie unie­moż­li­wia­ją­cej zmia­nę sta­nu. Technika ta zo­sta­ła opi­sa­na mię­dzy in­ny­mi przez Andy'ego Gibsona.

Sposób wy­da­je się z wierz­chu spryt­ny i oszczęd­ny: za po­mo­cą sa­mych inter­fej­sów, dzie­dzi­cze­nia i rzu­to­wa­nia uzys­ku­je­my nie­mu­to­wal­ność mu­to­wal­nych obiek­tów. Diabeł tkwi w szcze­gó­łach. Wyobraź­my so­bie te­raz mo­duł pro­gra­mu, któ­ry bar­dzo sil­nie za­le­ży od nie­mu­to­wal­ność prze­ka­zy­wa­nych mu obiek­tów. Zapamię­tu­je on re­fe­ren­cje do obiek­tów ImmutableCapability i ko­rzy­sta z za­pi­sa­nych w nich in­for­mac­ji w wie­lu wąt­kach. Problem w tym, że stan tych obiek­tów mo­że się zmie­nić, gdyż tak na­praw­dę wciąż są one mu­to­wal­ne:

Obiekt moż­na na­zwać nie­mu­to­wal­nym do­pie­ro w mo­men­cie, gdy:

Niestety, nie po­ra­dzi­my so­bie bez dwóch od­ręb­nych im­ple­men­tac­ji: jed­nej nie­mu­to­wal­nej, z po­la­mi fi­nal­ny­mi i jed­nej mu­to­wal­nej, z po­la­mi nie­fi­nal­ny­mi. Pozytyw­nym skut­kiem ubocz­nym bę­dzie wyż­sza wy­daj­ność, gdyż ma­szy­na wir­tu­al­na mo­że sil­niej op­ty­ma­li­zo­wać dzia­ła­nie ko­du ko­rzys­ta­ją­ce­go z pól fi­nal­nych.

Nie mu­si­my jed­nak re­zyg­no­wać ze sztucz­ki z inter­fej­sa­mi, któ­rą za­pro­po­no­wał Andy Gibson. Zmodyfikuj­my jed­nak tro­chę za­pis:

public interface Capability {
   
   @Nonnull String getName();

   int getLevel();

}

public interface ImmutableCapability extends Capability {
}

public interface MutableCapability extends Capability {
   
   void setName(@Nonnull String name);

   void setLevel(int level);

}

Uzyskaliś­my w ten spo­sób bar­dzo waż­ny po­dział: obiek­ty klas ImmutableCapability oraz MutableCapability nie są zgod­ne i nie pod­le­ga­ją rzu­to­wa­niu. Jeżeli za­le­ży nam na nie­mu­to­wal­no­ści, wy­ma­gaj­my re­fe­ren­cji do ImmutableCapability i nikt nam nie prze­ka­że obiek­tu mu­to­wal­ne­go, speł­nia­ją­ce­go inter­fejs MutableCapability. Jeżeli jed­nak jest wszyst­ko jed­no, czy otrzy­my­wa­ny obiekt jest mu­to­wal­ny, czy nie, mo­że­my przyj­mo­wać pa­ra­metr Capabili­ty i ma­my do­stęp do obiek­tu w for­mie tyl­ko do od­czy­tu, choć bez gwa­ran­cji nie­mu­to­wal­no­ści.

Rozwiąza­nie to spraw­dza się, gdy mie­sza­my ze so­bą „czys­ty” kod obiek­to­wy oraz bar­dziej pro­ce­du­ral­ne roz­wią­za­nia w sty­lu JSFJPA. Warto jed­nak, gdy tyl­ko jest to moż­li­we, dą­żyć właś­nie w stro­nę ele­ganc­kie­go ko­du obiek­to­we­go cał­ko­wi­cie po­zba­wio­ne­go ak­ce­so­rów.