Stosuj zasady programowania funkcjonalnego

Do napisania tego postu zainspirował mnie artykuł na 97 rzeczy pt. Stosuj zasady programowania funkcjonalnego. Każdemu polecam przeczytanie go aby dowiedział się dlaczego należy stosować te zasady. Ja w tym poście będę chciał pokrótce przedstawić jak rady zawarte w artykule można odnieść do kodu w PHP i jakie korzyści odniesiemy stosując refaktoring poszczególnych rodzajów kodu.

Duchem całego programowania funkcjonalnego jest przezroczystość referencyjna – funkcja dla tych samych danych zwraca konsekwentnie ten sam wynik, aby tak było kod musi spełniać kilka zasad:

Pierwsza zasadą jest unikanie globalnego stanu. Często można natknąć się w skryptach na następujące konstrukcje „popełniane” przez początkujących:

$conn = mysql_connect();

function fetch_users(){
global $conn;
/**
reszta kodu
**/
}

Problem z tego rodzaju kodem jest taki, że jest on całkowicie nieprzewidywalny. Jeżeli zamkniemy gdzieś w naszym skrypcie zamkniemy połączenie do bazy, a potem wywołamy funkcję fetch_users, po prostu dostaniemy błąd. Oprócz tego, osoba postronna czytająca kod, w zasadzie nie jest w stanie wywnioskować co powinno znaleźć się w zmiennej $conn.

Są dwa rozwiązania refaktoringu takiego kodu, pierwszy:

$conn = mysql_connect();

function fetch_users($connection){
/**
kod
*/
}

Drugi:

class DbService {
    protected $_conn;
    public function __construct(){
       $this->_conn = mysql_connect();
    }

    public function fetch_users(){
         /** kod **/
    }

    public function __destruct(){
         mysql_close($this->_conn);
    }
}

Dzięki refaktoringowi nr.1 zyskaliśmy większą przewidywalność funkcji i czytelność. Dzięki refaktoringowi nr.2 uzyskaliśmy przewidywalność, abstrakcje i enkapsulacje.

Przykładem pogwałcenia tej zasady jest również wzorzec singleton (namiętnie używany w ZF) i rejestr.

class FooController extends Zend_Controller_Action {

    function barAction(){
        $baz = Zend_Registry->getInstance()->baz;
        // czym jest baz ???
    }

}

Jak widzimy powyżej, gdybyśmy recenzowali taki kod, nie jesteśmy bez uruchamiania skryptu w stanie stwierdzić, jakiego typu jest zmienna $baz, co więcej nie jesteśmy w stanie stwierdzić czy rejestr przechowuje coś pod tą zmienną. Refaktoring:

class FooController extends Zend_Controller_Action {

    function barAction(ModelUser $baz){
        $aList = $baz->getList();
        // baz jest instancją ModelUser, wiemy jakie ma metody i musi istnieć żeby ta metoda mogła być //wywołana 
    }

}

Oczywiście osobną kwestią jest jak wstrzyknąć zależności do kontrolera, ja bym zastosował do tego kontener IOC, ale to osobna bajka.

Drugą zasadą jest nieprzekazywanie przez referencję do funkcji:


$aFoo = array(1,2,3);

function fooBar(&$fooFoo){
    $fooFoo[0]+=5;
    if(end($fooFoo) == 3){
       $fooFoo[0]++;
    }
}
fooBar($aFoo);
echo current($aFoo);
//nie znając implementacji fooBar spodziewamy się otrzymać 7, otrzymujemy 3

Czyni to podobnie jak w pierwszym przypadku kod nieprzewidywalnym ale na odwrót – w pierwszym przypadku to globalny stan wpływał na funkcję przy przekazywaniu przez referencje funkcja wpływa na globalny stan. Poza tym funkcja, która nie zwraca konkretnej wartości jest nietestowalna. Refaktoring:


$aFoo = array(1,2,3);

function fooBar($fooFoo){
    $fooFoo[0]+=5;
    if(end($fooFoo) == 3){
       $fooFoo[0]++;
    }
    return $fooFoo;
}
$aFoo = fooBar($aFoo);
echo $aFoo[1];
//otrzymujemy spodziewany wynik

Wracając do testowalności będę musiał po raz kolejny zbesztać ZF. Jeżeli ktoś z Was próbował testować kontrolery w ZF, wie pewnie, że generalnie nie jest to łatwe. A gdyby mądrzy panowie z Zenda umożliwili pisanie kontrolerów tak:

class FooController extends Zend_Controller_Action {

    public function bazAction(){
        /** kod **/
    }

    public function barAction($request, ModelUser $user, ModelBar $bar){
       if($user->isGuest()){
           return new Zend_Event_Redirect('baz');
       }
       $aViewVariables = array($user->getList(), $bar->getBarList());
       return $aViewVariables;
    }
}

W takim układzie widać jak na dłoni i można w unit testach przetestować jakie jest zachowanie poszczególnych akcji kontrolera – tutaj jeżeli użytkownik jest gościem to zostaje przeniesiony do innej akcji, metoda zwraca obiekt Zend_Event_Redirect (oczywiście nie ma w tej chwili czegoś takiego), a w innym przypadku tworzona jest tablica z danymi które powinny zostać przekazane do widoku i również zwracana.

Zaletą takiego podejścia jest większa izolacja akcji kontrolerów, a co za tym idzie możliwość swobodnego wywoływania ich z innych akcji bez jakiś dziwnych zendowych efektów ubocznych typu odpalania całego stacku requestu z post i pre dispatchami.

Jak widać tak naprawdę zasady programowania funkcjonalnego mają odbicie w świecie programowania obiektowego w kilku wzorcach takich jak dependency injection, strategy pattern itp, co słusznie zauważył autor wspomnianego na wstępie artykułu.

Warto stosować te zasady, gdyż poprawiają one w sposób znaczny jakoś tworzonego kodu, promując pisanie mniejszych i lepiej zdefiniowanych metod i funkcji, które są w wysokim stopniu testowalne.

Na koniec jeszcze przedstawię dwie phpowe funkcje, które w prostej linii wywodzą się z języków funkcjonalnych:

Pierwszą z nich jest funkcja array_map (przykład z manuala):

function cube($n)
{
    return($n * $n * $n);
}

$a = array(1, 2, 3, 4, 5);
$b = array_map("cube", $a);
print_r($b);
/*wynik Array
(
    [0] => 1
    [1] => 8
    [2] => 27
    [3] => 64
    [4] => 125
)
*/

Wywołuje ona na każdym elemencie tablicy funkcję zwrotną „cube”, oczywiście od php 5.3 można przekazywać do niej również funkcje anonimowe oraz obiekty z metodą __invoke.

Druga funkcja to array_reduce, jako argument przyjmuje tablice i funkcje i służy do sprowadzania tablicy do jednej wartości skalarnej. Dla przykładu implementacja funkcji implode przy użyciu array_reduce:


$func = function ($resultString, $currentElement){
    $resultString.= '<>'. $currentElement;
    return $resultString;
}
$arr = array('ala','ma','kota');
echo array_reduce($arr, $func);
//output: ala<>ma<>kota
  1. Dobre I czytelne przykłady, programowanie funkcyjne (pozostanę przy tej nazwie, funkcjonalne brzmi fatalnie) to bardzo ciekawe podejście, dużo wnosi do klasycznego OOP, dlatego m.in. ostatnio wolę programować w js,niż w php, jakoś tak lżej jest 😉 Osobną kwestią jest pisanie tak, żeby kod był testowalny, tutaj słusznie-najważniejsze, żeby się pozbyć globali. Ale to często walka z wiatrakami, jeśli nie piszesz czegoś zupełnie od nowa…

  2. batman pisze:

    Standardowo nieco pomarudzę 😉
    Podane tutaj i na wskazanej stronie są podstawami programowania, które zna każdy programista piszący nie tylko w PHP. To, że jest takie coś jak global, to wina języka, który pozwala na takie kwiatki oraz programistów, którzy idą na skróty.

    Odnośnie testowania kontrolerów Zend Frameworka. Zacząłem się właśnie przegryzać przez Zend_Test i z tego co już udało mi się go poznać, to testowanie sprowadza się do napisania kodu, opisującego proces przejścia do konkretnej akcji.

    Singleton w ZF i innych tworach był po prostu modny w czasach, gdy one powstawały. Obecnie jest moda na IoC i wszyscy starają się do tej mody dopasować.

    Twój przykład z Zend_Event_Redirect, to świetny pomysł. Czegoś takiego brakuje w ZF i znacznie ułatwiłoby to wykrywanie odpowiedzi akcji i na tej podstawie, podejmowane byłoby odpowiednie działanie. Tak jak jest to zrobione w ASP.NET MVC.

  3. Wojciech Soczyński pisze:

    @batman, nie powiedział bym do końca, że to są takie podstawy, po prostu takie są cechy programowania proceduralnego, że stosuje się globale i przekazywanie przez referencje, przypomnij sobie młodzieńcze lata w C, tak to się tam robiło.

    Natomiast programowanie funk(cyjne/cjonalne) kładzie nacisk na jasny przepływ sterowania i jak największe rozdrobnienie problemu na poszczególne funkcje, które robią tylko jedną rzecz. Nawet w dobie obiektowości i całego szumu wobec abstrakcji i enkapsulacji spotykam się wszystko-robiącym kodem, który tak naprawdę jest kodem proceduralnym ubranym w klasy. To, że nam wydają się niektóre rzeczy oczywiste i podstawowe to nie znaczy, że tak samo jest dla innych.

    Co do mody na Singletony i Ioc wg. mnie nie jest to do końca moda a bardziej to, że po prostu ludzie w którymś momencie wchodzą na wyższy poziom abstrakcji i stwierdzają, że są lepsze rozwiązania, natomiast reszta to być może i małpuje.

    A co do globala, to nawet tak „świetnie zaprojektowany” język jak Python je ma więc musi być coś na rzeczy :>

  4. Ciekawy tekst. Od siebie dodam, że zamiast zwracać instancję Zend_Event_Redirect wolę… wyrzucić wyjątek o takiej nazwie. To wynika jednak z faktu, że trochę pisałem w językach statycznie typowanych i zwracanie danych różnych typów drażni mnie 😉 Z resztą – brak autoryzacji wygląda mi na wyjątkową sytuację, zatem pasuje jak ulał 😉
    Ostatnio przyglądam się zmianom w różnych FW, szczególnie w Symfony i ZF. Idą w dobrą stronę, tylko zapewne przyjdzie nam jeszcze dłuuuugo poczekać zanim się upowszechnią :(

    Pozdrawiam

  5. Wojciech Soczyński pisze:

    @michal bachowski: Z brakiem autoryzacji to masz racje może być i wyjątek, jednak to jest tylko przykład i miał na celu pokazanie jak można w sposób czytelny zrobić redirecta…, co zmian w ZF to czytałem post Mathew Weinera O’Phineya i stwierdzam, że jego koncepcja na nowe kontrolery nie przemawia do mnie w ogóle, co więcej uważam ją za całkowicie chybioną – bazuje na goto :>

  6. RobertG pisze:

    „Duchem całego programowania funkcjonalnego jest przezroczystość referencyjna – funkcja dla tych samych danych zwraca konsekwentnie ten sam wynik”

    A jeśli by napisać funkcję przyjmującą jako argument nazwę pliku i zwracającą liczbę linijek w nim, to dla tych samych danych wejściowych wyniki mogą być różne. Tak samo z generowaniem liczb losowych, ogólnie z IO, z obsługą socketów etc, generalnie wszędzie na styku algorytmów i rzeczywistego świata..

    Jak radzić sobie z tym w PHPie w sposób funkcyjny? Jest tu coś takiego jak monady w Haskellu?

  7. Wojciech Soczyński pisze:

    Monad raczej nie znajdziesz. Generalnie php, jak większość języków jest tworem hybrydowym, miksuje paradygmaty proceduralny, obiektowy i funkcjonalny. Nie da się uniknąć niestety tego typu niespodzianek, jakie przytoczyłeś w przykładzie, nie wszystkie funkcje mogą być „pure”. Pewnym rozwiązaniem tego problemu jest tworzenie kolejnych warstw abstrakcji oraz stosowanie testów jednostkowych, dzięki którym nasze funkcje zyskują jakiś stopień pewności…

  8. Pamiętajmy, że pisanie z wykorzystaniem klas nie daje absolutnie prawa do mówienia o OOP. Programowanie zorientowane obiektowo, to zdecydowanie coś więcej :)

    Chodzi o operowanie na interfejsie, nie implementacji. Dobry kod OOP [niezależnie od języka] jest czytelny i nie wymaga komentarzy poza „autor, data, klasa robiąca X”. Bo w bebechy klasy przeważnie nie trzeba zaglądać.

    Może w tym wpisie nie do końca to, o czym tu napisałem jest sednem, ale wynikowy kod daje to co jest ważne – interfejs.
    http://www.yarpo.pl/2010/12/12/przeciazanie-konstruktora/

    Spróbujcie wyobrazić sobie, że w samochodzie zamiast przyjaznego interfejsu [kierownica, pedały] macie rurkę do której na zmianę dmuchacie powietrze, albo wlewacie paliwo :)

    Wciskam gaz i jadę szybciej. A to, co się dzieje w środku – nieważne [dla programisty użytkownika. Twórca powinien był to zoptymalizować, przetestować, itd.]

  1. There are no trackbacks for this post yet.

Leave a Reply

Informuj mnie o odpowiedziach poprzez e-mail. Możesz również subskrybować wpis bez zostawiania komentarza.