Przykład wykorzystania domknięć w PHP

Domknię­cia i funk­cje anoni­mowe to nowa rzecz w PHP 5.3. Nie będę pisał teorii, bo szkoda czasu (Google sporo wie jakby co). Za to skupię się na prak­tycz­nym wyko­rzy­sta­niu. Przy­kłady w necie zawie­rają zazwy­czaj bardzo proste rzeczy. Dzisiaj pokażę coś trochę trudniejszego.

Napi­szemy sobie klasę Liczydlo, w której da się zapi­sy­wać różne algo­rytmy liczące np. jak poli­czyć śred­nią aryt­me­tyczną, medianę i najwięk­szy wspólny dziel­nik. Algo­rytmy będą zapi­sy­wane w jednej zmien­nej obiektu w postaci tablicy asocja­cyj­nej. Osta­teczne rozwią­za­nia będą wyko­ny­wane poprzez metodę przy­po­mi­na­jącą wzorzec projek­towy Strategia.

Do napi­sa­nia ~100 lini­jek. Start!

<?php
class Liczydlo {
 
   protected $algorytmy = array();
 
   public function setAlgorytm($nazwa, $funkcja) {
      // sprawdzamy czy uzytkownik rzeczywiscie podaje nam
      // funkcje anonimowa
      if (is_callable($funkcja)) {
         // algorytm latwo daje sie zapisac w tablicy
         $this->algorytmy[$nazwa] = $funkcja;
      }
      else {
         throw new Exception("{$funkcja} nie da sie wywolac jako funkcja");
      }
   }
 
   // glowna metoda liczaca, przyjmuje stringa z nazwa algorytmu
   // i zestaw liczb, na ktorych ma byc wykonane dzialanie
   public function rozwiaz($ciag) {
      $rozpoznany = $this->rozpoznajCiag($ciag);
      $algorytm = $this->zwrocAlgorytm($rozpoznany['algorytm']);
      return $algorytm($rozpoznany['liczby']);
   }
 
   // rozdziela ciag na nazwe algorytmu i tablice z liczbami do policzenia
   protected function rozpoznajCiag($ciag) {
      $ciag = preg_split('/[\s,;]+/', $ciag);
      return array('algorytm' => strtolower($ciag[0]), 'liczby' => array_slice($ciag, 1));
   }
 
   protected function zwrocAlgorytm($nazwa) {
      if (empty($this->algorytmy[$nazwa])) {
         throw new Exception("Nie znaleziono algorytmu o nazwie {$nazwa}");
      }
         return $this->algorytmy[$nazwa];
   }
}

Tyle na temat klasy. Póki co wie ona jak rozdzie­lić podany jej ciąg na dzia­ła­nie i liczby, ale nie zna żadnych algo­ryt­mów. Tutaj na scenę wcho­dzą domknięcia.

$liczydlo = new Liczydlo();
 
// na poczatek banal, ktory przyda sie pozniej
// czyli suma wszystkich liczb
$liczydlo->setAlgorytm('suma', function(array $liczby) {
   $wynik = '';
   foreach ($liczby as $liczba) {
      $wynik += $liczba;
   }
   return $wynik;
});
 
// teraz uzyjemy use() do wlaczenia w cialo funkcji zmiennej
// spoza zasiegu
$liczydlo->setAlgorytm('srednia', function(array $liczby) use ($liczydlo) {
   $liczbaElementow = count($liczby);
   if ($liczbaElementow < 2) throw new Exception('Podaj co najmniej 2 liczby');
   // skoro liczydlo potrafi policzyc sume to skorzystamy z tej mozliwosci
   $suma = $liczydlo->rozwiaz('suma ' . implode(',', $liczby));
   return $suma / $liczbaElementow;
});
 
// tutaj pojdziemy krok dalej, czyli policzymy mediane,
// ktora z kolei skorzysta ze sredniej (ktora z kolei korzysta z sumy)
$liczydlo->setAlgorytm('mediana', function(array $liczby) use ($liczydlo) {
   $liczbaElementow = count($liczby);
   if ($liczbaElementow < 2) throw new Exception('Podaj conajmniej 2 liczby');
   sort($liczby,SORT_NUMERIC);
   if (($liczbaElementow % 2) == 0) {
      return $liczydlo->rozwiaz('srednia ' .
         $liczby[($liczbaElementow/2)-1] . ',' .
         $liczby[$liczbaElementow/2]);
   }
   else {
      return $liczby[floor($liczbaElementow/2)];
   }
});
 
// no i to co tygryski lubia najbardziej, czyli
// liczenie najwiekszego wspolnego dzielnika za pomoca
// funkcji anonimowej zagniezdzonej w funkcji anonimowej
$liczydlo->setAlgorytm('nwd', function(array $liczby) {
   $nwdAlg = function($liczba1, $liczba2) {
      $liczba1 = (int) $liczba1;
      $liczba2 = (int) $liczba2;
      $liczba3 = 1;
      while ($liczba2 != 0) {
         $liczba3 = $liczba1 % $liczba2;
         $liczba1 = $liczba2;
         $liczba2 = $liczba3;
      }
      return $liczba1;
   }; // tutaj koniec zagniezdzonej f. anonimowej
   $liczbaElementow = count($liczby);
   if ($liczbaElementow < 2) throw new Exception('Podaj conajmniej 2 liczby');
   $nwd = $nwdAlg($liczby[0],$liczby[1]);
      if ($liczbaElementow > 2) {
         for ($i = 2 ; $i < $liczbaElementow ; $i++) {
            // rekurencyjne wywolanie funkcji $nwdAlg            
            $nwd = $nwdAlg($nwd,$liczby[$i]);
         }
      }
      return $nwd;
});

Jak tego ustroj­stwa używać? Np. tak:

echo $liczydlo->rozwiaz('nwd 6,9,90');
echo $liczydlo->rozwiaz('mediana 1,3,4,5,6');

Otrzy­mu­jemy wyniki zgodne z prze­wi­dy­wa­niami: 34.

Na koniec wnioski:

  • możemy spraw­dzić czy zmienna nadaje się do wywo­ła­nia za pomocą funk­cji is_callable()
  • jedna funk­cja anoni­mowa może zawie­rać inne funk­cje anonimowe
  • aby dostar­czyć coś spoza zasięgu funk­cji używamy use()

Zaża­le­nia lub prośby o dodat­kowe wyja­śnie­nia pisać w komen­ta­rzach. Klasę pisa­łem w ramach relaksu niedziel­nego, to mogą poja­wić się jakieś błędy.

P.S.: Sposób licze­nia NWD podpie­przy­łem z jakie­goś forum dla gimna­zja­li­stów :-) (tak to jest jak się śpi na matematyce)

Podobne wpisy:

  1. Wygrze­bane z GitHuba (6) : Monolog
  2. 10 trików w Smarty
  3. Wygrze­bane z GitHuba (4) : PHP User Agent
  4. 3 wzorce projek­towe w ok. 100 liniach kodu PHP

7 Comments

  • jarek_bolo
    15 marca 2011 - 07:40 | Permalink

    Hmm…

    Niby kumam o co chodzi, ale wydaje mi się, że to wszystko co zrobi­łeś można by według mnie wyko­nać z więk­szą czytel­no­ścią dekla­ru­jąc normalne metody w Klasie Liczy­dlo i stosu­jąc Reflection.

    Ale może ja czegoś nie kumam jednak, coś pomijam.

    Przy­dało by się spraw­dzić czy anoni­mowe może są szyb­sze od Reflectiona.

  • 15 marca 2011 - 07:57 | Permalink

    W tym przy­padku lepiej nadałby się nam wzorzec „Stra­te­gia”, a nie domknię­cia :). Domknię­cia świet­nie się spraw­dzają tam, gdzie istotne jest dla nas posia­da­nie dostępu do kontek­stu, w którym są one dekla­ro­wane. Drugą prze­słankę dobrze ilustruje mecha­nizm do progra­mo­wa­nia zdarze­nio­wego, gdzie możli­wych reak­cji na zdarze­nia może być stosun­kowo dużo. Użycie klasycz­nej obiek­tówki spra­wi­łoby, że musie­li­by­śmy napi­sać kilka­dzie­siąt klas tylko po to, by wyko­nać jedną, dwie komendy.

    Jeśli chodzi o sam kod, to jeśli korzy­stasz z is_callable(), powi­nie­neś używać też call_user_func() do wywo­ły­wa­nia funk­cji, bowiem is_callable() zwróci true także dla „starych” warto­ści call­back. Rzeczą, która komplet­nie woła o pomstę do nieba, jest kodo­wa­nie liczb w postaci ciągu teksto­wego, zamiast prze­ka­zy­wać go po ludzku w tablicy. Skąd u ludzi takie zapędy masochistyczne?

  • Śpiechu
    15 marca 2011 - 07:59 | Permalink

    @jarek_bolo
    Najwięk­szą czytel­ność dałoby się uzyskać tworząc inter­fejs Algorytm z metodą rozwiaz() i imple­men­to­wać każdy z algo­ryt­mów w osob­nych klasach imple­men­tu­ją­cych inter­fejs. Wtedy mieli­by­śmy do czynie­nia z wzor­cem Stra­te­gia w czystej postaci. Liczydlo dele­guje rozwią­za­nie zada­nia i tyle.
    Tutaj jest bardziej zagma­twane. Z kolei nie wiem co jest szyb­sze, bo przy tworze­niu każdego domknię­cia tworzony jest niejaw­nie obiekt Closure.
    Co do reflek­sji to… nie wiem :-)

  • 15 marca 2011 - 10:28 | Permalink

    Gene­ral­nie wzorzec stra­te­gia, jest tak naprawdę obej­ściem ogra­ni­cze­nia języ­ków takich jak Java, gdzie funk­cja nie jest „first class object”. Co do kodu, to ja zamiast metody setAl­go­rytm, w meto­dzie „rozwiąż” dodał­bym, drugi argu­ment, którym byłby algo­rytm. Wtedy jest to bardziej „real life”. W końcu mówi się uczniowi „rozwiąż zada­nie takim sposo­bem” a nie „uczniu, masz sposób, uczniu, rozwiąż zadanie” ;)

  • 16 marca 2011 - 02:17 | Permalink

    Nie jest żadnym obej­ściem, choćby z tego powodu że wymu­sza imple­men­ta­cję okre­ślo­nego inter­fejsu, zatem kod wywo­łu­jący wie, czego się można po takiej stra­te­gii spodzie­wać. Ponadto jest też kwestia czytel­no­ści kodu. Zauważ, że już nawet w tak prostym przy­kła­dzie Śpie­chu robi sobie domknię­cie w domknię­ciu (a właści­wie to funk­cję anoni­mową). A co, jeśli algo­rytm się jesz­cze bardziej skom­pli­kuje? Przy­pusz­czam, że progra­mi­sta się w nim zwyczaj­nie pogubi, bo wszystko jest „anoni­mowe” i jesz­cze ma jakieś shizo­wate zależ­no­ści między kontek­stami poszcze­gól­nych wywołań :).

  • Śpiechu
    16 marca 2011 - 08:20 | Permalink

    @sokzzuka
    To by było dobre dla gimna­zja­li­stów :-) „Masz podaną tablicę liczb, napisz algo­rytm, za pomocą którego poli­czysz sumę, mini­mum, maksimum…”

    @Zyx
    Wpadł mi do głowy jesz­cze bardziej szalony przy­kład zagma­twa­nia sumy:

    function(array $liczby) {
       $wynik = 0;
       $dodaj = function($liczba) use (&amp; $wynik) {
          $wynik += $liczba;
       };
       foreach ($liczby as $liczba) {
          $dodaj($liczba);
       }
       return $wynik;
    };

    A więc wyko­rzy­sta­nie refe­ren­cji w use(). Czy da się tak prostą rzecz jesz­cze bardziej skom­pli­ko­wać?
    Trzeba przy­znać, że domknię­cia TROCHĘ lubią pokom­pli­ko­wać kod…

  • 16 marca 2011 - 09:47 | Permalink

    @zyx: cytat z wiki­pe­dii (http://en.wikipedia.org/wiki/Strategy_pattern)

    …The essen­tial requ­ire­ment in the program­ming langu­age is the ability to store a refe­rence to some code in a data struc­ture and retrieve it. This can be achie­ved by mecha­ni­sms such as the native func­tion poin­ter, the first-class func­tion, clas­ses or class instan­ces in object-oriented program­ming langu­ages, or acces­sing the langu­age implementation’s inter­nal storage of code via reflection…

    Czyli imple­men­ta­cja stra­tegy pattern może być poprzez obiekt, a może poprzez „first class func­tion” jaką jest funk­cja anoni­mowa lub domknię­cie. Oczy­wi­ście stoso­wa­nie inter­fejsu spra­wia, że kod jest samo doku­men­tu­jący się, nato­miast nie jest koniecz­nie potrzebne. Nato­miast co do kodu Śpie­cha, to mam wraże­nie, że wybrał trochę zły przykład.

    Lepszym ale bardziej złożo­nym, było by np. wyszu­ki­wa­nie drogi w Labi­ryn­cie. Mamy obiekt „labi­rynt”, który ma w sobie mapę i punkt star­towy oraz końcowy oraz metodę „wyznacz­Trasę”, której argu­men­tem jest funk­cja anoni­mowa lub jakiś obiekt imple­men­tu­jący odpo­wiedni inter­fejs. Algo­ryt­mów wyszu­ki­wa­nia drogi w labi­ryn­cie jest masa i różnią się znacz­nie, także było by to bardziej adekwatne.

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.

    Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <p> <pre lang="" line="" escaped=""> <q cite=""> <strike> <strong>