Jekyll2019-06-21T14:08:17+00:00http://psliwa.org/feed.xmlBrudnopis programistyczyli zapiski o programowaniupsliwaProperty based testing - wprowadzenie2019-06-21T00:00:00+00:002019-06-21T00:00:00+00:00http://psliwa.org/property-based-testing-wprowadzenie<p>Ostatnio prowadziłem krótki warsztat na temat property based testing na
przykładzie <a href="https://www.scalacheck.org/">scalachecka</a>. W tym wpisie postaram się przedstawić ideę
jaka stoi za tym sposobem testowania. Kod źródłowy ze wspomnianego
warsztatu jest dostępny na <a href="https://github.com/psliwa/scalacheck-workshop">githubie</a>. Jest tam zalążek projektu,
który się kompiluje, a na branchu <code class="highlighter-rouge">solutions</code> są rozwiązania do ćwiczeń.</p>
<p>Klasyczne podejście do testowania to testy w postaci “Given / When /
Then”. Sekcja “Given” polega na zdefiniowaniu stanu początkowego
środowiska wokół tego co testujemy i utworzenie samej jednostki do
testowania. Następnie w “When” wywołujemy metodę / funkcję którą chcemy
przetestować przekazując określone dane wejściowe i w “Then” sprawdzamy
czy zadziało się to, czego oczekujemy. Dane wejściowe są wpisane na
sztywno - w teście podajemy przykładowe dane. Stąd też potoczna nazwa
“example based testing”, gdyż w tych testach wypisujemy przykładowe
scenariusze z konkretnymi danymi. Czasami dwa scenariusze różnią się od
siebie tylko danymi testowymi, prowadzi to do stosowania
parametryzowanych testów (w <a href="https://github.com/junit-team/junit4/wiki/parameterized-tests">junit</a>, w <a href="https://phpunit.readthedocs.io/en/8.0/writing-tests-for-phpunit.html#data-providers">phpunit</a> itp.). Co by się
stało, jeśli wszystkie testy były z definicji parametryzowane, a w
dodatku parametry byłyby wygenerowane przez silnik testowy? Doszlibyśmy
do “property based testing”. Wtedy nie mielibyśmy przykładowych
konkretnych scenariuszy, tylko “właściwość” naszego kodu.</p>
<p>Przykład testu example based:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">test</span><span class="o">(</span><span class="s">"map list values"</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">List</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">3</span><span class="o">).</span><span class="n">map</span><span class="o">(</span><span class="k">_</span><span class="o">.</span><span class="n">toString</span><span class="o">)</span> <span class="n">shouldBe</span> <span class="nc">List</span><span class="o">(</span><span class="s">"1"</span><span class="o">,</span> <span class="s">"2"</span><span class="o">,</span> <span class="s">"3"</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p>W jaki sposób zapisać taki test używając podejście property based?</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">test</span><span class="o">(</span><span class="s">"map list values"</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// forAll to funkcja, która mówi, że "dla każdego zestawu argumentów podana właściwość
</span> <span class="c1">// zachodzi". Można przyjąć, że jest ona równoznaczna z kwantyfikatorem "dla każdego"
</span> <span class="c1">// w matematyce, tak więc test zapisany w taki sposób jest w pewnym sensie twierdzeniem
</span> <span class="c1">// matematycznym.
</span> <span class="n">forAll</span> <span class="o">{</span> <span class="o">(</span><span class="n">as</span><span class="k">:</span> <span class="kt">List</span><span class="o">[</span><span class="kt">Int</span><span class="o">],</span> <span class="n">f</span><span class="k">:</span> <span class="kt">Int</span> <span class="o">=></span> <span class="nc">String</span><span class="o">)</span> <span class="k">=></span>
<span class="n">as</span><span class="o">.</span><span class="n">map</span><span class="o">(</span><span class="n">f</span><span class="o">)</span> <span class="o">...</span> <span class="c1">// problem, bo rzuca się w oczy asercja typu as.map(f) shouldBe as.map(f),
</span> <span class="c1">// która nie ma sensu i nic nie sprawdza
</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Jest to probematyczne, gdyż musimy zmienić lekko sposób myślenia o
teście. Musimy się zastanowić jakie właściwości ma operacja <code class="highlighter-rouge">map</code>,
zamiast podawać już konkretne, skomplikowane scenariusze, które mogą
powodować, że tak na prawdę powtórzymy w teście kod produkcyjny lub kod
testowy. Np. możemy zauważyć, że dla każdej listy wywołanie
<code class="highlighter-rouge">as.map(identity)</code> powinno dać tą samą listę (<code class="highlighter-rouge">identity</code> to funkcja
jednoargumentowa zwracająca argument bez modyfikacji). Zapiszmy to:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">test</span><span class="o">(</span><span class="s">"map list values using identity function should preserve original list"</span><span class="o">)</span> <span class="o">{</span>
<span class="n">forAll</span> <span class="o">{</span> <span class="n">as</span><span class="k">:</span> <span class="kt">List</span><span class="o">[</span><span class="kt">Int</span><span class="o">]</span> <span class="k">=></span>
<span class="n">as</span><span class="o">.</span><span class="n">map</span><span class="o">(</span><span class="n">identity</span><span class="o">)</span> <span class="n">shouldBe</span> <span class="n">as</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Mamy pierwszą właściwość operacji <code class="highlighter-rouge">map</code>. Możemy się dalej zastananawiać,
jakie właściwości ma operacja <code class="highlighter-rouge">map</code>. Inną właściwością może być to, że
jeśli mamy wartość <code class="highlighter-rouge">a</code> i utworzymy z niej listę i wywołamy <code class="highlighter-rouge">map(f)</code>, to
dostaniemy dokładnie <code class="highlighter-rouge">List(f(a))</code>. Zapiszmy więc to:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">test</span><span class="o">(</span><span class="s">"map over single value"</span><span class="o">)</span> <span class="o">{</span>
<span class="n">forAll</span> <span class="o">{</span> <span class="o">(</span><span class="n">a</span><span class="k">:</span> <span class="kt">Int</span><span class="o">,</span> <span class="n">f</span><span class="k">:</span> <span class="kt">Int</span> <span class="o">=></span> <span class="nc">String</span><span class="o">)</span> <span class="k">=></span>
<span class="nc">List</span><span class="o">(</span><span class="n">a</span><span class="o">).</span><span class="n">map</span><span class="o">(</span><span class="n">f</span><span class="o">)</span> <span class="n">shouldBe</span> <span class="nc">List</span><span class="o">(</span><span class="n">f</span><span class="o">(</span><span class="n">a</span><span class="o">))</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ten test w sumie wygląda podobnie jak pierwszy scenariusz “example
based”. Możemy jeszcze pogłówkować dalej i spróbować wymyśleć jakąś inną
właściwość <code class="highlighter-rouge">map</code>. Co jeśli mamy dwie funkcje: <code class="highlighter-rouge">f</code> i <code class="highlighter-rouge">g</code>? Czy <code class="highlighter-rouge">map</code>
wspiera komponowanie funkcji? Sprawdźmy to:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">test</span><span class="o">(</span><span class="s">"map is composable"</span><span class="o">)</span> <span class="o">{</span>
<span class="n">forAll</span> <span class="o">{</span> <span class="o">(</span><span class="n">as</span><span class="k">:</span> <span class="kt">List</span><span class="o">[</span><span class="kt">Int</span><span class="o">],</span> <span class="n">f</span><span class="k">:</span> <span class="kt">Int</span> <span class="o">=></span> <span class="nc">String</span><span class="o">,</span> <span class="n">g</span><span class="k">:</span> <span class="kt">String</span> <span class="o">=></span> <span class="nc">Int</span><span class="o">)</span> <span class="k">=></span>
<span class="n">as</span><span class="o">.</span><span class="n">map</span><span class="o">(</span><span class="n">a</span> <span class="k">=></span> <span class="n">g</span><span class="o">(</span><span class="n">f</span><span class="o">(</span><span class="n">a</span><span class="o">)))</span> <span class="n">shouldBe</span> <span class="n">as</span><span class="o">.</span><span class="n">map</span><span class="o">(</span><span class="n">f</span><span class="o">).</span><span class="n">map</span><span class="o">(</span><span class="n">g</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Wychodzi na to, że tak. Tak więc zdefiniowaliśmy 3 właściwości operacji
<code class="highlighter-rouge">map</code>. Tak na prawdę <code class="highlighter-rouge">map</code> ma <a href="https://wiki.haskell.org/Functor#Description">2 podstawowe prawa (właściwości)</a> - w
naszym przypadku pierwsza i trzecia właściwość. Druga właściwość wynika
z trzeciej. Zadanie domowe, aby uzasadnić w jaki sposób ;)</p>
<p>Jak widzimy, nasze testy mają całkowicie inną postać niż w przypadku
“example based”. Ważną rzeczą jest nie uogólnianie testów “example
based” tylko spojrzenie na funkcję z dalszej perspektywy: jakie operacja
faktycznie ma właściwości. Innym ważnym czynnikiem jest, aby właściwości
były możliwe jak naprostsze, gdyż z natury mogą one być abstrakcyjne.
Jeśli dodatkowo będą skomplikowane, to taki kod nie będzie czytelny i za
pół roku czytając taką skomplikowaną właściwość będziesz się drapał po
głowie myśląc “co autor miał na myśli”. Zaletą takiego podejścia jest
testowanie wielu corner casów za darmo, gdyż tym zajmuje się silnik do
generowania danych.</p>
<p>Skąd się biorą wartości na których operujemy w teście? Są one
wygenerowane przez silnik <a href="https://www.scalacheck.org/">scalacheck</a>. Dba on o to, aby wygenerować
wartości specjalne (np. pusta lista, lista jedno elementowa, zero, min
int, max int itp.) i wartości nie specjalne. Za każdym uruchomieniem
zestaw wartości może być różny, gdyż są one dobierane w sposób pseudo
losowy. Przykład z listą bazuje na podstawowych typach, więc scalacheck
jest w stanie wygenerować nam odpowiednie wartości: listy, funkcje, czy
pojedyncze wartości. Co jeśli mamy własne typy, które byśmy chcieli
przetestować? Musimy napisać własne generatory, o których napiszę w
kolejnym wpisie za jakiś czas :)</p>
<p>Chciałbym też zwrócić uwagę, że “property based testing” to nie święty
graal testowania. Takiego podejścia nie używa się wszędzie, tylko w
uzasadnionych przypadkach. Osobiście w większości przypadków stosuję
klasyczne podejście. Jednak gdy widzę, że property based testing by
przyniósł wartość przy testowaniu konkretnej funkcji, to używam go mając
również jeden / dwa scenariusze “example based”.</p>psliwaOstatnio prowadziłem krótki warsztat na temat property based testing na przykładzie scalachecka. W tym wpisie postaram się przedstawić ideę jaka stoi za tym sposobem testowania. Kod źródłowy ze wspomnianego warsztatu jest dostępny na githubie. Jest tam zalążek projektu, który się kompiluje, a na branchu solutions są rozwiązania do ćwiczeń.TimeZone.getDefault() - dwa słowa o niespodziankach2017-03-29T00:00:00+00:002017-03-29T00:00:00+00:00http://psliwa.org/timezone-getdefault-dwa-slowa-o-niespodziankach-2<p>Ostatnio przygotowywałem pull requesta dla biblioteki <a href="https://github.com/typesafehub/config">typesafe/config</a> - prosty ficzer. Okazało się, że po moich zmianach build na windowsach przestał przechodzić. Po czasochłonnej inwestygacji, debugowaniu i namierzaniu problemu, okazało się, że winowajcą jest tytułowy <code class="highlighter-rouge">TimeZone.getDefault()</code>. Gdy to odkryłem, uświadomiłem sobie dlaczego tak bardzo doceniłem niemodyfikowalne obiekty, brak side effectów i czyste funkcje.</p>
<p>W czym był problem? Pliki <code class="highlighter-rouge">HOCON</code> (format plików konfiguracyjnych wspierany przez tą bibliotekę) mogą includować inne pliki konfiguracyjne. Do odczytywania takich plików używana jest klasa <code class="highlighter-rouge">URLConnection</code>. W implementacji tej biblioteki wywoływana jest funkcja <code class="highlighter-rouge">URLConnection.getContentType()</code> aby sprawdzić jakiego typu plik jest wczytywany. Metoda ta pobiera nagłówki połączenia, w którym jest data ostatniej modyfikacji. Jeśli jest data, to trzeba ją sformatować. Klasa <code class="highlighter-rouge">SimpleDateFormat</code> używa domyślnej strefy czasowej, chyba że powiemy jej inaczej. <code class="highlighter-rouge">TimeZone.getDefault()</code>, wbrew sugestywnej nazwie i zasadzie najmniejszego zaskoczenia, ma side effect, a jak! Jak to, dlaczego? Bo może! Wywołuje bowiem <code class="highlighter-rouge">System.setProperty("user.timezone", <strefa czasowa>)</code> jeśli takie property nie jest jeszcze ustawione. Pech w tym, że typesafe/config wczytuje java propertisy robiąc w testach asercje na tychże wartościach. W zależności czy <code class="highlighter-rouge">TimeZone.getDefault()</code> było wcześniej wywołane i czy <code class="highlighter-rouge">user.timezone</code> było jawnie ustawione, testy mogły failować lub też nie (co też miało miejsce).</p>psliwaOstatnio przygotowywałem pull requesta dla biblioteki typesafe/config - prosty ficzer. Okazało się, że po moich zmianach build na windowsach przestał przechodzić. Po czasochłonnej inwestygacji, debugowaniu i namierzaniu problemu, okazało się, że winowajcą jest tytułowy TimeZone.getDefault(). Gdy to odkryłem, uświadomiłem sobie dlaczego tak bardzo doceniłem niemodyfikowalne obiekty, brak side effectów i czyste funkcje.Przetrwać chaos - ChaosJournal w Akka Persistence2017-03-06T00:00:00+00:002017-03-06T00:00:00+00:00http://psliwa.org/przetrwac-chaos-chaosjournal-w-akka-persistence<p>Przeglądając kod źródłowy akka-persistence, natknąłem się na bardzo ciekawy sposób testowania odporności na błędy <a href="https://github.com/akka/akka/blob/master/akka-persistence/src/test/scala/akka/persistence/AtLeastOnceDeliveryFailureSpec.scala">persystencji</a>. W tym teście użyty jest <a href="https://github.com/akka/akka/blob/master/akka-persistence/src/test/scala/akka/persistence/journal/chaos/ChaosJournal.scala">ChaosJournal</a> - implementacja journala losowo rzucająca wyjątkami przy odczycie lub zapisie danych. “<em>Właśnie tego szukałem!</em>” - pomyślałem i zacząłem się bawić nową zabawką.</p>
<p>Nie bez przyczyny zacząłem owe poszukiwania - ostatnio pracowałem nad poprawieniem odporności naszej aplikacji na błędy persystencji - niektóre typy wiadomości nie mogą być gubione, przez co trzeba było wprowadzić <code class="highlighter-rouge">AtLeastOnceDelivery</code>, idempotentne reagowanie na zduplikowane wiadomości i inne bajery. Przyszedł czas na przetestowanie szeregu wprowadzonych zmian. Wybór padł na wykorzystanie istniejących już testów E2E z <code class="highlighter-rouge">ChaosJournalem</code>. Po zwiększeniu <code class="highlighter-rouge">akka.test.timefactor</code>, przekonfigurowaniu circuit breakerów, backoff supervisorów, przeróżnych timeoutów, przekonfigurowaniu <code class="highlighter-rouge">ChaosJournala</code>, kilku zmianach w testach E2E i poprawieniu kilku znalezionych błędów, testy zaczęły stabilnie przechodzić.</p>
<p>Trzeba było wprowadzić kilka zmian w testach E2E aby współpracowały one z <code class="highlighter-rouge">ChaosJournalem</code>, m. in.:</p>
<ul>
<li>wiadomości, które mogą być zgubione, muszą być periodycznie powtarzane, a logika aplikacji powinna oczywiście obsługiwać ich duplikaty</li>
<li>instruując aplikację poprzez wysłanie wiadomości przez api, należy powtarzać wysłanie gdy dostanie się błąd <code class="highlighter-rouge">50x</code> (błąd rzucany w przypadku błędu persystencji)</li>
</ul>
<p>Innym pomysłem na testowanie Akkowej aplikacji pod kątem odporności na gubienie wiadomości jest customowa implementacja Mailboxa - gubienie określonego procenta wiadomości. Zaleta jest taka, że tutaj możemy skonfigurować Mailboxa per aktor i przetestować jak aplikacja się zachowuje gdy jeden konkretny aktor gubi wiadomości.</p>
<p>Potężniejszym, bardziej generycznym, ale i trudniejszym do zaimplementowania rozwiązaniem jest użycie np. <a href="https://github.com/carlpulley/docker-compose-testkit">docker-compose-testkit</a>. Minusem tego rozwiązania jest to, iż trzeba pisać nowy rodzaj testów specjalnie pod kątem odporności na błędy, a sam sposób pisania takich testów nie przemawia do mnie - wygląda na zbyt złożony. W rozwiązaniu z <code class="highlighter-rouge">ChaosJournalem</code> / <code class="highlighter-rouge">ChaosMailboxem</code> nic nie stoi na przeszkodzie, aby wykorzystać już istniejące i działające testy integracyjne lub e2e (lekki lifting może być wskazany) - dzięki temu jest mniej kodu. A jak dobrze wiemy, najlepszy i najczystszy kod to ten, który nie istnieje. Prezentację na temat <code class="highlighter-rouge">docker-compose-testkit</code> oraz o testowaniu odporności na błędy możecie znaleźć na <a href="https://github.com/carlpulley/docker-compose-testkit/tree/scala-sphere-2017/docs/Scala%20Sphere%202017">githubie</a>.</p>
<p>Za niedługo prawdopodobnie zrobię PR do Akka Persistence, który udostępnia <code class="highlighter-rouge">ChaosJournal</code> jako plugin do persystencji. Jak na razie klasa ta jest tylko w źródłach testowych, można jej użyć stosując znany i lubiany wzorzec projektowy o nazwie “copy & paste”. Należy pamiętać, że domyślna implementacja <code class="highlighter-rouge">ChaosJournala</code> ma globalny storage, więc każda instancja tej klasy współdzieli ten sam stan. Jeśli nie tego oczekujemy, można to zachowanie łatwo zmienić. Innym problemem jest to, iż ta implementacja nie wspiera <a href="http://doc.akka.io/docs/akka/current/scala/persistence-query.html">Persistence Query</a>.</p>psliwaPrzeglądając kod źródłowy akka-persistence, natknąłem się na bardzo ciekawy sposób testowania odporności na błędy persystencji. W tym teście użyty jest ChaosJournal - implementacja journala losowo rzucająca wyjątkami przy odczycie lub zapisie danych. “Właśnie tego szukałem!” - pomyślałem i zacząłem się bawić nową zabawką.Side effecty, a Mock2016-03-25T00:00:00+00:002016-03-25T00:00:00+00:00http://psliwa.org/side-effecty-a-mock<p>Jakiś czas temu rozpisywałem się o <a href="http://psliwa.org/nie-mockuj-tak-czyli-o-naduzywaniu-mockow-w-testach/">mockach</a>. Teraz pora na krótką opowiastkę o tym samym, ale z innej perspektywy.</p>
<p>Przez ostatni rok piszę w scali, nie napisałem w niej ani jednego mocka (nie licząc <code class="highlighter-rouge">TestProbe</code> z Akki). Niedawno też wróciłem popisać sobie w javie, praktycznie od razu gdy miałem zamiar pisać test jednostkowy, chciałem użyć mocka. Dlaczego? Odpowiedź jest prosta i po części znajduje się w przytoczonym już <a href="http://psliwa.org/nie-mockuj-tak-czyli-o-naduzywaniu-mockow-w-testach/">wpisie</a>. Scala to funkcyjno-obiektowy język, duży nacisk jest kładziony na <strong>niemodyfikowalność</strong> i <strong>brak efektów ubocznych</strong>, język do tego zachęca. Jeśli nie ma side effectów (wywołań typu <strong>command</strong>), to po co korzystać z mocków? Nie mają racji bytu. Java nie zachęca tak do niemodyfikowalności i braków side effectów, tak więc w niektórych przypadkach mock jest wskazany.</p>
<p>Wspomniałem, że jedyne mocki, które napisałem w scali, to te testujące aktorów z Akki - to jest naturalne. Akka jest oparta na side effectach, każda wysłana wiadomość do dowolnego aktora jest efektem ubocznym.</p>
<p>Nauka z tego jest taka, że zbiór rozwiązań danych problemów z języka X nie przekłada się na zbiór rozwiązań tych problemów w języku Y. Dlaczego? Bo w języku Y te problemy mogą w ogóle nie istnieć lub mogą istnieć inne narzędzia do ich rozwiązania. Przykładowo wzorce obiektowe w programowaniu funkcyjnym oczywiście nie mają zastosowania, do rozwiązania tych problemów stosuje się funkcje wyższego rzędu (np. zamiast strategii), składanie funkcji (np. zamiast dekoratora), czy innych funkcyjnych konstrukcji językowych (np. pattern matching zamiast visitora).</p>psliwaJakiś czas temu rozpisywałem się o mockach. Teraz pora na krótką opowiastkę o tym samym, ale z innej perspektywy.Bądź leniwy, a nie chciwy - słów kilka o leniwej ewaluacji2015-11-05T00:00:00+00:002015-11-05T00:00:00+00:00http://psliwa.org/leniwa-ewaluacja-wyrazen<p>Ostatnio można było odnieść wrażenie, że tak jak tytuł mówi, jestem leniwy (ostatni wpis na początku roku), ale nic bardziej mylnego. Już zabieram się za temat leniwej ewaluacji. Wyjdę od Scali, a później przejdę do PHP.</p>
<h2 id="leniwa-ewaluacja-na-przykładzie-scali">Leniwa ewaluacja na przykładzie Scali</h2>
<p>Są dwa sposoby ewaluacji wyrażeń: chciwe (strict) oraz leniwe (lazy). Scala jest językiem, który domyślnie wyrażenia ewaluuje chciwie (tak jak większość języków), ale gdy tego chcemy, możemy oznaczyć aby dane wyrażenie było leniwe.</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">lazy</span> <span class="k">val</span> <span class="n">a</span> <span class="k">=</span> <span class="mi">1</span> <span class="o">+</span> <span class="mi">2</span>
<span class="k">lazy</span> <span class="k">val</span> <span class="n">b</span> <span class="k">=</span> <span class="n">a</span> <span class="o">+</span> <span class="mi">3</span>
<span class="k">val</span> <span class="n">c</span> <span class="k">=</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span> <span class="c1">// obliczenie wartości a i b odbywa się dopiero tutaj
</span> <span class="c1">// a nie w momencie inicjalizacji zmiennej
</span></code></pre></div></div>
<p>Listy i większość innych kolekcji w scali są chciwe, jedną z leniwych struktur jest Stream - jest to leniwa wersja listy. Przykład strumienia będącego nieskończonym ciągiem liczb naturalnych:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">val</span> <span class="n">naturals</span><span class="k">:</span> <span class="kt">Stream</span><span class="o">[</span><span class="kt">Int</span><span class="o">]</span> <span class="k">=</span> <span class="mi">0</span> <span class="o">#::</span> <span class="n">naturals</span><span class="o">.</span><span class="n">map</span><span class="o">(</span><span class="k">_</span> <span class="o">+</span> <span class="mi">1</span><span class="o">)</span>
<span class="c1">//res0: Stream[Int] = Stream(0, ?)
</span></code></pre></div></div>
<p>Powyższy zapis jest skróconym zapisem:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">val</span> <span class="n">naturals</span><span class="k">:</span> <span class="kt">Stream</span><span class="o">[</span><span class="kt">Int</span><span class="o">]</span> <span class="k">=</span> <span class="nc">Stream</span><span class="o">.</span><span class="n">cons</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="n">naturals</span><span class="o">.</span><span class="n">map</span><span class="o">(</span><span class="k">_</span> <span class="o">+</span> <span class="mi">1</span><span class="o">))</span>
</code></pre></div></div>
<p>Poniżej dwa akapity wyjaśnień, bo mimo iż zapis ten jest prosty, naturalny i dosyć matematyczny, mózg może się przed nim lekko opierać.</p>
<p>Co to jest rekurencyjna struktura danych? Wyjaśnię to na przykładzie. <a href="https://pl.wikipedia.org/wiki/Matrioszka">Matrioszka</a> to klasyczna rosyjska zabawka składająca się z <code class="highlighter-rouge">X</code> lalek. <code class="highlighter-rouge">X - 1</code> lalek jest otwieranych i może zawierać w sobie kolejną (mniejszą) lalkę. Ostatnia lalka nie jest otwierana i nie można już do niej nic wsadzić. Tak więc pierwsza lalka zawiera drugą, która zawiera trzecią i tak do ostatniej, która nie zawiera już kolejnej lalki. Każda z lalek ma ten sam “interfejs”, wygląda podobnie. Ostatnia lalka różni się od reszty tym, że nie zawiera kolejnej. Podobnie jest ze strukturami rekurencyjnymi. W przypadku strumieni, “lalkę” mogąca zawierać inną “lalkę” nazywamy <code class="highlighter-rouge">Cons</code>, zaś ostatnią pustą “lalkę” <code class="highlighter-rouge">Empty</code>. <code class="highlighter-rouge">Cons</code> jest węzłem, który ma wartość i referencję do pozostałej części strumienia (ogona). <code class="highlighter-rouge">Empty</code> to pusty strumień. Konstruktor <code class="highlighter-rouge">Cons</code> wygląda następująco: <code class="highlighter-rouge">Cons(value, Stream)</code> - ma referencję do przechowywanej wartości oraz dalszej części strumienia. Dalsza część strumienia może być <code class="highlighter-rouge">Cons</code> lub <code class="highlighter-rouge">Empty</code> (otwierana lub ostatnia pusta “lalka”). Przykładowy strumień złożony z 2 elementów: <code class="highlighter-rouge">Stream.cons(0, Stream.cons(1, Stream.Empty))</code> lub składnia skrócona: <code class="highlighter-rouge">0 #:: 1</code>. Jako referencji do całego strumienia używamy referencję do jego pierwszego elementu - tak samo jak w przypadku matrioszki, pierwsza lalka jest lalką samą w sobie, nie jest potrzebne dodatkowe opakowanie tak jak w przypadku klasycznej listy powiązanej.</p>
<p>Wracając do naszego ciągu liczb naturalnych (ignorujemy to że może się przekręcić po dojściu do max int). <code class="highlighter-rouge">0</code> to pierwszy element strumienia, <code class="highlighter-rouge">naturals.map(_ + 1)</code> to strumień będący dalszą częścią głównego strumienia (drugim argumentem konstruktora <code class="highlighter-rouge">Cons</code>). Bardzo ważne jest to, że <code class="highlighter-rouge">naturals.map(_ + 1)</code> jest <strong>ewaluowane leniwie</strong>, czyli wartość drugiego elementu zostanie wyliczona tylko gdy będziemy chcieli go odczytać. Zapis może na początku lekko kołować, ale wystarczy sobie uświadomić, że jest to rekurencyjna definicja równoznaczna z rekurencyjnym wywołaniem funkcji:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">createNaturals</span><span class="o">(</span><span class="n">from</span><span class="k">:</span> <span class="kt">Int</span><span class="o">)</span><span class="k">:</span> <span class="kt">Stream</span><span class="o">[</span><span class="kt">Int</span><span class="o">]</span> <span class="k">=</span> <span class="n">from</span> <span class="o">#::</span> <span class="n">createNaturals</span><span class="o">(</span><span class="n">from</span><span class="o">+</span><span class="mi">1</span><span class="o">)</span>
<span class="k">val</span> <span class="n">naturals</span> <span class="k">=</span> <span class="n">createNaturals</span><span class="o">(</span><span class="mi">0</span><span class="o">)</span>
</code></pre></div></div>
<p>Jeśli dwa poprzednie akapity nie są jasne, przeczytaj je jeszcze raz (rekurencyjnie, aż do spełnienia warunku wyjścia) ;)</p>
<p>Wyliczenie wartości następuje tylko gdy chcemy tą wartość odczytać:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">naturals</span><span class="o">(</span><span class="mi">5</span><span class="o">)</span> <span class="c1">//pobranie 5-tego elementu ciągu
</span> <span class="c1">//następuje ewaluacja ciągu do 5-tego
</span> <span class="c1">//elementu włącznie, 6-ty element nie jest ewaluowany
//res1: Int = 5
</span></code></pre></div></div>
<p>Ok, teraz trochę bardziej złożony przykład - definicja ciągu fibonacciego:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">val</span> <span class="n">fib</span><span class="k">:</span> <span class="kt">Stream</span><span class="o">[</span><span class="kt">Int</span><span class="o">]</span> <span class="k">=</span> <span class="mi">0</span> <span class="o">#::</span> <span class="mi">1</span> <span class="o">#::</span> <span class="n">fib</span><span class="o">.</span><span class="n">tail</span><span class="o">.</span><span class="n">zip</span><span class="o">(</span><span class="n">fib</span><span class="o">).</span><span class="n">map</span><span class="o">{</span> <span class="k">case</span><span class="o">(</span><span class="n">a</span><span class="o">,</span> <span class="n">b</span><span class="o">)</span> <span class="k">=></span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span> <span class="o">}</span>
<span class="c1">//res2: Stream[Int] = Stream(0, ?)
</span></code></pre></div></div>
<p>Fragment <code class="highlighter-rouge">fib.tail.zip(fib)</code> tworzy nowy strumień, którego elementy to pary odpowiadających sobie elementów ze strumieni <code class="highlighter-rouge">fib</code> oraz <code class="highlighter-rouge">fib.tail</code>. <code class="highlighter-rouge">fib.tail</code> to strumień <code class="highlighter-rouge">fib</code> “przesunięty” o jedną pozycję do przodu, dzięki czemu otrzymujemy sumę dwóch kolejnych wyrazów ciągu.</p>
<p>Można się pobawić ciągiem fibonacciego:</p>
<div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fib</span><span class="o">(</span><span class="mi">10</span><span class="o">)</span>
<span class="c1">//res3: Int = 55
</span><span class="n">fib</span><span class="o">.</span><span class="n">take</span><span class="o">(</span><span class="mi">5</span><span class="o">).</span><span class="n">toList</span> <span class="c1">//pobranie 5 pierwszych wyrazów ciągu
</span> <span class="c1">//i skonwertowanie na listę
//res4: List[Int] = List(0, 1, 1, 2, 3)
</span></code></pre></div></div>
<h3 id="zalety-strumieni-i-innych-leniwych-struktur">Zalety strumieni i innych leniwych struktur</h3>
<ol>
<li>
<p><strong>Liczba operacji</strong>
Do wyliczenia ostatecznego wyniku zostanie wykonana <strong>konieczna</strong> liczba operacji, <strong>ale nie więcej</strong> ;) Można operować na dużych lub nieskończonych strumieniach, a na końcu ograniczyć wynik do jakiegoś zakresu elementów lub pobrać jeden konkretny element. Podczas obliczeń nie trzeba się przejmować rozmiarem strumienia.</p>
</li>
<li>
<p><strong>Złożoność pamięciowa</strong>
Wykonując kilka operacji jedna po drugiej, nie powstają strumienie z danymi przejściowymi. Przykładowo wywołanie <code class="highlighter-rouge">Stream(1, 2, 3, 4, 5).map(_+1).filter(_ % 2 == 0).toList</code> powoduje, że najpierw operacje map i filter są wywołane na pierwszym elemencie, następnie na drugim itp. Tak więc <strong>następuje jedna iteracja</strong> po elementach strumienia. W przypadku list jest inaczej, wyniki przejściowe są wyliczane. Dla wyrażenia <code class="highlighter-rouge">List(1, 2, 3, 4, 5).map(_+1).filter(_ % 2 == 0)</code> najpierw na wszystkich elementach jest wywoływana operacja <code class="highlighter-rouge">map</code>, a następnie na wszystkich elementach nowej listy jest wywołana operacja filter. Wydawać by się mogło, że dzięki temu łączenie w łańcuch wywołań na strumieniu zawsze będzie mieć złożoność obliczeniową O(n) - niestety w praktyce tak nie jest. Teoretycznie tak powinno być, ale strumienie są zaimplementowane za pomocą <code class="highlighter-rouge">lazy val</code>, które nie są szczególnie wydajne, gdyż wykorzystują pod maską blok synchronizowany. Przez to <strong>wydajność operacji łączonych na strumieniu jest mniej przewidywalna niż w przypadku listy</strong>, jednakże nadal złożoność <strong>pamięciowa jest wyraźnie lepsza</strong> i cechuje się faktycznie na poziomie <strong>O(n)</strong>. Są także inne leniwe struktury w Scali, np. widoki, czy iteratory, które już nie mają tego problemu. Aby utworzyć leniwy widok dla listy, wystarczy wywołać na niej funkcję <code class="highlighter-rouge">view</code>.</p>
</li>
<li>
<p><strong>Bezpieczna rekurencja</strong>
Jak wiadomo rekurencja może powodować <strong>przepełnienie stosu (stack overflow)</strong>. W językach funkcyjnych rekurencja jest czymś powszednim i nie można sobie pozwolić na stack overflow np. przy operowaniu na liście o długości 2000 elementów. Bardzo wiele kombinatorów jest zaimplementowanych za pomocą rekurencji, co normalnie by powodowało stack overflow przy np. wywołaniu operacji <code class="highlighter-rouge">map</code> na liście z 2000 elementów. Jednakże istnieje optymalizacja kompilatora, która sprawia, że wywołania rekurencyjne nie odkładają się na stosie, jeśli mamy do czynienia z tzw. <strong>tail recursion</strong>. Nie będę się na ten temat więcej rozwodził - zainteresowanym polecam google. Niektóre algorytmy ciężko zapisać za pomocą tail recursion. W przypadku leniwych strumieni rekurencyjna funkcja nie zaimplementowana za pomocą tail recursion, która operuje na tym strumieniu, nie spowoduje stack overflow. <strong>Rekurencyjne funkcje operujące na leniwych kolekcjach są bezpieczne</strong> i nie trzeba ich zapisywać za pomocą tail recursion. W PHP, o którym za chwilę, tak czy siak nie ma optymalizacji tail recursion, a leniwe kolekcje nie uchronią nas przed stack overflow.</p>
</li>
</ol>
<h2 id="leniwy-php">Leniwy PHP</h2>
<p>Zanim przejdziesz dalej, spróbuj zaimplementować analogiczny ciąg liczb naturalnych i ciąg fibonacciego w php.</p>
<p>. <em>przerwa aby uchronić przed spojlerem</em></p>
<p>.</p>
<p>.</p>
<p>.</p>
<p>W PHP 5.5 został wprowadzony nowy ficzer: <strong>generatory</strong>. Generator to nic innego jak iterator stworzony za pomocą wygodnej składni i z leniwie wyliczanym kolejnym elementem.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="nf">naturals</span><span class="p">()</span> <span class="p">{</span>
<span class="nv">$i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">while</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="k">yield</span> <span class="nv">$i</span><span class="o">++</span><span class="p">;</span>
<span class="p">}</span>
<span class="nv">$naturals</span> <span class="o">=</span> <span class="nx">naturals</span><span class="p">();</span><span class="c1">//nieskończony ciąg liczb
</span> <span class="c1">//naturalnych (może się przekręcić ;))
</span></code></pre></div></div>
<p>Słowo kluczowe <code class="highlighter-rouge">yield</code> generuje jeden element kolekcji, drugi zostanie wygenerowany tylko wtedy gdy jawnie uzyska się do niego dostęp. W PHP 7 generatory zostały ulepszone i generator może korzystać z innego generatora w celu dostarczenia kolejnych wartości. Służy do tego składnia <code class="highlighter-rouge">yield from $generator</code> (patrz poniższa definicja funkcji <code class="highlighter-rouge">zip</code>).</p>
<p>Od razu zdefiniuję kilka leniwych kombinatorów, które będą przydatne.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="nf">map</span><span class="p">(</span><span class="nx">callable</span> <span class="nv">$f</span><span class="p">,</span> <span class="nv">$coll</span><span class="p">)</span> <span class="p">{</span>
<span class="k">foreach</span><span class="p">(</span><span class="nv">$coll</span> <span class="k">as</span> <span class="nv">$element</span><span class="p">)</span> <span class="p">{</span>
<span class="k">yield</span> <span class="nv">$f</span><span class="p">(</span><span class="nv">$element</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">//pobiera $count pierwszych elementów
</span><span class="k">function</span> <span class="nf">take</span><span class="p">(</span><span class="nv">$count</span><span class="p">,</span> <span class="nv">$coll</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">foreach</span><span class="p">(</span><span class="nv">$coll</span> <span class="k">as</span> <span class="nv">$element</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="nv">$i</span><span class="o">++</span> <span class="o">>=</span> <span class="nv">$count</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span>
<span class="k">yield</span> <span class="nv">$element</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">//porzuca $count pierwszych elementów
</span><span class="k">function</span> <span class="nf">drop</span><span class="p">(</span><span class="nv">$count</span><span class="p">,</span> <span class="nv">$coll</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">foreach</span><span class="p">(</span><span class="nv">$coll</span> <span class="k">as</span> <span class="nv">$element</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="nv">$i</span><span class="o">++</span> <span class="o">>=</span> <span class="nv">$count</span><span class="p">)</span> <span class="k">yield</span> <span class="nv">$element</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">//pobranie $n-tego elementu
</span><span class="k">function</span> <span class="nf">get</span><span class="p">(</span><span class="nv">$n</span><span class="p">,</span> <span class="nv">$coll</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">foreach</span><span class="p">(</span><span class="nv">$coll</span> <span class="k">as</span> <span class="nv">$element</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="nv">$i</span><span class="o">++</span> <span class="o">===</span> <span class="nv">$n</span><span class="p">)</span> <span class="k">return</span> <span class="nv">$element</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//Poniższa implementacja działa tylko w PHP 7+.
//Standardowa funkcja zip, z dwóch iteratorów o długości "n"
//tworzy jeden, którego "n-ty" element to wynik wywołania $func z
//"n-tymi" elementami dwóch iteratorów.
</span><span class="k">function</span> <span class="nf">zip</span><span class="p">(</span><span class="nv">$func</span><span class="p">,</span> <span class="nx">\Iterator</span> <span class="nv">$as</span><span class="p">,</span> <span class="nx">\Iterator</span> <span class="nv">$bs</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="nv">$as</span><span class="o">-></span><span class="na">valid</span><span class="p">()</span> <span class="o">&&</span> <span class="nv">$bs</span><span class="o">-></span><span class="na">valid</span><span class="p">())</span> <span class="p">{</span>
<span class="k">yield</span> <span class="nv">$func</span><span class="p">(</span><span class="nv">$as</span><span class="o">-></span><span class="na">current</span><span class="p">(),</span> <span class="nv">$bs</span><span class="o">-></span><span class="na">current</span><span class="p">());</span>
<span class="nv">$as</span><span class="o">-></span><span class="na">next</span><span class="p">();</span> <span class="nv">$bs</span><span class="o">-></span><span class="na">next</span><span class="p">();</span>
<span class="k">yield</span> <span class="nx">from</span> <span class="nx">zip</span><span class="p">(</span><span class="nv">$func</span><span class="p">,</span> <span class="nv">$as</span><span class="p">,</span> <span class="nv">$bs</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">//tworzy funkcję dodającą $a do argumentu
</span><span class="k">function</span> <span class="nf">addX</span><span class="p">(</span><span class="nv">$a</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">function</span><span class="p">(</span><span class="nv">$b</span><span class="p">)</span> <span class="k">use</span><span class="p">(</span><span class="nv">$a</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$a</span> <span class="o">+</span> <span class="nv">$b</span><span class="p">;</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="c1">//funkcja dodająca dwie liczby
</span><span class="k">function</span> <span class="nf">add</span><span class="p">(</span><span class="nv">$a</span><span class="p">,</span> <span class="nv">$b</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$a</span> <span class="o">+</span> <span class="nv">$b</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Ciąg liczb naturalnych już zaimplementowaliśmy, teraz pora na ciąg fibonacciego (wersja iteracyjna dla PHP >=5.5 i <7).</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="nf">fib</span><span class="p">()</span> <span class="p">{</span>
<span class="nv">$a</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nv">$b</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">yield</span> <span class="nv">$a</span><span class="p">;</span>
<span class="k">yield</span> <span class="nv">$b</span><span class="p">;</span>
<span class="k">while</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$c</span> <span class="o">=</span> <span class="nv">$a</span> <span class="o">+</span> <span class="nv">$b</span><span class="p">;</span>
<span class="k">yield</span> <span class="nv">$c</span><span class="p">;</span>
<span class="nv">$a</span> <span class="o">=</span> <span class="nv">$b</span><span class="p">;</span>
<span class="nv">$b</span> <span class="o">=</span> <span class="nv">$c</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nv">$fib</span> <span class="o">=</span> <span class="nx">take</span><span class="p">(</span><span class="mi">20</span><span class="p">,</span> <span class="nx">drop</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="nx">map</span><span class="p">(</span><span class="nx">addX</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span> <span class="nx">fib</span><span class="p">())));</span>
<span class="c1">//szaleństwo, mapuję po nieskończonym ciągu,
//później porzucam 10 elementów i wybieram 20 kolejnych,
//czy to przejdzie i zadziała? Tak.
</span></code></pre></div></div>
<p>Wersja rekurencyjna (PHP 7+) - ten sam algorytm co w scali. Wykorzystanie rekurencyjnych generatorów:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="nf">fib</span><span class="p">()</span> <span class="p">{</span>
<span class="k">yield</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">yield</span> <span class="mi">2</span><span class="p">;</span>
<span class="k">yield</span> <span class="nx">from</span> <span class="nx">zip</span><span class="p">(</span><span class="s1">'add'</span><span class="p">,</span> <span class="nx">fib</span><span class="p">(),</span> <span class="nx">drop</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nx">fib</span><span class="p">()));</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="zalety-generatorów-php-a-zalety-strumieni-scala">Zalety generatorów (PHP), a zalety strumieni (Scala)</h3>
<ol>
<li>
<p><strong>Liczba operacji</strong>
Generatory w php także wykonają minimalną, konieczną pracę.</p>
</li>
<li>
<p><strong>Złożoność pamięciowa</strong>
Generatory w php zachowują się podobnie przy łączeniu operacji co strumienie w scali. Czas wykonania jest zbliżony do operacji nie generujących generatory. Z czego to wynika? Nie wiem, być może również ze szczegółów implementacyjnych jak w przypadku strumieni w scali. Złożoność pamięciowa jest podobna jak w strumieniach, nie są generowane przejściowe wyniki.</p>
</li>
<li>
<p><strong>Bezpieczna rekurencja</strong>
Niestety generatory nie chronią przed stack overflow przy rekurencji.</p>
</li>
</ol>
<h3 id="inne-zalety-generatorów">Inne zalety generatorów</h3>
<ol>
<li>Pozwalają wygodnie i elegancko iterować po dużej strukturze danych, mając w pamięci tylko jeden jej element. Np. iterowanie po liniach pobranych z dużego pliku.</li>
<li>Tworzenie generatorów wymaga minimum boilerplatu. Generator można wykorzystywać do tworzenia iteratorów za pomocą czytelnej i przyjaznej składni.</li>
<li>Kod klienta nie musi wiedzieć, że korzysta z generatora. Struktura danych, przekazana do przykładowej funkcji <code class="highlighter-rouge">map</code>, może być tablicą bądź dowolnym <code class="highlighter-rouge">Traversable</code>, a na wyjściu otrzymamy generator.</li>
<li>W PHP 7 generatory mogą korzystać z innych generatorów, dzięki czemu można stworzyć generator rekurencyjny. Pozwala to na krótszy i bardziej elegancki kod.</li>
</ol>psliwaOstatnio można było odnieść wrażenie, że tak jak tytuł mówi, jestem leniwy (ostatni wpis na początku roku), ale nic bardziej mylnego. Już zabieram się za temat leniwej ewaluacji. Wyjdę od Scali, a później przejdę do PHP.Nie Mockuj tak! Czyli o (nad)używaniu Mocków w testach…2015-01-07T00:00:00+00:002015-01-07T00:00:00+00:00http://psliwa.org/nie-mockuj-tak-czyli-o-naduzywaniu-mockow-w-testach<p>Na wstępie: fajnie by było, abyś wiedział mniej więcej co to jest Mock, Stub i Fake - nie będę tego jakoś szczególnie objaśniał bo idea tego wpisu jest inna niż wstęp do “zaślepek”. <a href="https://phpunit.de/manual/current/en/test-doubles.html">Tutaj</a> możesz poczytać o różnych zaślepkach na przykładzie PHPUnit.</p>
<p>Spis treści, a jak! <a name="toc"></a></p>
<ol>
<li><a href="#wstepniak">Wstępniak</a></li>
<li><a href="#kiedy">Kiedy Mock?</a>
<ol>
<li><a href="#query">Przykład zaślepiania metody typu Query</a>
<ol>
<li><a href="#query-mock">Zaślepienie metody Query za pomocą Mocka</a></li>
<li><a href="#query-stub">Zaślepienie metody Query za pomocą Stuba</a></li>
<li><a href="#query-fake">Zaślepienie metody Query za pomocą Fake</a></li>
<li><a href="#query-summary">Przemyślenia na temat zaślepiania metod typu Query</a></li>
</ol>
</li>
<li><a href="#command">Przykład zaślepiania metody typu Command</a>
<ol>
<li><a href="#command-stub">Zaślepienie metody Command za pomocą Stuba</a></li>
<li><a href="#command-mock">Zaślepienie metody Command za pomocą Mocka</a></li>
<li><a href="#command-fake">Zaślepienie metody Command za pomocą Fake</a></li>
<li><a href="#command-summary">Przemyślenia na temat zaślepiania metod typu Command</a></li>
</ol>
</li>
</ol>
</li>
<li><a href="#summary">Końcowe przemyślenia</a></li>
</ol>
<h3 id="wstępniak-do-góry">Wstępniak <small><a href="#toc">do góry</a></small><a name="wstepniak"></a></h3>
<p>Dla przypomnienia, zaślepka (z ang. Test Double) to obiekt, który jest przekazywany do obiektu testowanego zamiast obiektu będącego rzeczywistą zależnością w kodzie produkcyjnym. Przykładem może być wstrzyknięcie zaślepki obiektu <code class="highlighter-rouge">ProductRepository</code> (abstrakcji na persystentną kolekcję produktów) do jakieś usługi, np. <code class="highlighter-rouge">ProductService</code>.</p>
<p>Aby utworzyć obiekt <code class="highlighter-rouge">ProductService</code> potrzeba <code class="highlighter-rouge">ProductRepository</code>, aby utworzyć <code class="highlighter-rouge">ProductRepository</code> musimy utworzyć <code class="highlighter-rouge">EntityManagera</code> (zakładam implementację opartą na Doctrine), aby utworzyć <code class="highlighter-rouge">EntityManagera</code> trzeba utworzyć konfigurację oraz połączenie z bazą danych, aby połaczyć się z bazą danych musimy mieć z czym się połaczyć, więc serwer bazy danych musi być zainstalowany, schema bazy powinno być utworzone itp. Aby utworzyć obiekt konfiguracji, trzeba utworzyć obiekt cache… Na litość Boską, ja tylko chcę przetestować czy <code class="highlighter-rouge">ProductService::createProduct($name, $price)</code> tworzy obiekt produktu, zapisuje go w repozytorium i triggeruje przy tym odpowiedni event…</p>
<p>Powyższa historyjka pokazuje jedną z przyczyn stosowania zaślepek. Jest ich więcej, np:</p>
<ul>
<li>Uproszczenie setUp testu</li>
<li>Testy w izolacji (tzw. testy jednostkowe), nie chcemy drugi raz testować <code class="highlighter-rouge">DoctrineProductRepository</code> - ta klasa ma osobne testy!</li>
<li>Zwiększenie szybkości wykonywania testów - zewnętrzny web service zastępujemy lokalnym Stubem/Fake, repozytorium operujące na bazie zastępujemy implementacją in memory</li>
<li>Możemy w prosty sposób przetestować sytuacje brzegowe, które ciężko zreprodukować wykorzystując rzeczywisty obiekt</li>
<li>Podczas pisania implementacji klasy X, która zależy od typu Y, nie mamy jeszcze implementacji Y</li>
</ul>
<p>Oczywiście są sytuacje, w których nie należy stosować zaślepek. Należy pamiętać, że nie należy zamieniać części obiektu/podsystemu który testujemy. Np. nie powinno się zastępować <code class="highlighter-rouge">EntityManagera</code> implementacją “in memory” w testach <code class="highlighter-rouge">DoctrineProductRepository</code>, gdyż testy te mają testować, czy poprawnie szukamy / zapisujemy dane w bazie.</p>
<h3 id="kiedy-mock-do-góry">Kiedy Mock? <small><a href="#toc">do góry</a></small><a name="kiedy"></a></h3>
<p>W idealnym świecie, zgodnie z zasadą <strong>Command-Query separation</strong>, każda metoda powinna być typu <strong>Command</strong> lub <strong>Query</strong>.</p>
<ul>
<li><strong>Command</strong> - zmienia stan obiektu/systemu nie zwracając żadnej wartości - najprostszy przykład to setter ;)</li>
<li><strong>Query</strong> - pobranie stanu obiektu/systemu nie zmieniając stanu - najprostszy przykład to getter</li>
</ul>
<p>W złym guście jest tworzenie metod które zarówno są Command i Query, bo taka funkcja robi de facto dwie rzeczy - zmienia stan i go zwraca. Nie można wywołać 2x taką metodę aby pobrać stan, gdyż za każdym wywołaniem ten stan jest zmieniany.</p>
<p>Związek typu metod z zaślepkami:</p>
<ol>
<li>Mocki <strong>można</strong> stosować do zaślepiania metod typu <strong>Command</strong>, ale <strong>nie powinno się</strong> stosować do zaślepiania metod typu <strong>Query</strong> (są wyjątki)</li>
<li>Stuby <strong>można</strong> stosować do zaślepiania metod typu <strong>Query</strong>, ale <strong>nie należy</strong> stosować do zaślepiania metod typu <strong>Command</strong></li>
<li>Fake <strong>można</strong> stosować do zaślepiania metod typu <strong>Query</strong> i <strong>Command</strong></li>
</ol>
<blockquote>
<p><strong>Krótkie wyjaśnienie</strong> aby nie było zamętu:</p>
</blockquote>
<blockquote>
<p>Można utworzyć Stuba za pomocą frameworka do tworzenia Mocków. To brzmi dziwnie, ale jest to jak najbardziej stosowana i poprawna praktyka. <a href="https://phpunit.de/manual/current/en/test-doubles.html">Tutaj</a> są przykłady tworzenia różnych zaślepek za pomocą frameworku do mockowania, który jest wykorzystany w PHPUnit. To czy obiekt jest mockiem, nie jest definiowane przez fakt, że został utworzony przez metodę <code class="highlighter-rouge">getMock</code> (w PHPUnit), ale przez to, że nałożono na ten obiekt dodatkowe oczekiwania, które są później jawnie bądź niejawnie weryfikowane.</p>
</blockquote>
<h4 id="przykład-zaślepiania-metody-typu-query-do-góry">Przykład zaślepiania metody typu Query <small><a href="#toc">do góry</a></small><a name="query"></a></h4>
<p>Przykład kodu do którego chcemy napisać test:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//interfejs translatora
</span><span class="k">interface</span> <span class="nx">Translator</span> <span class="p">{</span>
<span class="k">function</span> <span class="nf">trans</span><span class="p">(</span><span class="nv">$id</span><span class="p">,</span> <span class="k">array</span> <span class="nv">$params</span> <span class="o">=</span> <span class="p">[],</span> <span class="nv">$locale</span> <span class="o">=</span> <span class="kc">null</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">class</span> <span class="nc">UserService</span> <span class="p">{</span>
<span class="k">private</span> <span class="nv">$translator</span><span class="p">;</span>
<span class="c1">//...
</span>
<span class="c1">//trywialna metoda którą chcemy przetestować
</span> <span class="k">function</span> <span class="nf">createInvitation</span><span class="p">(</span><span class="nx">User</span> <span class="nv">$user</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Invitation</span><span class="p">(</span>
<span class="nv">$user</span><span class="p">,</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">translator</span><span class="o">-></span><span class="na">trans</span><span class="p">(</span>
<span class="s1">'Hello %user%'</span><span class="p">,</span>
<span class="p">[</span><span class="s1">'%user%'</span> <span class="o">=></span> <span class="p">(</span><span class="nx">string</span><span class="p">)</span> <span class="nv">$user</span><span class="p">]</span>
<span class="p">),</span>
<span class="o">...</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Translator ma metodą <code class="highlighter-rouge">trans</code>, która jest typu <strong>Query</strong> - tak więc nie powinniśmy stosować Mocka, ale dla celów naukowych spróbujmy…</p>
<h5 id="zaślepienie-metody-query-za-pomocą-mocka-do-góry">Zaślepienie metody Query za pomocą Mocka <small><a href="#toc">do góry</a></small><a name="query-mock"></a></h5>
<p>Uwaga, bardzo <strong>ZŁY</strong> test! Nie rób tego w domu (w pracy też)!</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//given
</span>
<span class="nv">$user</span> <span class="o">=</span> <span class="o">...</span><span class="p">;</span>
<span class="nv">$translator</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">getMock</span><span class="p">(</span><span class="s1">'Translator'</span><span class="p">);</span>
<span class="nv">$service</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">UserService</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="nv">$translator</span><span class="p">);</span>
<span class="nv">$translator</span><span class="o">-></span><span class="na">expects</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">once</span><span class="p">())</span>
<span class="o">-></span><span class="na">method</span><span class="p">(</span><span class="s1">'trans'</span><span class="p">)</span>
<span class="o">-></span><span class="na">with</span><span class="p">(</span><span class="s1">'Hello %user%'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'%user%'</span> <span class="o">=></span> <span class="p">(</span><span class="nx">string</span><span class="p">)</span> <span class="nv">$user</span> <span class="p">])</span>
<span class="o">-></span><span class="na">willReturn</span><span class="p">(</span><span class="s1">'Hello Peter'</span><span class="p">);</span>
<span class="c1">//when
</span>
<span class="nv">$invitation</span> <span class="o">=</span> <span class="nv">$service</span><span class="o">-></span><span class="na">createInvitation</span><span class="p">(</span><span class="nv">$user</span><span class="p">);</span>
<span class="c1">//then
//... asercje
</span></code></pre></div></div>
<p><strong>Co zyskaliśmy</strong>:</p>
<ul>
<li>Mamy test, który faktycznie testuje</li>
</ul>
<p><strong>Problemy</strong>:</p>
<ul>
<li>Część kodu produkcyjnego przesiąkło do testu (argumenty metody <code class="highlighter-rouge">with</code>)</li>
<li>Test zna implementację, gdy implementacja się zmieni, trzeba będzie zmienić test - tak więc test jest bardzo kruchy i ciężki w utrzymaniu</li>
<li>Jest dłuższy od implementacji i niezbyt czytelny</li>
<li>Gdy będzie więcej metod do zaślepienia, to test będzie jeszcze mniej czytelny - wystąpi “eksplozja oczekiwań”</li>
</ul>
<h5 id="zaślepienie-metody-query-za-pomocą-stuba-do-góry">Zaślepienie metody Query za pomocą Stuba <small><a href="#toc">do góry</a></small><a name="query-stub"></a></h5>
<p>Lepsze rozwiązanie od poprzedniego, wg magicznej listy Stuba można wykorzystać do zaślepienia metod typu Query.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//given
</span>
<span class="nv">$user</span> <span class="o">=</span> <span class="o">...</span><span class="p">;</span>
<span class="nv">$translator</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">getMock</span><span class="p">(</span><span class="s1">'Translator'</span><span class="p">);</span>
<span class="nv">$service</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">UserService</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="nv">$translator</span><span class="p">);</span>
<span class="c1">//usuneliśmy $this->once() i wywołanie "with" - to były oczekiwania,
//a chcemy stworzyć Stuba - $translator mimo że powstał za pomocą
//metody "getMock" nie jest Mockiem
</span><span class="nv">$translator</span><span class="o">-></span><span class="na">expects</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">any</span><span class="p">())</span>
<span class="o">-></span><span class="na">method</span><span class="p">(</span><span class="s1">'trans'</span><span class="p">)</span>
<span class="o">-></span><span class="na">willReturn</span><span class="p">(</span><span class="s1">'Hello Peter'</span><span class="p">);</span>
<span class="c1">//when
</span>
<span class="nv">$invitation</span> <span class="o">=</span> <span class="nv">$service</span><span class="o">-></span><span class="na">createInvitation</span><span class="p">(</span><span class="nv">$user</span><span class="p">);</span>
<span class="c1">//then
//... asercje
</span></code></pre></div></div>
<p><strong>Co zyskaliśmy</strong>:</p>
<ol>
<li>Brak kodu produkcyjnego w teście -> test jest mniej kruchy</li>
<li>Słabe (ale nie brak) powiązanie kodu testu z implementacją -> test jest jeszcze mniej kruchy</li>
</ol>
<p><strong>Problemy</strong>:</p>
<ol>
<li>Test jest zaśmiecony tworzeniem stuba</li>
<li>Jakby było więcej metod do zaślepienia, to test byłby jeszcze bardziej zaśmiecony</li>
<li>Nie testujemy argumentów przekazywanych do metody <code class="highlighter-rouge">trans</code> - słabsze pokrycie kodu testami (nie w sensie <a href="http://en.wikipedia.org/wiki/Code_coverage">Code Coverage</a>, który tak naprawdę nie jest dobrą metryką oceny jakości testów, ale w sensie <a href="http://en.wikipedia.org/wiki/Mutation_testing">testów mutacyjnych</a>)</li>
</ol>
<h5 id="zaślepienie-metody-query-za-pomocą-fake-do-góry">Zaślepienie metody Query za pomocą Fake <small><a href="#toc">do góry</a></small><a name="query-fake"></a></h5>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//kod Fake
</span><span class="k">class</span> <span class="nc">FakeTranslator</span> <span class="k">implements</span> <span class="nx">Translator</span> <span class="p">{</span>
<span class="k">function</span> <span class="nf">trans</span><span class="p">(</span><span class="nv">$id</span><span class="p">,</span> <span class="k">array</span> <span class="nv">$params</span> <span class="o">=</span> <span class="p">[],</span> <span class="nv">$locale</span> <span class="o">=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">strtr</span><span class="p">(</span><span class="nv">$id</span><span class="p">,</span> <span class="nv">$params</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">//...
</span>
<span class="c1">//given
</span>
<span class="nv">$user</span> <span class="o">=</span> <span class="o">...</span><span class="p">;</span>
<span class="nv">$service</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">UserService</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="k">new</span> <span class="nx">FakeTranslator</span><span class="p">());</span>
<span class="c1">//when
</span>
<span class="nv">$invitation</span> <span class="o">=</span> <span class="nv">$service</span><span class="o">-></span><span class="na">createInvitation</span><span class="p">(</span><span class="nv">$user</span><span class="p">);</span>
<span class="c1">//then
//... asercje
</span></code></pre></div></div>
<p><strong>Co zyskaliśmy</strong>:</p>
<ol>
<li>Test jest prosty i skalowalny - mniejszy wpływ liczby zaślepianych metod na długość testu</li>
<li>Możliwość testowania argumentów, które są przekazywane do metody <code class="highlighter-rouge">trans</code></li>
<li>Mozliwość zastosowania <code class="highlighter-rouge">FakeTranslator</code> w wielu różnych testach oraz w różnych klasach testowych.</li>
</ol>
<p><strong>Problemy</strong>:</p>
<ol>
<li>Gdy dojdzie metoda do interfejsu <code class="highlighter-rouge">Translator</code>, trzeba zaktualizować implementację <code class="highlighter-rouge">FakeTranslator</code></li>
<li>Gdy <strong>Fake</strong> urośnie i jego logika nie będzie trywialna, należy taką klasę również przetestować. Najlepiej dokładnie tymi samymi testami jak rzeczywista implementacja - o tym, jak to zrobić, pisałem w <a href="http://psliwa.org/jeden-testcase-dla-wielu-testowanych-klas/">jednym z poprzednich wpisów</a>.</li>
</ol>
<h5 id="przemyślenia-na-temat-zaślepiania-metod-typu-querya--do-góry">Przemyślenia na temat zaślepiania metod typu Query<a <small><a href="#toc">do góry</a></small><a name="query-summary"></a></h5>
<p>Przy małej liczbie metod do zaślepienia, można zbudować Stuba za pomocą frameworka do Mockowania. Gdy liczba metod jest większa i w kilku klasach testowych wykorzystywany jest podobny stub, powinno się zastanowić czy wprowadzenie “fałszywej” implementacji nie będzie korzystne. Zastosowanie Mocka w tym przypadku jest bardzo słabiutkie, aczkolwiek istnieją przypadki w których zastosowanie Mocka dla metod typu Query jest wskazane - o tym w końcowych przemyśleniach.</p>
<h4 id="przykład-zaślepiania-metody-typu-command-do-góry">Przykład zaślepiania metody typu Command <small><a href="#toc">do góry</a></small><a name="command"></a></h4>
<p>Przykład kodu do którego chcemy napisać test:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">interface</span> <span class="nx">ProductRepository</span> <span class="p">{</span>
<span class="k">function</span> <span class="nf">save</span><span class="p">(</span><span class="nx">Product</span> <span class="nv">$product</span><span class="p">);</span>
<span class="k">function</span> <span class="nf">findOneById</span><span class="p">(</span><span class="nv">$id</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">interface</span> <span class="nx">EventDispatcher</span> <span class="p">{</span>
<span class="k">function</span> <span class="nf">dispatch</span><span class="p">(</span><span class="nv">$eventName</span><span class="p">,</span> <span class="nv">$event</span> <span class="o">=</span> <span class="kc">null</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">class</span> <span class="nc">ProductService</span> <span class="p">{</span>
<span class="k">private</span> <span class="nv">$repository</span><span class="p">;</span>
<span class="k">private</span> <span class="nv">$eventDispatcher</span><span class="p">;</span>
<span class="c1">//trywialna metoda, którą chcemy przetestować
</span> <span class="k">function</span> <span class="nf">saveProduct</span><span class="p">(</span><span class="nx">Product</span> <span class="nv">$product</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">repository</span><span class="o">-></span><span class="na">save</span><span class="p">(</span><span class="nv">$product</span><span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">eventDispatcher</span><span class="o">-></span><span class="na">dispatch</span><span class="p">(</span>
<span class="s1">'product.saved'</span><span class="p">,</span>
<span class="k">new</span> <span class="nx">Event</span><span class="p">([</span> <span class="s1">'product'</span> <span class="o">=></span> <span class="nv">$product</span> <span class="p">]</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Metody <code class="highlighter-rouge">ProductRepository::save</code> i <code class="highlighter-rouge">EventDispatcher::dispatch</code> są typu <strong>Command</strong> (nic nie zwracają, zmieniają stan), więc powinniśmy zastosować Mocki lub ewentualnie Fake. Nie powinno się stosować Stuba, ale dla cełów naukowych to rozwiązanie idzie na pierwszy ogień…</p>
<h5 id="zaślepienie-metody-command-za-pomocą-stuba-do-góry">Zaślepienie metody Command za pomocą Stuba <small><a href="#toc">do góry</a></small><a name="command-stub"></a></h5>
<p>Uwaga, okropnie <strong>ZŁY</strong> test!</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//given
</span>
<span class="nv">$product</span> <span class="o">=</span> <span class="o">...</span><span class="p">;</span>
<span class="nv">$repository</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">getMock</span><span class="p">(</span><span class="s1">'ProductRepository'</span><span class="p">);</span>
<span class="nv">$eventDispatcher</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">getMock</span><span class="p">(</span><span class="s1">'EventDispatcher'</span><span class="p">);</span>
<span class="nv">$service</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ProductService</span><span class="p">(</span><span class="nv">$repository</span><span class="p">,</span> <span class="nv">$eventDispatcher</span><span class="p">);</span>
<span class="nv">$repository</span><span class="o">-></span><span class="na">expects</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">any</span><span class="p">())</span>
<span class="o">-></span><span class="na">method</span><span class="p">(</span><span class="s1">'save'</span><span class="p">);</span>
<span class="nv">$eventDispatcher</span><span class="o">-></span><span class="na">expects</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">any</span><span class="p">())</span>
<span class="o">-></span><span class="na">method</span><span class="p">(</span><span class="s1">'dispatch'</span><span class="p">);</span>
<span class="c1">//when
</span>
<span class="nv">$service</span><span class="o">-></span><span class="na">saveProduct</span><span class="p">(</span><span class="nv">$product</span><span class="p">);</span>
<span class="c1">//then
//??? jak zweryfikować taki test? Nie da się ;)
</span></code></pre></div></div>
<p><strong>Co zyskaliśmy</strong>:</p>
<ul>
<li>Nic oprócz fałszywego przeświadczenia, że mamy test - ten test nic nie testuje</li>
</ul>
<p><strong>Problemy</strong>:</p>
<ul>
<li>Zbędny kod, który trzeba utrzymywać</li>
<li>Fałszywe przeświadczenie, że kod jest przetestowany - <a href="http://en.wikipedia.org/wiki/Code_coverage">Code Coverage</a> będzie 100% ;)</li>
</ul>
<h5 id="zaślepienie-metody-command-za-pomocą-mocka-do-góry">Zaślepienie metody Command za pomocą Mocka <small><a href="#toc">do góry</a></small><a name="command-mock"></a></h5>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//given
</span>
<span class="nv">$product</span> <span class="o">=</span> <span class="o">...</span><span class="p">;</span>
<span class="nv">$repository</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">getMock</span><span class="p">(</span><span class="s1">'ProductRepository'</span><span class="p">);</span>
<span class="nv">$eventDispatcher</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">getMock</span><span class="p">(</span><span class="s1">'EventDispatcher'</span><span class="p">);</span>
<span class="nv">$service</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ProductService</span><span class="p">(</span><span class="nv">$repository</span><span class="p">,</span> <span class="nv">$eventDispatcher</span><span class="p">);</span>
<span class="nv">$repository</span><span class="o">-></span><span class="na">expects</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">once</span><span class="p">())</span>
<span class="o">-></span><span class="na">method</span><span class="p">(</span><span class="s1">'save'</span><span class="p">)</span>
<span class="o">-></span><span class="na">with</span><span class="p">(</span><span class="nv">$product</span><span class="p">);</span>
<span class="nv">$eventDispatcher</span><span class="o">-></span><span class="na">expects</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">once</span><span class="p">())</span>
<span class="o">-></span><span class="na">method</span><span class="p">(</span><span class="s1">'dispatch'</span><span class="p">)</span>
<span class="o">-></span><span class="na">with</span><span class="p">(</span><span class="s1">'product.saved'</span><span class="p">,</span> <span class="k">new</span> <span class="nx">Event</span><span class="p">([</span> <span class="s1">'product'</span> <span class="o">=></span> <span class="nv">$product</span> <span class="p">]);</span>
<span class="c1">//when
</span>
<span class="nv">$service</span><span class="o">-></span><span class="na">saveProduct</span><span class="p">(</span><span class="nv">$product</span><span class="p">);</span>
<span class="c1">//then
//PHPUnit na końcu odpala weryfikacje mocków
</span></code></pre></div></div>
<p><strong>Co zyskaliśmy</strong>:</p>
<ul>
<li>Testy, które faktycznie całkiem nieźle testują</li>
</ul>
<p><strong>Problemy</strong>:</p>
<ul>
<li>Testy są związane z implementacją i są trochę przegadane</li>
</ul>
<h5 id="zaślepienie-metody-command-za-pomocą-fake-do-góry">Zaślepienie metody Command za pomocą Fake <small><a href="#toc">do góry</a></small><a name="command-fake"></a></h5>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//implementacja Fakeów
</span><span class="k">class</span> <span class="nc">FakeProductRepository</span> <span class="k">implements</span> <span class="nx">ProductRepository</span> <span class="p">{</span>
<span class="k">private</span> <span class="nv">$products</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">function</span> <span class="nf">save</span><span class="p">(</span><span class="nx">Product</span> <span class="nv">$product</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="nv">$product</span><span class="o">-></span><span class="na">getId</span><span class="p">()</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$product</span><span class="o">-></span><span class="na">setId</span><span class="p">(</span><span class="nb">rand</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">9999999</span><span class="p">));</span>
<span class="p">}</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">products</span><span class="p">[</span><span class="nv">$product</span><span class="o">-></span><span class="na">getId</span><span class="p">()]</span> <span class="o">=</span> <span class="nv">$product</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">function</span> <span class="nf">findOneById</span><span class="p">(</span><span class="nv">$id</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nb">isset</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">products</span><span class="p">[</span><span class="nv">$id</span><span class="p">]))</span> <span class="k">throw</span> <span class="k">new</span> <span class="nx">Exception</span><span class="p">();</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="na">products</span><span class="p">[</span><span class="nv">$id</span><span class="p">];</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">class</span> <span class="nc">FakeEventDispatcher</span> <span class="k">implements</span> <span class="nx">EventDispatcher</span> <span class="p">{</span>
<span class="k">private</span> <span class="nv">$dispatchedEvents</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">function</span> <span class="nf">dispatch</span><span class="p">(</span><span class="nv">$name</span><span class="p">,</span> <span class="nv">$event</span> <span class="o">=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">dispatchedEvents</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span><span class="nv">$name</span><span class="p">,</span> <span class="nv">$event</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">function</span> <span class="nf">getDispatchedEvents</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="na">dispatchedEvents</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">//... i testy
</span>
<span class="c1">//given
</span>
<span class="nv">$product</span> <span class="o">=</span> <span class="o">...</span><span class="p">;</span>
<span class="nv">$repository</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FakeProductRepository</span><span class="p">();</span>
<span class="nv">$eventDispatcher</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FakeEventDispatcher</span><span class="p">();</span>
<span class="nv">$service</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ProductService</span><span class="p">(</span><span class="nv">$repository</span><span class="p">,</span> <span class="nv">$eventDispatcher</span><span class="p">);</span>
<span class="c1">//when
</span>
<span class="nv">$service</span><span class="o">-></span><span class="na">saveProduct</span><span class="p">(</span><span class="nv">$product</span><span class="p">);</span>
<span class="c1">//then
</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">assertEquals</span><span class="p">(</span>
<span class="nv">$product</span><span class="p">,</span>
<span class="nv">$repository</span><span class="o">-></span><span class="na">findOneById</span><span class="p">(</span><span class="nv">$product</span><span class="o">-></span><span class="na">getId</span><span class="p">()</span>
<span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">assertEquals</span><span class="p">(</span>
<span class="p">[[</span> <span class="s1">'product.saved'</span><span class="p">,</span> <span class="k">new</span> <span class="nx">Event</span><span class="p">([</span><span class="s1">'product'</span> <span class="o">=></span> <span class="nv">$product</span><span class="p">])</span> <span class="p">]],</span>
<span class="nv">$eventDispatcher</span><span class="o">-></span><span class="na">getDispatchedEvents</span><span class="p">()</span>
<span class="p">);</span>
</code></pre></div></div>
<p><strong>Co zyskaliśmy</strong>:</p>
<ul>
<li>kod testu jest stosunkowo prosty i krótki</li>
<li>Test działa i faktycznie całkiem nieźle testuje</li>
<li>Test jest w mniejszym stopniu związany z implementacją <code class="highlighter-rouge">ProductService</code> - np. nie ma wzmianki o metodzie <code class="highlighter-rouge">ProductRepository::save</code></li>
</ul>
<p><strong>Problemy</strong>:</p>
<ul>
<li>Trzeba było dopisać metodę <code class="highlighter-rouge">FakeEventDispatcher::getDispatchedEvents</code> aby dało się przetestować, czy event miał miejsce</li>
<li>Trzeba było napisać obiekty Fake (co z ich testami?)</li>
<li>Druga asercja (sprawdzanie eventu) jest średio czytelna</li>
</ul>
<h5 id="przemyślenia-na-temat-zaślepiania-metod-typu-command-do-góry">Przemyślenia na temat zaślepiania metod typu Command <small><a href="#toc">do góry</a></small><a name="command-summary"></a></h5>
<p>W tym konkretnym przypadku, dobrym rozwiązaniem byłoby wykorzystanie <code class="highlighter-rouge">FakeProductRepository</code> oraz mocka <code class="highlighter-rouge">EventDispatcher</code>, czyli rozwiązanie hybrydowe. Dzięki temu zniwelowane byłyby problemy związane z zastosowaniem tylko Mocków lub tylko Fakeów. O ile zastosowanie Mocków do zaślepiania metod typu Query ma jeszcze jakikolwiek sens (w niektórych przypadkach), to zastosowanie Stubów do zaślepienia metod typu Command już nie.</p>
<h3 id="końcowe-przemyślenia-do-góry">Końcowe przemyślenia <small><a href="#toc">do góry</a></small><a name="summary"></a></h3>
<p>Mocki mają zastosowanie głównie dla zaślepiania metod typu Command. Jednakże nie każda metoda Command powinna być z urzędu zaślepiana Mockiem - niekiedy lepszym rozwiązaniem jest Fake. Kiedy lepszy będzie Fake? Gdy w wielu klasach testowych są wykorzystywane podobne Mocki (Fake pozwala zachować zasadę DRY). Fake pozwala również uprościć kod testów i sprawić, że test będzie w mniejszym stopniu powiązany z implementacją testowanej klasy.</p>
<p>Istnieją sytuacje, w których wskazane jest użycie <strong>Mocka</strong> nawet dla metod typu <strong>Query</strong>, np:</p>
<ul>
<li>wywołanie przypadków granicznych, np. rzucenie wyjątku</li>
<li>konieczność przetestowania wpływu wartości zwrotnych z zaślepionej jednej metody na wywołanie kolejnej zaślepionej metody. Np. testowanie obiektu korzystającego z Cache - gdy metoda <code class="highlighter-rouge">Cache::test</code> zwróci false, ma zostać wywołana metoda <code class="highlighter-rouge">Cache::save</code>, ale nie powinna być wywołana metoda <code class="highlighter-rouge">Cache::load</code> - tutaj dobrym wyborem jest Mock, bo inaczej ciężko to przetestować - mimo iż <code class="highlighter-rouge">test</code> i <code class="highlighter-rouge">load</code> są typu Query</li>
</ul>
<p>Gdy doprowadziło się do sytuacji, w której jeden Mock zwraca drugiego, to jest to zapach złego designu (prawdopodobnie złamanie <a href="http://pl.wikipedia.org/wiki/Prawo_Demeter">prawa Demeter</a>) lub ewentualnie źle dobranych rodzajów zaślepek.</p>
<p>Testy powinny w miarę możliwości traktować obiekt testowany jak czarną skrzynkę:</p>
<ol>
<li>Dajemy jakieś dane na wejście</li>
<li>Odpalamy metodę testowaną</li>
<li>Nie wiemy co się dzieje w środku obiektu testowanego (test nie zna implementacji i algorytmu)</li>
<li>Sprawdzamy rezultat operacji</li>
</ol>
<p>Test powinien sprawdzać, czy obiekt zrobił to co chcemy, a nie jak to zrobił. Test powinien pozostać nienaruszony i działający, jeśli zmienimy implementacje metody testowanej, zmienimy algorytm, zrefaktoryzujemy kod itp.</p>
<p>Stosowanie Mocków skutecznie uniemożliwia takie podejście, gdyż samo użycie Mocka wiąże test z implementacją obiektu. Dlatego trzeba w pełni świadomie korzystać z Mocków, bo w przeciwnym wypadku testy będą kruche i każda zmiana w kodzie produkcyjnym będzie ciągnąć za sobą zmianę testów. Może to doprowadzić do kuriozalnej sytuacji, w której nie będziemy refaktoryzować, bo wymagałoby to zmiany w testach. Polecam tego <a href="https://www.youtube.com/watch?v=wbAtJlbRhbQ">talka</a> - porusza on m. in. problem “zabetonowania” kodu za pomocą złych testów.</p>psliwaNa wstępie: fajnie by było, abyś wiedział mniej więcej co to jest Mock, Stub i Fake - nie będę tego jakoś szczególnie objaśniał bo idea tego wpisu jest inna niż wstęp do “zaślepek”. Tutaj możesz poczytać o różnych zaślepkach na przykładzie PHPUnit.PhpStorm? composer? plugin?2014-12-15T00:00:00+00:002014-12-15T00:00:00+00:00http://psliwa.org/phpstorm-composer-plugin<p>Napisałem prosty plugin do PhpStorma (w wersji od 8.0.2 wzwyż, więc nie obijajcie się z aktualizacją). Jest on zatytuowany <a href="https://plugins.jetbrains.com/plugin/7631"><strong>PHP Composer AutoCompletion</strong></a>, jak sama nazwa wskazuje, dodaje on podpowiadanie składni do pliku <code class="highlighter-rouge">composer.json</code>. Działa podpowiadanie struktury pliku oraz nazw paczek wraz z wersjami (tylko z packagist.org).</p>
<p>Issue na bugtrakerze PhpStorma z pomysłem tego ficzera wisi od ponad 2 lat…</p>
<p>Plugin w obecnej wersji ma minimalny zbiór funkcjonalności. Z czasem będą dodawane kolejne rzeczy, takie jak ulepszony autocomplete wersji paczek (wildcary, zakresy itp.), walidacja schematu i podkreślanie błędów, inspekcje, wyświetlanie obecnie zainstalowanej wersji (z composer.lock), wykrywanie dodanych paczek i ich instalowanie, optymalizacja całości.</p>
<p>Kod źródłowy można znaleźć na <a href="https://github.com/psliwa/idea-composer-plugin">githubie</a>, jak się podoba, to zainstalowanie i <a href="https://plugins.jetbrains.com/plugin/7631">ocenienie</a> będzie mile widziane.</p>
<p>Plugin w akcji:</p>
<p><img src="http://plugins.jetbrains.com/files/7631/screenshot_14847.png" alt="Podgląd" /></p>psliwaNapisałem prosty plugin do PhpStorma (w wersji od 8.0.2 wzwyż, więc nie obijajcie się z aktualizacją). Jest on zatytuowany PHP Composer AutoCompletion, jak sama nazwa wskazuje, dodaje on podpowiadanie składni do pliku composer.json. Działa podpowiadanie struktury pliku oraz nazw paczek wraz z wersjami (tylko z packagist.org).Jeden TestCase dla wielu testowanych klas2014-11-10T00:00:00+00:002014-11-10T00:00:00+00:00http://psliwa.org/jeden-testcase-dla-wielu-testowanych-klas<p>Na początku nakreślę problem. Mamy klasę <code class="highlighter-rouge">Product</code> oraz interfejs <code class="highlighter-rouge">ProductRepository</code>.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">interface</span> <span class="nx">ProductRepository</span> <span class="p">{</span>
<span class="k">function</span> <span class="nf">save</span><span class="p">(</span><span class="nx">Product</span> <span class="nv">$product</span><span class="p">);</span>
<span class="k">function</span> <span class="nf">delete</span><span class="p">(</span><span class="nx">Product</span> <span class="nv">$product</span><span class="p">);</span>
<span class="k">function</span> <span class="nf">findOne</span><span class="p">(</span><span class="nv">$id</span><span class="p">);</span>
<span class="k">function</span> <span class="nf">findAll</span><span class="p">();</span>
<span class="c1">//...
</span><span class="p">}</span>
</code></pre></div></div>
<p><code class="highlighter-rouge">ProductRepository</code> może mieć kilka implementacji, np.:</p>
<ul>
<li><code class="highlighter-rouge">DoctrineProductRepository</code> - zapisywanie obiektów do bazy danych z wykorzystaniem <code class="highlighter-rouge">EntityManagera</code> z Doctrine, nasza “rzeczywista” implementacja używana przez aplikację</li>
<li><code class="highlighter-rouge">InMemoryProductRepository</code> - trzymanie obiektów w pamięci, klasa wykorzystywana np. do testów funkcjonalnych kontrolerów lub serwisów - nie chcemy angażować bazy danych aby testy były szybsze, prostsze do konfiguracji itp.</li>
</ul>
<p>Obie implementacje muszą działać tak samo, tak więc powinny mieć taki sam zestaw testów.</p>
<h3 id="rozwiązanie-problemu">Rozwiązanie problemu</h3>
<p>Jednym z rozwiązań jest napisanie abstrakcyjnej klasy testu, testowany obiekt oraz tworzenie danych testowych są w formie metod abstrakcyjnych.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">abstract</span> <span class="k">class</span> <span class="nc">ProductRepositoryTest</span> <span class="k">extends</span> <span class="nx">PHPUnit_Framework_TestCase</span> <span class="p">{</span>
<span class="k">protected</span> <span class="nv">$repository</span><span class="p">;</span>
<span class="k">protected</span> <span class="k">function</span> <span class="nf">setUp</span><span class="p">()</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">repository</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">createSUT</span><span class="p">();</span>
<span class="p">}</span>
<span class="c1">//tworzenie obiektu testowanego
</span> <span class="k">abstract</span> <span class="k">protected</span> <span class="k">function</span> <span class="nf">createSUT</span><span class="p">();</span>
<span class="c1">//umieszczanie Produktu w źródle danych
</span> <span class="k">abstract</span> <span class="k">protected</span> <span class="k">function</span> <span class="nf">insertProduct</span><span class="p">(</span><span class="nx">Product</span> <span class="nv">$product</span><span class="p">);</span>
<span class="sd">/**
* @test
*/</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">givenExistingProduct_whenItIsDeleted_thenProductCouldNotBeFound</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">//given
</span> <span class="nv">$product</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Product</span><span class="p">(</span><span class="o">...</span><span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">insertProduct</span><span class="p">(</span><span class="nv">$product</span><span class="p">);</span>
<span class="c1">//when
</span> <span class="nv">$this</span><span class="o">-></span><span class="na">repository</span><span class="o">-></span><span class="na">delete</span><span class="p">(</span><span class="nv">$product</span><span class="p">);</span>
<span class="nv">$actualProduct</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">repository</span><span class="o">-></span><span class="na">findOne</span><span class="p">(</span><span class="nv">$product</span><span class="o">-></span><span class="na">getId</span><span class="p">());</span>
<span class="c1">//then
</span> <span class="nv">$this</span><span class="o">-></span><span class="na">assertNull</span><span class="p">(</span><span class="nv">$actualProduct</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">//...
</span><span class="p">}</span>
</code></pre></div></div>
<p>W konkretnych podklasach nadpisujemy metody <code class="highlighter-rouge">createSUT</code> i <code class="highlighter-rouge">insertProduct</code>.</p>
<p>Jest jednak problem… Klasa testu operującego na rzeczywistej bazie danych niekoniecznie rozszerza bezpośrednio <code class="highlighter-rouge">PHPUnit_Framework_TestCase</code>. Klasa bazowa dla testów na bazie dostarcza tworzenie połączenia, obtacza testy w transakcję która na końcu jest rollbackowana itp. Tak więc występuje potrzeba wielodziedziczenia, <code class="highlighter-rouge">DoctrineProductRepositoryTest</code> powinien rozszerzać zarówno <code class="highlighter-rouge">ProductRepositoryTest</code> oraz <code class="highlighter-rouge">DoctrineTestCase</code>. Nie jest to możliwe, chyba że zamienimy <code class="highlighter-rouge">ProductRepositoryTest</code> w trait.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="sd">/**
* @mixin \PHPUnit_Framework_TestCase
*/</span>
<span class="nx">trait</span> <span class="nx">ProductRepositoryTest</span> <span class="p">{</span>
<span class="c1">//to samo co w klasie ProductRepositoryTest
</span> <span class="c1">//w poprzednim listingu
</span><span class="p">}</span>
</code></pre></div></div>
<p>Adnotacja <code class="highlighter-rouge">@mixin</code> jest interpretowana przez PhpStorm tak, jakby <code class="highlighter-rouge">ProductRepositoryTest</code> “rozszerzał” <code class="highlighter-rouge">PHPUnit_Framework_TestCase</code>, dzięki czemu poprawnie działa podpowiadanie metod (np. asercji).</p>
<p>Możemy zatem wykorzystać trait do stworzenia testu dla <code class="highlighter-rouge">DoctrineProductRepository</code>.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DoctrineProductRepositoryTest</span> <span class="k">extends</span> <span class="nx">DoctrineTestCase</span> <span class="p">{</span>
<span class="k">use</span> <span class="nx">ProductRepositoryTest</span><span class="p">;</span>
<span class="k">protected</span> <span class="k">function</span> <span class="nf">createSUT</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">DoctrineProductRepository</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="na">em</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">protected</span> <span class="k">function</span> <span class="nf">insertProduct</span><span class="p">(</span><span class="nx">Product</span> <span class="nv">$product</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">em</span><span class="o">-></span><span class="na">persist</span><span class="p">(</span><span class="nv">$product</span><span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">em</span><span class="o">-></span><span class="na">flush</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Podobnie by wyglądał <code class="highlighter-rouge">InMemoryProductRepositoryTest</code>. Gdy dopisujemy jakiś test do <code class="highlighter-rouge">ProductRepositoryTest</code>, automatycznie będzie on wykonywany dla dwóch różnych implementacji <code class="highlighter-rouge">ProductRepository</code>. Minusem tego rozwiązania może być zaburzenie pętli TDD. Pisząc jeden test tak naprawdę mamy w rezultacie dwa czerwone testy - czyli w następnym kroku musimy zaimplementować dwie funkcjonalności dla dwóch całkowicie różnych klas, aby na nowo mieć przed oczami tylko zieleń. Pętla “Red -> Green -> Refactor” zamienia się w “Red x2 -> Green, Red -> Green x 2 -> Refactor x 2”. Można temu przeciwdziałać wstawiając test najpierw do <code class="highlighter-rouge">DoctrineProductRepositoryTest</code>, a po zaimplementowaniu funkcjonalności i refaktoryzacji, przenieść go do <code class="highlighter-rouge">ProductRepositoryTest</code>.</p>
<p>Mając na początku interfejs <code class="highlighter-rouge">ProductRepository</code> i pisząc pierwszą implementację (np. <code class="highlighter-rouge">DoctrineProductRepository</code>), nie powinniśmy zaczynać od razu od tworzenia cechy/klasy abstrakcyjnej <code class="highlighter-rouge">ProductRepositoryTest</code>, gdyż tak naprawdę nie wiemy czy ona będzie potrzebna. Metody testowe należy umieszczać bezpośrednio w <code class="highlighter-rouge">DoctrineProductRepositoryTest</code>, a gdy będziemy mieć drugą implementację <code class="highlighter-rouge">ProductRepository</code>, powinniśmy dokonać refaktoryzacji wydzielenia cechy (trait) bądź klasy.</p>
<p>Bonusem tego rozwiązania jest to, że <code class="highlighter-rouge">ProductRepositoryTest</code> zawiera tylko czyste testy bez szczegółów tworzenia SUT i środowiska testowego, dzięki temu zyskujemy na czytelności.</p>
<h3 id="wielodziedziczenie-a-kompozycja">(Wielo)dziedziczenie, a kompozycja</h3>
<p>Nie jestem zwolennikiem dziedziczenia jako mechanizmu składającego funkcjonalności, gdyż więcej i czyściej można zrobić stosując kompozycję obiektów. W tym przypadku, czysto teoretycznie, również można rozwiązać ten problem kompozycją.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ProductRepositoryTest</span> <span class="k">extends</span> <span class="nx">PHPUnit_Framework_TestCase</span> <span class="p">{</span>
<span class="k">private</span> <span class="nv">$repository</span><span class="p">;</span>
<span class="k">private</span> <span class="nv">$productFactory</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">__construct</span><span class="p">(</span>
<span class="nx">ProductRepository</span> <span class="nv">$productRepository</span><span class="p">,</span>
<span class="nx">ProductFactory</span> <span class="nv">$productFactory</span>
<span class="p">)</span> <span class="p">{</span>
<span class="c1">//...
</span> <span class="p">}</span>
<span class="c1">//testy
</span><span class="p">}</span>
<span class="k">interface</span> <span class="nx">ProductFactory</span> <span class="p">{</span>
<span class="k">function</span> <span class="nf">insertProduct</span><span class="p">(</span><span class="nx">Product</span> <span class="nv">$product</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Mamy klasę testu, którą możemy parametryzować konkretnymi implementacjami <code class="highlighter-rouge">ProductRepository</code> oraz <code class="highlighter-rouge">ProductFactory</code> (tworzy dane testowe). Problem otaczania testów transakcją można rozwiązać za pomocą listenera, a całość sklejać za pomocą kontenera wstrzykiwania zależności. Jednak PHPUnit takiego rozwiązania nie obsługuje natywnie, trzeba by było mocno grzebać i napisać swoje rozszerzenia. Czy warto? Wg mnie nie. Przewaga kompozycji nad (wielo)dziedziczeniem jest bezdyskusyjna, ale w przypadku testów nie ma większych różnic.</p>
<p>Kompozycję stosujemy m. in. dlatego aby mieć mniejsze, łatwiej testowalne, spójne, z jedną odpowiedzialnością klocki, z których budujemy bardziej złożone struktury wykorzystując składanie obiektów, a nie dziedziczenie (większa elastyczność, brak eksplozji kombinatorycznej bytów itp.). Testów się nie testuje (w sensie pisania testów do testów, można “testować” np. <a href="http://pl.wikipedia.org/wiki/Testowanie_mutacyjne">testami mutacyjnymi</a>), nie ma potrzeby aby mieć możliwość “składania” testów w runtime, może być to osiągnięte dziedziczeniem. Fabryki danych testowych faktycznie mogą być wydzielone do osobnych, spójnych klas (aby można było je używać w wielu klasach testowych), ale tworzenie obiektów fabryk może być zahardcodowane np. w metodzie <code class="highlighter-rouge">setUp</code>.</p>
<h3 id="podsumowanie">Podsumowanie</h3>
<p>Pisząc testy zaślepiamy nieporęczny obiekt Stubem/Fakem - na początku z zaślepioną tylko jedną metodą która jest nam akurat potrzebna. Często bywa tak, że po jakimś czasie ten Stub/Fake staje się kompletną implementacją, gdyż wraz powstawaniem kolejnych testów, każda lub większość z metod Stuba została poprawnie zaimplementowania. Wtedy należy się zastanowić, czy nie należy pokryć tego Stuba testami, gdyż od poprawności jego działania zależy, czy testy które z niego korzystają są poprawne. Test bezgranicznie ufa Stubowi, my nie powinniśmy - więc jeśli zawiera on jakąś logikę, powinno się go również testować. Jak? Np. technikami które przedstawiono wyżej.</p>
<p>Jeden TestCase dla wielu testowanych klas nie ogranicza się tylko do przypadku stubów testowych, czy zaślepianiu baz danych. Ten przypadek można uogólnić do sytuacji, gdzie mamy kilka implementacji tego samego interfejsu.</p>psliwaNa początku nakreślę problem. Mamy klasę Product oraz interfejs ProductRepository.Biblioteka programisty2014-10-20T00:00:00+00:002014-10-20T00:00:00+00:00http://psliwa.org/biblioteka-programisty<p>Programista nie małpa, czytać musi. Wrzucam listę książek z niezwykle prestiżowym, moim certyfikatem jakości. Nie zamieszczałem tytułów, których nie przeczytałem, a wiem że są świetne i są na mojej liście do przeczytania. Kolejność nie ma większego znaczenia. Jeśli chcesz dodać coś od siebie, to są komentarze pod tym wpisem.</p>
<h4 id="programowanie-i-projektowanie-obiektowe">Programowanie i projektowanie obiektowe</h4>
<ul>
<li><a href="http://helion.pl/ksiazki/php5-zaawansowane-programowanie-edward-lecky-thompson-heow-eide-goodman-steven-d,php5zp.htm">PHP5. Zaawansowane programowanie</a> - pomocna jeśli chce się wejść w OOP znając programowanie strukturalne</li>
<li><a href="http://helion.pl/ksiazki/php5-obiekty-wzorce-narzedzia-matt-zandstra,php5ob.htm">PHP obiekty, wzorce, narzędzia</a> - jak wyżej, jest dostępne nowsze wydanie</li>
<li><a href="http://helion.pl/ksiazki/uml-i-wzorce-projektowe-analiza-i-projektowanie-obiektowe-oraz-iteracyjny-model-wytwarzania-aplikac-craig-larman,umwzo3.htm">UML i wzorce projektowe</a> - wbrew tytułowi, bardzo dobra książka o OOP oraz OOA</li>
<li><a href="http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/">Domain Driven Design</a> - klasyka DDD, raczej każdy powinien ją przeczytać</li>
<li><a href="http://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/">Implementing Domain-Driven Design</a> - świetne uzupełnienie książki Erica Evansa, ten sam poziom merytoryczny i wiele nowych pomysłów i patternów</li>
</ul>
<h4 id="refaktoryzacja">Refaktoryzacja</h4>
<ul>
<li><a href="http://helion.pl/ksiazki/refaktoryzacja-ulepszanie-struktury-istniejacego-kodu-martin-fowler-kent-beck-john-brant-william-opdy,refuko.htm">Refaktoryzacja</a> - klasyka, refaktoryzacje niskiego poziomu. Uczy refaktoryzacji używając małych kroków</li>
<li><a href="http://helion.pl/ksiazki/refaktoryzacja-do-wzorcow-projektowych-joshua-kerievsky,refawp.htm">Refaktoryzacja do wzorców projektowych</a> - uzupełnienie powyższej pozycji, refaktoryzacje wyższego poziomu</li>
</ul>
<h4 id="dobre-praktyki-i-samorozwój">Dobre praktyki i samorozwój</h4>
<ul>
<li><a href="http://helion.pl/ksiazki/czysty-kod-podrecznik-dobrego-programisty-robert-c-martin,czykod.htm">Czysty kod</a> - klasyka, o dobrym kodzie i o tym jak zamienić zły kod w dobry</li>
<li><a href="http://helion.pl/ksiazki/pragmatyczny-programista-od-czeladnika-do-mistrza-andrew-hunt-david-thomas,pragpr.htm">Pragmatyczny programista</a> - wiele dobrych praktyk i technik</li>
<li><a href="http://helion.pl/ksiazki/mistrz-programowania-zwieksz-efektywnosc-i-zrob-kariere-neal-ford,mispro.htm">Mistrz programowania</a> - o produktywności i dobrych praktykach</li>
<li><a href="http://www.amazon.com/Apprenticeship-Patterns-Guidance-Aspiring-Craftsman/dp/0596518382/">Apprenticeship Patterns: Guidance Aspiring Craftsman</a> - książka o ścieżkach samorozwoju, dla początkujących jak i bardziej zaawansowanych programistów</li>
<li><a href="http://www.amazon.com/Things-Every-Programmer-Should-Know/dp/0596809484/">97 Things Every Programmer Should Know</a> - zbiór 97, w większości dobrych, artykułów</li>
<li><a href="http://www.amazon.com/Clean-Coder-Conduct-Professional-Programmers/dp/0137081073/">The Clean Coder</a> - Książka autora “Czysty Kod”, m. in. o profesjonalizmie, rozwoju, ale nie o kodzie. Jest dostępne polskie wydanie</li>
</ul>
<h4 id="wzorce-projektowe">Wzorce projektowe</h4>
<ul>
<li><a href="http://helion.pl/ksiazki/wzorce-projektowe-elementy-oprogramowania-obiektowego-wielokrotnego-uzytku-erich-gamma-richard-helm-ralph-johnson-john-m,wzoele.htm">Wzorce projektowe</a> - klasyka, którą trzeba przeczytać w pierwszej kolejności</li>
<li><a href="http://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420/">Patterns Enterprise Application Architecture</a> - wiele wzorców z różnych warstw aplikacji, zwłaszcza rozdział z wzorcami do ORMów jest świetny. Czytałem polskie wydanie, które ma bardzo słabe tłumaczenie, więc polecam anglojęzyczne wydanie</li>
<li><a href="http://helion.pl/ksiazki/j2ee-wzorce-projektowe-wydanie-2-deepak-alur-john-crupi-dan-malks,j2eew2.htm">J2EE Wzorce projektowe</a> - wiele dobrych wzorców, jednak część z nich jest specyficzna tylko dla środowiska Java EE</li>
<li><a href="http://helion.pl/ksiazki/uml-i-wzorce-projektowe-analiza-i-projektowanie-obiektowe-oraz-iteracyjny-model-wytwarzania-aplikac-craig-larman,umwzo3.htm">UML i wzorce projektowe</a> - książka głównie o OOP i OOA, ale również porusza temat wielu wzorców</li>
<li><a href="http://helion.pl/ksiazki/refaktoryzacja-do-wzorcow-projektowych-joshua-kerievsky,refawp.htm">Refaktoryzacja do wzorców projektowych</a> - wzorce z innego punktu widzenia</li>
</ul>
<h4 id="testowanie">Testowanie</h4>
<ul>
<li><a href="http://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530/">Test-Driven Development by Example</a> - klasyka, jest polskie wydanie - jednak nie wiem jakiej jakości. Świetny przykład tworzenia narzędzia xUnit z wykorzystaniem TDD nie mając jeszcze frameworku do testowania (bo jesteśmy w trakcie jago pisania)</li>
<li><a href="http://www.amazon.com/xUnit-Test-Patterns-Refactoring-Code/dp/0131495054/">xUnit Test Patterns: Refactoring Test Code</a> - zwłaszcza pierwsza 1/3 książki jest świetna, reszta trochę przegadana i wiele Copy&Paste. Jednak warto nabyć dla tej 1/3 zawartości.</li>
<li><a href="http://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/">Growing Object-Oriented Software, Guided by Tests</a> - świetne uzupełnienie TDD by Example. Pokazuje praktyczne podejście TDD na większą skalę i wiele innych aspektów testowania</li>
</ul>
<h4 id="języki-programowania">Języki programowania</h4>
<ul>
<li><a href="http://helion.pl/ksiazki/javascript-mocne-strony-douglas-crockford,jscmoc.htm">Javascript: mocne strony</a> - fajna książeczka opisujące tą jasną stronę Javascriptu - jeśli piszesz coś w js, to musisz ją przeczytać</li>
<li><a href="http://helion.pl/ksiazki/java-podstawy-wydanie-ix-cay-s-horstmann-gary-cornell,javpd9.htm">Java: Podstawy</a> - dobra książka do rozpoczęcia nauki Javy</li>
<li><a href="http://helion.pl/ksiazki/java-techniki-zaawansowane-wydanie-ix-cay-s-horstmann-gary-cornell,javtz9.htm">Java: Techniki zaawansowane</a> - uzupełnienie powyższej pozycji</li>
<li><a href="http://helion.pl/ksiazki/java-efektywne-programowanie-wydanie-ii-joshua-bloch,javep2.htm">Java Efektywne programowanie</a> - świetna książka na temat dobrego kodu napisanego w Javie</li>
<li><a href="http://helion.pl/ksiazki/scala-od-podszewki-joshua-suereth-d,scalao.htm">Scala od podszewki</a> - nie jest to książka o podstawach Scali, a o jej bardziej zaawansowanych mechanizmach. Warto przeczytać gdy chce się poznać Scalę, zapewniam że głowa nie raz będzie swędziała podczas lektury ;)</li>
</ul>
<h4 id="programowanie-funkcyjne">Programowanie funkcyjne</h4>
<ul>
<li><a href="http://www.bookdepository.com/Real-World-Functional-Programming-Tomas-Petricek/9781933988924">Real-World Functional Programming</a> - świetna książka o programowaniu funkcyjnym. Warto przeczytać aby na nowo znaleźć się w piaskownicy. Do książki drukowanej jest prezent - wersja elektroniczna w formatach pdf, epub i mobi - lubię to! Nie należy zrażać się przykładami, które są w C# i F#. Tak swoją drogą, F# wygląda bardzo ciekawie ;)</li>
<li><a href="http://helion.pl/ksiazki/scala-od-podszewki-joshua-suereth-d,scalao.htm">Scala od podszewki</a> - w tej książce jest również wiele o programowaniu funkcyjnym w bardziej zaawansowanym wydaniu</li>
</ul>
<h4 id="bezpieczeństwo">Bezpieczeństwo</h4>
<ul>
<li><a href="http://www.amazon.com/The-Tangled-Web-Securing-Applications/dp/1593273886">The Tangled Web</a> - porusza tematy bezpieczeństwa aplikacji webowych oraz protokołu http. Autor jest Polakiem, więc jest również polskie wydanie</li>
<li><a href="http://helion.pl/ksiazki/cisza-w-sieci-michal-zalewski,bekomp.htm">Cisza w sieci</a> - o bezpieczeństwie sieci i protokołów</li>
<li><a href="http://helion.pl/ksiazki/metasploit-przewodnik-po-testach-penetracyjnych-david-kennedy-jim-o-gorman-devon-kearns-mati-ah,metasp.htm">Metasploit. Przewodnik po testach penetracyjnych</a> - daje wyobrażenie na temat ogólnego bezpieczeństwa systemów informatycznych, w tym aplikacji webowych. Dobre wprowadzenie do Metasploit</li>
</ul>
<h4 id="narzędzia">Narzędzia</h4>
<ul>
<li><a href="http://git-scm.com/book">Pro Git</a> - świetny podręcznik git, dostępna darmowa wersja online, darmowa anglojęzyczna wersja mobilna oraz darmowa polskojęzyczna wersja mobilna (ale trzeba sobie samemu zbudować z repozytorium na githubie)</li>
</ul>psliwaProgramista nie małpa, czytać musi. Wrzucam listę książek z niezwykle prestiżowym, moim certyfikatem jakości. Nie zamieszczałem tytułów, których nie przeczytałem, a wiem że są świetne i są na mojej liście do przeczytania. Kolejność nie ma większego znaczenia. Jeśli chcesz dodać coś od siebie, to są komentarze pod tym wpisem.Typ Option, czyli jak uniknąć Fatal error: Call to member function getTitle() on a non-object…2014-09-26T00:00:00+00:002014-09-26T00:00:00+00:00http://psliwa.org/option-type-w-php<p>Poprzedni mój <a href="http://psliwa.org/bo-obiekty-to-za-malo-czyli-o-programowaniu-funkcyjnym-w-php/">wpis</a> objaśniał podstawy programowania funkcyjnego, tym razem zajmę się wzorcem projektowym, który wywodzi się z języków funkcyjnych i jest szeroko stosowany w Haskellu, Scali, czy F#. Mowa tutaj o type <strong>Option</strong>. Inne nazwy tego patternu to Optional (java8), czy Maybe (Haskell) - podaję jabyście chcieli coś na ten temat wygooglować.</p>
<p>Nadmiar <code class="highlighter-rouge">ifów</code> w kodzie nie jest dobry, jest wiele sposobów które pozwalają zredukować ich liczbę. Takimi sposobami są niektóre wzorce takie jak State, czy NullObject. Wzorzec Option Type w php ma ograniczone zastosowanie, ale mimo to w wielu przypadkach jest użyteczny.</p>
<h3 id="przykład">Przykład</h3>
<p>Może na początek przykład, który będzie nam towarzyszył do końca artykułu. Jest on z grubsza zaczerpnięty z kodu produkcyjnego, więc jest jak najbardziej praktyczny i nie modeluje zoo.</p>
<p>Na początek zarysuję trochę dziedzinę problemu oraz sam problem. Mamy kanały, które agregują wiadomości, a każdy kanał należy do grupy kanałów.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ChannelGroup 1----* Channel 1----* Message
</code></pre></div></div>
<p>Grupa kanałów m. in. określa, czy w kanałach które do niej należą jest aktywne “zaawansowane targetowanie wiadomości” (cokolwiek to znaczy - dla przykładu nie jest to ważne). Mamy tablicę danych, w których prawdopodobnie jest wartość pod kluczem “channelId”, chcemy dodać coś do zapytania z Doctrine2 (np. join lub warunek where) z id grupy, gdy grupa podanego kanału ma włączone zaawansowane targetowanie.</p>
<p>W powyższym opisie występują słowa takie jak: “prawdopodobnie”, “gdy”, więc kilka <code class="highlighter-rouge">ifów</code> będzie niezbędnych.</p>
<p>Przykład kodu w stylu imperatywnym:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span><span class="p">(</span><span class="nb">isset</span><span class="p">(</span><span class="nv">$data</span><span class="p">[</span><span class="s1">'channelId'</span><span class="p">]))</span> <span class="p">{</span>
<span class="nv">$channel</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="na">channelRepo</span><span class="o">-></span><span class="na">find</span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="s1">'channelId'</span><span class="p">]);</span>
<span class="k">if</span><span class="p">(</span><span class="nv">$channel</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$group</span> <span class="o">=</span> <span class="nv">$channel</span><span class="o">-></span><span class="na">getGroup</span><span class="p">();</span>
<span class="k">if</span><span class="p">(</span><span class="nv">$group</span><span class="o">-></span><span class="na">hasAdvancedMessageTargeting</span><span class="p">())</span> <span class="p">{</span>
<span class="nv">$qb</span><span class="o">->...</span><span class="p">;</span><span class="c1">//Dodanie jakiegoś warunku do zapytania
</span> <span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>3 ify, w dodatku zagnieżdżone. Ten kod nie wygrałby konkursu piękności…</p>
<p>Ale działa.</p>
<p>Czy ten kod da się zapisać nie używając ani jednego wyrażenia warunkowego (ifa lub operatora trójkowego)? Tak, za pomocą typu <strong>Option</strong>.</p>
<h3 id="option">Option?</h3>
<p>Na wstępie podaję namiary na bardzo dobrą i zdatną do użytku implementację typu <code class="highlighter-rouge">Option</code> w php: <a href="https://github.com/schmittjoh/php-option">php-option</a>.</p>
<p>Zanim pokażę przykład, najpierw powiem czym jest typ <code class="highlighter-rouge">Option</code>, bo inaczej pokazany kod mógłby nie być wystarczająco zrozumiały. Option służy do zastąpienia pustych referencji, czyli wartości <code class="highlighter-rouge">null</code>. Ma 2 stany:</p>
<ul>
<li>gdy zawiera wartość - wtedy nazywamy taki option <code class="highlighter-rouge">Some(x)</code>, gdzie <code class="highlighter-rouge">x</code> to wartość opakowywana</li>
<li>gdy nie zawiera żadnej wartości - nazywamy go wtedy <code class="highlighter-rouge">None</code></li>
</ul>
<p>Wartość można opakować w typ Option za pomocą <code class="highlighter-rouge">Option::fromValue($value)</code>. Wartość zwrócona przez tą metodę będzie <code class="highlighter-rouge">Some($value)</code> gdy <code class="highlighter-rouge">$value</code> nie jest <code class="highlighter-rouge">nullem</code>, lub w przeciwnym wypadku <code class="highlighter-rouge">None</code>. Samo z siebie to nie byłoby zbyt użyteczne, ale <code class="highlighter-rouge">Option</code> ma szereg przydatnych metod. Dzięki nim możemy przekształcać ewentualnie przechowywaną wartość, filtrować ją, podawać wartości domyślne oraz oczywiście pobrać opakowaną wartość. Najważniejsze metody to: <code class="highlighter-rouge">Option map(callable)</code>, <code class="highlighter-rouge">Option filter(callable)</code>, <code class="highlighter-rouge">Option flatMap(callable)</code>, <code class="highlighter-rouge">Option orElse(Option)</code>, <code class="highlighter-rouge">mixed getOrElse(mixedDefaultValue)</code>, <code class="highlighter-rouge">mixed get()</code>, <code class="highlighter-rouge">void forAll(callable)</code>.</p>
<p>Poniższa tabelka przedstawia wartości zwrotne z poszczególnych metod w zależności od tego czy Option to <code class="highlighter-rouge">Some(x)</code>, czy <code class="highlighter-rouge">None</code>.</p>
<table>
<thead>
<tr><th>metoda</th><th>Some(x)</th><th>None</th></tr>
</thead>
<tbody>
<tr>
<td><code>map($func)</code></td>
<td><code>new Some($func($x))</code></td>
<td><code>$this</code> - czyli None</td>
</tr>
<tr>
<td><code>flatMap($func)</code></td>
<td><code>$func($x)</code><br />$func musi zwracać Option!</td>
<td><code>$this</code> - czyli None</td>
</tr>
<tr>
<td><code>filter($predicate)</code></td>
<td><code>$predicate($x)<br /> ? $this : new None</code></td>
<td><code>$this</code> - czyli None</td>
</tr>
<tr>
<td><code>orElse($option)</code></td>
<td><code>$this</code></td>
<td><code>$option</code></td>
</tr>
<tr>
<td><code>getOrElse($val)</code></td>
<td><code>$x</code> - opakowana wartość</td>
<td><code>$val</code></td>
</tr>
<tr>
<td><code>get()</code></td>
<td><code>$x</code> - opakowana wartość</td>
<td><code>throw new Exception()</code></td>
</tr>
</tbody>
</table>
<p>Metoda <code class="highlighter-rouge">forAll</code> działa jak <code class="highlighter-rouge">map</code>, z tą różnicą, że wartość zwrotna jest ignorowana. Można ją użyć, gdy chcemy wykonać kawałek kodu, gdy Option przechowuje jakąś wartość.</p>
<p>Przykładowo jeśli chcemy wyświetlić autora książki, nie wiedząc czy zmienna która reprezentuje książkę jest <code class="highlighter-rouge">nullem</code>, możemy to zrobić tak:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">echo</span> <span class="nx">Option</span><span class="o">::</span><span class="na">fromValue</span><span class="p">(</span><span class="nv">$book</span><span class="p">)</span>
<span class="o">-></span><span class="na">map</span><span class="p">(</span><span class="nx">F</span><span class="o">::</span><span class="na">prop</span><span class="p">(</span><span class="s1">'author'</span><span class="p">))</span>
<span class="o">-></span><span class="na">getOrElse</span><span class="p">(</span><span class="s1">'No book, no author...'</span><span class="p">);</span>
</code></pre></div></div>
<p>Użyłem funkcji <code class="highlighter-rouge">F::prop('author')</code> (z <a href="https://github.com/psliwa/rfp/blob/master/src/Rfp/F.php">rfp</a>), która tworzy funkcję zwracającą atrybut <code class="highlighter-rouge">author</code> obiektu podanego jako argument. Mógłbym użyć funkcji anonimowej, ale niestaty w php nie są one zwięzłe i unikam ich gdzie tylko mogę. W dalszej części wpisu również będę korzystał z innych funkcji z <a href="https://github.com/psliwa/rfp/blob/master/src/Rfp/F.php">rfp</a> (np. <code class="highlighter-rouge">F::pipe</code>), zachęcam do zapoznania sięz <a href="http://psliwa.org/bo-obiekty-to-za-malo-czyli-o-programowaniu-funkcyjnym-w-php/">poprzednim wpisem</a>, bo pomoże on Wam w jakimś stopniu nabrać “funkcyjnego myślenia” i dzięki temu przykłady z tego wpisu będą bardziej zrozumiałe.</p>
<p>Wracając do tematu, w tym kodzie jest jeden problem, a mianowicie co się stanie gdy książka nie ma autora? Metoda <code class="highlighter-rouge">map</code>, wg tabelki wyżej, zawsze zwraca <code class="highlighter-rouge">Some</code>, tak więc zostanie zwrócone <code class="highlighter-rouge">Some(null)</code> - nie jest to pożądane. Funkcję <code class="highlighter-rouge">map</code> używa się wtedy, gdy mamy pewność że funkcja mapująca nie zwraca <code class="highlighter-rouge">null</code>. Jeśli funkcja mapująca może zwrócić <code class="highlighter-rouge">null</code> używamy funkcji <code class="highlighter-rouge">flatMap</code>. Ale… Bardziej spostrzegawczy czytalnik zauważył, że wg magicznej tabeli, funkcja przekazana do <code class="highlighter-rouge">flatMap</code> musi zwrócić <code class="highlighter-rouge">Option</code>… Mamy 2 wyjścia: 1) atrybut <code class="highlighter-rouge">author</code> obiektu <code class="highlighter-rouge">$book</code> będzie miał wartość <code class="highlighter-rouge">Option</code>, czyli <code class="highlighter-rouge">Option</code> przesiąknie do naszego publicznego api, lub 2) opakujemy wartość zwrotną funkcji <code class="highlighter-rouge">F::prop('author')</code> w typ <code class="highlighter-rouge">Option</code>. Sposób 1) niekoniecznie musi być dobry. Jestem zdania, że typ <code class="highlighter-rouge">Option</code> w php nie powinien przesiąknąć do publicznego api, gdyż nie wiemy jakiego typu przechowywana jest wartość w <code class="highlighter-rouge">Option</code>, stracimy (i tak ograniczoną) kontrolę typów na poziomie języka oraz możliwość podpowiadania składni w IDE, gdyż w phpdoc nie ma czegoś takiego jak typy generyczne (np. <code class="highlighter-rouge">Option<Book></code>). Pozostaje sposób 2), jak to możemy zrobić? Za pomocą funkcji <code class="highlighter-rouge">F::pipe()</code> oraz <code class="highlighter-rouge">Option::fromValue</code>.</p>
<p>Bezpieczna wersja przykładu</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">echo</span> <span class="nx">Option</span><span class="o">::</span><span class="na">fromValue</span><span class="p">(</span><span class="nv">$book</span><span class="p">)</span>
<span class="o">-></span><span class="na">flatMap</span><span class="p">(</span>
<span class="nx">F</span><span class="o">::</span><span class="na">pipe</span><span class="p">(</span><span class="nx">F</span><span class="o">::</span><span class="na">prop</span><span class="p">(</span><span class="s1">'author'</span><span class="p">),</span> <span class="p">[</span><span class="nx">Option</span><span class="o">::</span><span class="na">class</span><span class="p">,</span><span class="s1">'fromValue'</span><span class="p">])</span>
<span class="p">)</span>
<span class="o">-></span><span class="na">getOrElse</span><span class="p">(</span><span class="s1">'No book or no author...'</span><span class="p">);</span>
</code></pre></div></div>
<p>Ok, wróćmy do naszego głównego przykładu.</p>
<h3 id="ok-to-wracamy-do-przykładu">Ok, to wracamy do przykładu</h3>
<p>Ok, wróciliśmy do przykładu.</p>
<p>Oto kod dodający coś do zapytania <code class="highlighter-rouge">$qb</code> pod pewnymi warunkami, zapisany z wykorzystaniem typu <code class="highlighter-rouge">Option</code>.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$option</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="nv">$func</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">F</span><span class="o">::</span><span class="na">pipe</span><span class="p">(</span><span class="nv">$func</span><span class="p">,</span> <span class="p">[</span><span class="nx">Option</span><span class="o">::</span><span class="na">class</span><span class="p">,</span><span class="s1">'fromValue'</span><span class="p">]);</span>
<span class="p">};</span>
<span class="nx">Option</span><span class="o">::</span><span class="na">fromArrayValue</span><span class="p">(</span><span class="nv">$data</span><span class="p">,</span> <span class="s1">'channelId'</span><span class="p">)</span>
<span class="o">-></span><span class="na">flatMap</span><span class="p">(</span><span class="nv">$option</span><span class="p">([</span><span class="nv">$this</span><span class="o">-></span><span class="na">channelRepo</span><span class="p">,</span><span class="s1">'find'</span><span class="p">]))</span>
<span class="o">-></span><span class="na">map</span><span class="p">(</span><span class="nx">F</span><span class="o">::</span><span class="na">prop</span><span class="p">(</span><span class="s1">'group'</span><span class="p">))</span>
<span class="o">-></span><span class="na">filter</span><span class="p">(</span><span class="nx">F</span><span class="o">::</span><span class="na">prop</span><span class="p">(</span><span class="s1">'advancedMessageTargeting'</span><span class="p">))</span>
<span class="o">-></span><span class="na">forAll</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="nx">ChannelGroup</span> <span class="nv">$group</span><span class="p">)</span> <span class="k">use</span><span class="p">(</span><span class="nv">$qb</span><span class="p">){</span>
<span class="nv">$qb</span><span class="o">->...</span><span class="p">;</span><span class="c1">//dodanie jakiegoś warunku do zapytania
</span> <span class="p">});</span>
</code></pre></div></div>
<p>Tak jak obiecałem - nie ma ifów, nie ma zagnieżdżonych bloków kodu. Zdefiniowałem funkcję <code class="highlighter-rouge">$option</code>, aby było czytelniej. Równie dobrze można sobie tą funkcję umieścić w jakiś funkcyjnych utilsach, bo będzie przydatna nie raz.</p>
<p>Wyrażenia warunkowe zostały zastąpione wywołaniami <code class="highlighter-rouge">Option::fromArrayValue</code>, <code class="highlighter-rouge">flatMap</code> oraz <code class="highlighter-rouge">filter</code>. Po każdym wywołaniu tych funkcji, może zostać zwrócone <code class="highlighter-rouge">Some(x)</code> lub <code class="highlighter-rouge">None</code>. Jeśli któraś z funkcji zwróci <code class="highlighter-rouge">None</code>, to reszta wywołań zwraca <code class="highlighter-rouge">None</code> nie robiąc nic ponadto. Funkcja <code class="highlighter-rouge">map</code> zawsze zwraca <code class="highlighter-rouge">Some(x)</code> - w tym przypadku mogłem ją użyć, bo każdy kanał musi należeć do grupy kanałów. Jeśli predykat w funkcji <code class="highlighter-rouge">filter</code> jest spełniony, to zwracane jest <code class="highlighter-rouge">Some(x)</code>, w przeciwnym wypadku <code class="highlighter-rouge">None</code>. Funkcja przekazana do <code class="highlighter-rouge">forAll</code> zostanie wywołana tylko gdy Option ma wartość, czyli jest <code class="highlighter-rouge">Some(x)</code>.</p>
<p>Tak, ten kod też działa.</p>
<h3 id="konkluzja">Konkluzja</h3>
<p>Na co dzień używam trochę prostszych słów na <strong>k</strong>, ale czego się nie robi dla nauki.</p>
<p>Typ <code class="highlighter-rouge">Option</code> W niektórych statycznie typowanych językach z typami generycznymi (Scala, #F) całkowicie zastąpił wartość null. W php jednak takie podejście, oprócz zalet, ma również poważne wady. Type hinting na poziome języka oraz specyfikacja phpdoc nie przewidziały typów generycznych = brak pomocy IDE przy obsłudze typ <code class="highlighter-rouge">Option</code>. Są również inne ograniczenia języka, które nie faworyzują tego podejścia względem wartości <code class="highlighter-rouge">null</code>. Tak więc, typ <code class="highlighter-rouge">Option</code> nie powinien być używany przy definiowaniu naszego api (nie powinien być zwracany czy przyjmowany jako parametr przez publiczne metody). Jednak nie samym api aplikacja żyje, <code class="highlighter-rouge">Option</code> doskonale odnajdzie się jako szczegół implementacyjny metod. Dzięki niemu można sprawić, że kod będzie czytelniejszy, bardziej deklaratywny. <code class="highlighter-rouge">Option</code> doskonale integruje się z innymi technikami funkcyjnymi, które również powinny być szczegółem implementacyjnym - tak więc nie pozostaje nic innego jak zaprząc je razem do tańca.</p>psliwaPoprzedni mój wpis objaśniał podstawy programowania funkcyjnego, tym razem zajmę się wzorcem projektowym, który wywodzi się z języków funkcyjnych i jest szeroko stosowany w Haskellu, Scali, czy F#. Mowa tutaj o type Option. Inne nazwy tego patternu to Optional (java8), czy Maybe (Haskell) - podaję jabyście chcieli coś na ten temat wygooglować.