RSS

Niemutowalność obiektów

Liczba odsłon: 1598

Jednym z pierw­szych ele­men­tów pro­gra­mo­wa­nia obiek­to­we­go, po­zna­wa­nym w ra­mach kur­sów pro­gra­mo­wa­nia, jest her­me­ty­za­cja. Prawidło­wo sto­so­wa­na her­me­ty­za­cja ma zni­ko­my wpływ na wy­daj­ność, a zna­czą­co zwięk­sza przej­rzy­stość tek­stu źród­ło­we­go i nie­za­wod­ność pro­gra­mu oraz ułat­wia póź­niej­szą re­fak­to­ry­za­cję ko­du.

Hermetyza­cję reali­zu­je się ty­po­wo za po­śred­nic­twem tak zwa­nych getterów oraz setterów. Na przy­kład, kla­sa re­pre­zen­tu­ją­ca oso­bę i prze­cho­wu­ją­ca jej imię oraz naz­wis­ko mo­że zo­stać za­her­me­ty­zo­wa­na na­stę­pu­ją­co:

class Osoba
{
   std::string imie;
   std::string nazwisko;
public:
   Osoba(const std::string &Imie, const std::string &Nazwisko);
   void UstawImie(const std::string &NoweImie);
   void UstawNazwisko(const std::string &NoweNazwisko);
   inline const std::string & Imie() const;
   inline const std::string & Nazwisko() const;
};

Metody her­me­ty­zu­ją­ce UstawImie oraz UstawNazwisko mo­gą zo­stać do­dat­ko­wo za­bez­pie­czo­ne przed po­da­niem nie­po­praw­nych da­nych. Podanie ar­gu­men­tu nie speł­nia­ją­ce­go pew­ne­go zbio­ru re­guł (do­ty­czą­cych dłu­goś­ci, wiel­koś­ci zna­ków lub sto­so­wa­nych sym­bo­li) mo­że po­wo­do­wać zgło­sze­nie sytu­acji wy­jąt­ko­wej. Z jed­nej stro­ny za­bez­pie­cza to pro­gram przed po­da­niem nie­właś­ci­wych da­nych (co dłu­go­fa­lo­wo prze­kła­da się na je­go bez­pie­czeń­stwo, lik­wi­du­jąc na przy­kład jed­no z po­tenc­jal­nych źró­deł ata­ków ty­pu SQL injection), z dru­giej — umoż­li­wia prze­chwy­ce­nie sytu­acji wy­jąt­ko­wych w war­stwie inter­fej­su użyt­kow­ni­ka i zmu­sze­nie ope­ra­to­ra do po­pra­wie­nia błęd­nych da­nych.

Hermety­za­cja mo­że też ewo­luo­wać w za­leż­noś­ci od po­trzeb. Na przy­kład, pro­gra­mis­ta mo­że stwier­dzić, że imię i naz­wis­ko oso­by są zaw­sze usta­la­ne ra­zem, za­tem dwie od­ręb­ne me­to­dy her­me­ty­zu­ją­ce

void UstawImie(const std::string &NoweImie);
oid UstawNazwisko(const std::string &NoweNazwisko);

moż­na za­stą­pić jed­ną wspól­ną:

void UstawImieNazwisko(const std::string &NoweImie,
     const std::string &NoweNazwisko);

Hermety­za­cja umoż­li­wia­ją­ca od­czy­ty­wa­nie i mo­dy­fi­ko­wa­nie cech obiek­tów spra­wia jed­nak prob­le­my w opro­gra­mo­wa­niu o bar­dziej skom­pli­ko­wa­nej archi­tek­tu­rze. Przede wszyst­kim, w za­sa­dzie unie­moż­li­wia oszczę­dza­nie pa­mię­ci przez za­stą­pie­nie ko­pio­wa­nia obiek­tu współ­dzie­le­niem re­fe­ren­cji do obiek­tu. Jeżeli w jed­nym miej­scu obiekt zo­sta­nie zmo­dy­fi­ko­wa­ny za po­mo­cą me­tod her­me­ty­zu­ją­cych, zmia­na prze­nie­sie się do wszyst­kich po­zo­sta­łych obiek­tów. Często do­kład­nie o to cho­dzi, ale jeś­li zmia­na mia­ła być tyl­ko lo­kal­na, oszczęd­ność skut­ku­je wpro­wa­dze­niem błę­du do pro­gra­mu.

Jeszcze więk­szy prob­lem po­ja­wia się w przy­pad­ku archi­tek­tur wy­ko­rzy­stu­ją­cych prze­twa­rza­nie współ­bież­ne. Modyfi­ka­cja obiek­tu uży­wa­ne­go przez wie­le wąt­ków wy­ma­ga sto­so­wa­nia me­cha­niz­mów syn­chro­ni­za­cji; w prze­ciw­nym przy­pad­ku pro­gram bę­dzie ko­rzy­stał z częś­cio­wo zmie­nio­nych da­nych lub uleg­nie cał­ko­wi­tej awarii. Synchro­ni­za­cja zna­czą­co utrud­nia pro­gra­mo­wa­nie i wpro­wa­dza na­rzut wy­daj­no­ścio­wy.

Jedną z tech­nik umoż­li­wia­ją­cych po­ra­dze­nie so­bie z ty­mi prob­le­ma­mi jest sto­so­wa­nie klas nie­mu­to­wal­nych (ang. immutable classes). Niemutowal­ność po­le­ga na bra­ku moż­li­woś­ci wpro­wa­dza­nia zmian do ist­nie­ją­cych obiek­tów. Najprost­szym środ­kiem ku te­mu jest usu­nię­cie me­tod her­me­ty­zu­ją­cych ty­pu setter. W ta­kim przy­pad­ku je­dy­nym spo­so­bem, by zmo­dy­fi­ko­wać obiekt, sta­je się stwo­rze­nie no­we­go, gdyż tyl­ko kon­struk­tor pa­ra­met­rycz­ny ma pra­wo usta­le­nia za­war­toś­ci pól.

Pierwszą za­le­tą nie­mu­to­wal­no­ści jest bez­pie­czeń­stwo współ­dzie­le­nia re­fe­ren­cji do obiek­tu. Ponie­waż ma­my pew­ność, że stan obiek­tu nie zmie­ni się, mo­że­my prze­cho­wy­wać w pa­mię­ci wskaź­ni­ki lub re­fe­ren­cje do obiek­tu w róż­nych miej­scach bez oba­wy, że da­ne prze­sta­ną być aktu­al­ne. Jest to szcze­gól­nie atrak­cyj­na per­spek­ty­wa w ję­zy­kach pro­gra­mo­wa­nia wy­po­sa­żo­nych w me­cha­nizm auto­ma­tycz­ne­go gos­po­da­ro­wa­nia pa­mię­cią opera­cyj­ną, gdyż zni­ka w nich po­ję­cie właś­ci­cie­la obiek­tu, od­po­wie­dzial­ne­go za zwol­nie­nie pa­mię­ci.

Drugą za­le­tą jest brak ko­niecz­noś­ci syn­chro­ni­zo­wa­nia zmian w obiek­cie. Synchro­ni­zo­wać trze­ba co naj­wy­żej zmien­ne prze­cho­wu­ją­ce wskaź­ni­ki lub re­fe­ren­cje al­bo kon­te­ne­ry prze­cho­wu­ją­ce zbiór obiek­tów. Skraca to czas prze­by­wa­nia pro­gra­mu w sek­cjach kry­tycz­nych: na­wet, je­że­li zmia­na sta­nu obiek­tu jest ko­sztow­na cza­so­wo, mo­że prze­bie­gać po­za sek­cją kry­tycz­ną. Dopiero pod­mia­na sta­rej re­fe­ren­cji na no­wą wy­mu­sza wejś­cie – na kil­ka cyk­li ze­ga­ra mi­kro­pro­ce­so­ra – w sek­cję kry­tycz­ną.

Niestety, nie­mu­to­wal­ność ma swo­ją ciem­ną stro­nę. W przy­pad­ku skom­pli­ko­wa­nych obiek­tów, wpro­wa­dza­nie drob­nych zmian jest du­żo bar­dziej kosz­tow­ne cza­so­wo, niż w przy­pad­ku klas mu­to­wal­nych. Każda zmia­na wią­że się bo­wiem z cza­so­chłon­nym stwo­rze­niem cał­kiem no­we­go obiek­tu oraz – w chwi­li, gdy jest to moż­li­we – znisz­cze­niem do­tych­cza­so­we­go. Przy oby­dwu tych ope­rac­jach ko­niecz­ne jest alo­ko­wa­nie i zwal­nia­nie blo­ków pa­mię­ci, co ne­ga­tyw­nie wpły­wa na wy­daj­ność opro­gra­mo­wa­nia pi­sa­ne­go we wszyst­kich ję­zy­kach pro­gra­mo­wa­nia, włącz­nie z ty­mi, któ­re dys­po­nu­ją me­cha­niz­mem auto­ma­tycz­ne­go gos­po­da­ro­wa­nia pa­mię­cią opera­cyj­ną.

Dodatkowo, w ta­kich właś­nie ję­zy­kach pro­gra­mo­wa­nia, nie­mu­to­wal­ność mo­że pro­wa­dzić do szyb­kie­go wzros­tu roz­mia­ru ster­ty, a więc do­dat­ko­wo spo­wal­niać dzia­ła­nie ca­łe­go sys­te­mu opera­cyj­ne­go oraz po­zo­sta­łych dzia­ła­ją­cych pro­gra­mów.

Niemutowal­ność jest za­tem tech­ni­ką bar­dzo przy­dat­ną w przy­pad­ku, gdy: