RSS

Wykorzystaj polimorfizm w JavaScript

Liczba odsłon: 900

Każdy nie­ba­nal­ny pro­gram mu­si w to­ku prze­twa­rza­nia da­nych po­dej­mo­wać de­cy­zje. Są one oczy­wiś­cie zde­fi­nio­wa­ne przez pro­gra­mis­tę, a pro­gra­mo­wi po­zos­ta­je je­dy­nie ścis­łe trzy­ma­nie się in­struk­cji. Większość po­cząt­ku­ją­cych pro­gra­mis­tów za­py­ta­nych o to, jak za­pi­sać za­leż­ność to­ku reali­zac­ji pro­gra­mu od da­nych wej­ścio­wych, od ra­zu wy­mie­ni in­struk­cje if, switch lub ich od­po­wied­ni­ki w wy­bra­nym ję­zy­ku pro­gra­mo­wa­nia. Warto jed­nak po­rzu­cić ten tra­dy­cyj­ny tok myś­le­nia i przy okreś­la­niu wa­run­ków reali­zac­ji wy­ko­rzys­tać po­li­mor­fizm.

Typowym dy­dak­tycz­nym przy­kła­dem pro­gra­mu po­dej­mu­ją­ce­go de­cy­zję jest kal­ku­la­tor czte­ro­dzia­ła­nio­wy. Wprowa­dza­my dwie licz­by, wy­bie­ra­my ro­dzaj ope­rac­ji i otrzy­mu­je­my wy­nik. W ję­zy­ku Java­Script ta­ki kal­ku­la­tor bę­dzie wy­glą­dał na­stę­pu­ją­co:

function calculate() {
   var result;
   var a = parseFloat(document.getElementById('a').value);
   var b = parseFloat(document.getElementById('b').value);
   var opCode = document.getElementById('op').value;
   if (opCode === 'add') result = a + b;
   else if (opCode === 'sub') result = a - b;
   else if (opCode === 'mul') result = a * b;
   else if (opCode === 'div') result = (b != 0.0 ? a / b : 'ERR');
   document.getElementById('a_result').value = result;
}

Kod jest zwar­ty, względ­nie czy­tel­ny i krót­ki, a prze­de wszyst­kim — dzia­ła. Ma jed­nak rów­nież wa­dy:

Spróbuj­my za­tem za­pro­jek­to­wać no­we, lep­sze roz­wią­za­nie. Wymienio­ne czte­ry wa­dy zo­sta­ną zlik­wi­do­wa­ne, jed­nak w ich miej­sce mu­si po­ja­wić się in­na, za­mien­na: ob­szer­ność. Nie da się nie­ste­ty stwo­rzyć ele­ganc­kie­go ar­chi­tek­to­nicz­nie, roz­sze­rzal­ne­go i asyn­chro­nicz­ne­go pro­gra­mu nie wpro­wa­dza­jąc pew­ne­go nad­mia­ru ko­du.

Najpierw za­sto­suj­my wzo­rzec pro­jek­to­wy po­le­ce­nia i za­łóż­my, że funk­cja reali­zu­ją­ca wska­za­ną ope­ra­cję ma­te­ma­tycz­ną bę­dzie przyj­mo­wa­ła obiekt skła­da­ją­cy się z czte­rech me­tod:

var request = {
   getA: function() { zwraca wartość A },
   getB: function() { zwraca wartość B },
   getOpCode: function() { zwraca identyfikator operacji do wykonania },
   success: function(result) { obsługuje uzyskanie wyniku },
   failure: function() { obsługuje błąd, np. dzielenie przez zero }
}

W ten spo­sób uzys­ku­je­my od ra­zu czte­ry bar­dzo po­zy­tyw­ne ce­chy pro­gra­mu:

Funkcja cal­cu­la­te() z po­przed­nie­go przy­kła­du przy­bie­rze za­tem na­stę­pu­ją­cą po­stać:

function calculate() {
   var request = {
      getA: function() {
         return parseFloat(document.getElementById('a').value);
      },
      getB: function() {
         return parseFloat(document.getElementById('b').value);
      },
      getOpCode: function() {
         return document.getElementById('op').value;
      },
      success: function(result) {
         document.getElementById('result').value = result;
      },
      failure: function() {
         document.getElementById('result').value = 'ERR';
      }
   };
   perform(request);
}

Warto za­uwa­żyć, że za­miast za­pi­sy­wać co kom­pu­ter ma wy­ko­nać w ko­lej­nych kro­kach, opi­su­je­my spo­sób reali­zac­ji za­da­nia w bar­dziej ab­strak­cyj­ny spo­sób: w ja­ki spo­sób ma­ją zo­stać po­bra­ne czyn­ni­ki i kod ope­rac­ji, co ma być zro­bio­ne z wy­ni­kiem i co ma się stać, je­że­li wy­stą­pi błąd. To po­waż­ny krok w kie­run­ku tech­ni­ki pro­gra­mo­wa­nia in­ten­cyj­ne­go.

Teraz trze­ba zde­cy­do­wać w ja­ki spo­sób do­ko­nać wy­bo­ru ścież­ki reali­zac­ji ko­du: w koń­cu w ja­kiś spo­sób kom­pu­ter mu­si zde­cy­do­wać, czy licz­by ab do­da­wać, odej­mo­wać, mno­żyć czy dzie­lić. Tutaj jest miej­sce dla po­li­mor­fiz­mu. Załóżmy, że ope­rac­ji bę­dzie do­ko­ny­wa­ła funk­cja, któ­rej pa­ra­met­rem bę­dzie obiekt żą­da­nia o zde­fi­nio­wa­nej wcześ­niej po­sta­ci; na przy­kład:

function performAdd(request) {
   request.success(request.getA() + request.getB());
}

Aby jed­nak moż­na by­ło zde­fi­nio­wać wie­le ope­rac­ji i jed­no­cześ­nie ułat­wić kom­pu­te­ro­wi wy­bór właś­ci­wej, funk­cje od­po­wie­dzial­ne za po­szcze­gól­ne ro­dza­je dzia­łań umie­ści­my w tab­li­cy asoc­ja­cyj­nej i wy­wo­ła­my za po­śred­nic­twem jed­ne­go punk­tu wej­ścio­we­go:

var operations = {
   'add': function(request) {
      request.success(request.getA() + request.getB());
   },
   'sub': function(request) {
      request.success(request.getA() - request.getB());
   },
   'mul': function(request) {
      request.success(request.getA() * request.getB());
   },
   'div': function(request) {
      var b = request.getB();
      if (b === 0.0) request.failure();
      else request.success(request.getA() / b);
   }
};

function perform(request) {
   operations[request.getOpCode()](request);
}

Tak za­pi­sa­ny kal­ku­la­tor dzia­ła oczy­wiś­cie do­kład­nie tak sa­mo, jak po­przed­ni:

Ten ca­ły nad­miar ko­du (40 wier­szy tek­stu źród­ło­we­go Java­Script za­miast 11!) nie po­szedł jed­nak na mar­ne. Usunęliś­my wszyst­kie wy­mie­nio­ne wa­dy i zna­ko­mi­cie ułat­wi­liś­my do­da­wa­nie no­wych ro­dza­jów ope­rac­ji. Na przy­kład, aby ob­słu­żyć po­tę­go­wa­nie, wy­star­czy do­pi­sać:

operations['pow'] = function(request) {
   request.success(Math.pow(request.getA(), request.getB()));
};

Oczywiś­cie, ozna­cza to do­da­nie trzech wier­szy ko­du a nie jed­ne­go. Warto jed­nak za­uwa­żyć, że w ogó­le nie zmie­nia­my ist­nie­ją­ce­go ko­du! Nową funkcjo­nal­ność mo­że­my do­dać na­wet, je­że­li nie ma­my pra­wa mo­dy­fi­ko­wa­nia ist­nie­ją­cej funk­cji. Nie ma też ry­zy­ka, że wpro­wa­dzi­my błąd do in­nych ope­rac­ji (chy­ba, że – ce­lo­wo lub omył­ko­wo – nad­pi­sze­my ich de­fi­ni­cje włas­ny­mi).

Przejrzy­sta, prze­myś­la­na archi­tek­tu­ra pro­gra­mu po­zwa­la uzys­kać wy­so­ką funkcjo­nal­ność, pra­wie nie­ogra­ni­czo­ną roz­sze­rzal­ność i du­żą od­por­ność na błę­dy. Wszystko to pra­wie bez uży­cia in­struk­cji wa­run­ko­wych, a więc bez de­gra­dac­ji wy­daj­noś­ci po­wią­za­nej zaw­sze z dłu­gi­mi łań­cu­cha­mi if/el­se. Jedyną ce­ną, ja­ką trze­ba za to za­pła­cić, jest nie­co więk­sza ob­szer­ność ko­du i dłuż­szy czas, któ­ry trze­ba po­świę­cić na opra­co­wa­nie i wdro­że­nie ta­kiej archi­tek­tu­ry.