Własny pingback – kiedy wysłać?

Ktoś, kogo śmiem nazywać “stałym” czytelnikiem tego bloga, czyli Kminek zadał mi ciekawe pytanie :

mam pewna zagwozdke – stworzylem mala platforme blogowa w Cake – cos a`la WordPress.com. przymierzam sie do implementacji protokolu Pingback. jest kwestia wysylania pingow – chyba raczej z punktu widzenia wydajnosci nie mozna tego robic przy zapisywaniu posta tylko uzyc taska shellowego, ktory co pewien czas bedzie to robil? jak sadzisz ?

Myślę, że tak jak sugerujesz, wysłanie pinga nie jest dobrym pomysłem. Sam pisząc posty czasem zapisuję je kilka razy zanim staną się one publicznie dostępne. Gdyby już przy pierwszym zapisie wysłać ping to byłby on wadliwy – prowadziłby do nieistniejącej strony.

Z drugiej strony wspomniany przez Ciebie task shellowy (rozumiem, że chodzi na przykład o crona) jest zadaniem dość kłopotliwym. Moim zdaniem komplikuje on architekturę aplikacji. Oczywiście zależy jak dużo do tego taska chcesz upchnąć? Jeśli parsowanie wpisu w poszukiwaniu linków etc. to moim zdaniem jest to zbyt wiele.

Moim pierwszym pomysłem było wysłanie pinga przy publikacji wpisu. Jednak gdyby w tekście znalazło się 20 różnych linków i wysyłać pingi przy publikacji – mogłoby to zbyt długo trwać i być niewygodne dla autora bloga (wyobrażam sobie sytuację, że przy 18 pingu następuje timeout wykonania skryptu i nie wiadomo czy post się opublikował i czy pingi się udały?).

Dlatego wybrałbym rozwiązanie kombinowane. Tzn. podczas publikowania wpisu (i update’u już opublikowanego) parsowałbym sobie tekst i znalezione linki umieszczałbym w tabeli pingbacks, która mogłaby wyglądać mniej więcej tak:

|----------------------------------------
| pingbacks
|----------------------------------------
| id: INT
| post_id: INT (albo permalink jeśli wolisz)
| uri : varchar (pingowany link)
| sent: tinyint(1) DEFAULT 0
|----------------------------------------

oczywiście przed zapisem sprawdzamy, czy już go nie ma w bazie ;). Przy takim rozwiązaniu świetnie powinien sprawdzić się Post::afterSave(). Wystarczy sprawdzić, czy zapisany model został z opublikowany i odpalić metodę Post::findAndSavePingbacks($postId).

Mam problem z tym, gdzie umieścić logikę wysyłania pingów. Myślę, że to też należałoby do warstwy Modelu (Pingback::send($limit)). Metoda taka pobrałaby kilka elementów z tabeli `pingbacks`, które nie zostały jeszcze wysłane i po udanym wysłaniu aktualizował pole `sent` (ustawiając 1).

Na koniec w kontrolerze zdefiniowałbym akcję, która miałaby za zadanie jedynie odpalić metodę Pingbacks::send()

class PingbacksController extends AppController{
  function send($limit=10){
     $this->autoRender = false;
     $this->Pingback->send($limit);
  }
}

I teraz jeśli mamy dostęp do crona to możemy zdefiniować na przykład akcję wget http://example.com/pingbacks/send
Jeśli nie mamy dostępu do takich luksusów – możemy wzbudzać tą akcję np. w AppController::afterFilter().

Co myślicie o tym rozwiązaniu?

Fat model, skinny controller – przykład

Niedawno przy projekcie trafiliśmy na ciekawy problem. Chciałbym się podzielić z Wami tym czego się nauczyliśmy.

Problem przedstawię w bardzo uproszczonej formie, bo trudno bez machania rękoma przy zabazgranej tablicy wyjaśnić go w całości.

Problem

Mamy formularz dodawania kosztów, który jest dość specyficzny. Można dodać “łysy koszt” i wtedy pojawia się formularz z wyborem firmy i innymi atrybutami. Można jednak z widoku konkretnej firmy wybrać opcję “dodaj koszt” – w takim wypadku lista wyboru firm się nie pojawia.

Dość naturalne jest załatwić to kontrolerem. Są różne możliwości, jedną z nich jest takie zdefiniowanie akcji:

function add($entryId=null) {
   if(!empty($this->data)){
       $this->Cost->save($this->data);
       // redirect w jakieś przyjemne okolice
   }
   if(is_null($entryId)){
      $this->set("entry", $this->Cost->Entry->read(null, $entryId);
   }else{
      $this->set("entries", $this->Cost->Entry->find("list");
   }
}

W widoku wiadomo – jeśli jest $entry, wyświetlimy $entry["Entry"]["name"] i w jakimś ukrytym inpucie wrzucimy id, jeśli istnieje $entries – wyświetlimy selecta.

Na razie w kontrolerze wygląda to wystarczająco zgrabnie, żeby tego nie ruszać. Ale jeśli znajdziesz się w sytuacji, gdy ilość przypadków jest coraz większa – możesz się zorientować, że dodajesz kolejne argumenty do metody add i rozbudowujesz tą metodę. W takiej sytuacji może Ci pomóc takie podejście.

Propozycja rozwiązania

Po pierwsze olej zwykłe parametry funkcji, a zacznij używać parametrów “named”. Czyli zamiast:
costs/add/1/3/45/23
costs/add/2///22
będziesz miał
costs/add/cost_id:1/entry_id:3/param3:45/param4:23
costs/add/cost_id:2/param4:22

Po drugie – przenieś logikę działania do modelu. np:

//model Cost
function giveMeProperData($params){
   if(isset($params["cost_id")){
      // do sth
      return $some_array;
   }
   if(isset($params["entry_id")){
      // do sth
      return $other_array;
   }
   return array();
}

Kontroler uprość do granic możliwości:

function add($entryId=null) {
   if(!empty($this->data)){
       $this->Cost->save($this->data);
       // redirect w jakieś przyjemne okolice
   }
   $this->data = $this->Cost->giveMeProperData($this->params["named"];
}

A w widoku w zależności od tego w jaki sposób wygląda $this->data renderuj widok.
Jeśli w tablicy jest “entry_id” to wyświetl zawartość $this->data["Entry"]["name"], a wartość $this->data["Entry"]["id"] przypisz do jakiegoś ukrytego pola. Itd.

Co zyskujesz?

  1. Kontroler robi to co do niego należy. Przestaje go interesować przypadek właśnie rozpatrywany. Jego interesuje tylko czy został przesłany formularz. Jeśli tak – spróbuj zapisać i przekierować, jeśli nie – pobierz dla niego dane i wyświetl (przekaż do widoku)
  2. Testowanie modeli, choć nie tak proste, jest o niebo łatwiejsze od testowania kontrolerów i widoków – możesz pokryć ważną część aplikacji testami. Możesz testować, czy metoda giveMeProperData() zwraca to, czego się spodziewasz dla konkretnych argumentów.
  3. Jeśli pokryjesz tą metodę testami będziesz mógł w komfortowych warunkach refaktoryzować ten kod – jak się pojawią powtórzenia wyodrębnisz wspólne części itd. Gdyby to siedziała w kontrolerze – nikt nie miałby ochoty tam zajrzeć, a testowanie polegało by głównie na odświeżaniu widoków w przeglądarce dla każdego z przypadków.
  4. Teraz widok jest klasą, która “ma wiedzę” na temat tego jak się zachować w zależności od otrzymanych danych (w prostych przypadkach to już się dzieje – chociażby w akcjach “edit”). Wcześniej część tej wiedzy znajdowała się w kontrolerze.
  5. Przyjemny side-effect wynikający z używania parametrów “named”: Teraz komponując link w widoku zamiast zastanawiać się czy formularz dodawania kosztów dla danej firmy jest pod /costs/add//[id_firmy]/ czy pod /costs/add///[id_firmy] – wiem, że jest pod /costs/add/entry_id:[id_firmy]

Jest oczywiście brak w tym rozwiązaniu – dość sporo kodu wędruje do widoku (który, jak wspomniałem, trudno testować). Jednak pozostawienie architektury w pierwotnym stanie tej kwestii nie rozwiązuje. Na początku masz problem z

  • dużą ilością kodu w kontrolerze
  • kodem kontrolera, który nie jest pokryty testami więc nie będzie refaktoryzowany
  • kodem w widoku, który nie jest pokryty testami

Po zastosowanej zmianie zostanie Ci tylko ostatni punkt. Samodzielnie możesz zdecydować, czy to się opłaca.

Dług techniczny – przykład z życia wzięty

Można się spierać co jest długiem technicznym, a co nie jest. Chciałbym Wam pokazać przykład kodu, który świetnie nadaje się do zobrazowania problemu.

Problem:
Na jeden ze stron jest wyszukiwarka- formularz, w którym po wpisaniu danych w inputach i selectach przesyła te dane jako parametry “named”, np.:
example.com/controller/action/field1:value1/field2:value2
Potrzebny jest kawałek kodu javascript, który wygeneruje odpowiedni link i wywoła ten adres

Rozwiązanie 1:

function submitform()
{
    form = document.getElementById('car_form');
    name = form.elements["CarName"].value;
    registration_number = form.elements["CarRegistrationNumber"].value;
    entry = document.getElementById('CarEntry').value
    driver = document.getElementById('CarDriver').value
    linkForm = 'url('/cars/find/')?>';
    location = linkForm+'name:'+name+'/registration_number:'+registration_number+'/entry:'+entry+'/driver:'+driver;
    document.forms["car_form"].action = location;
}

Jest ono poprawne. Warto się zastanowić, czy został w tym przypadku zaciągnięty dług techniczny? Na pewno będą tacy, którzy od razu będą wiedzieć co z tym kodem jest nie tak. Inni będą to czuli w kościach. Resztę pocieszę mówiąc, że ta umiejętność przychodzi z czasem pod warunkiem, że chcesz się uczyć jak pisać dobry kod.

Ten formularz wisi sobie gdzieś w jakimś panelu admina w listingu samochodów. Jednak taka wyszukiwarka jest potrzebna też przy listingu kierowców. Oczywiście kopiujesz rozwiązanie i poprawiasz je do nowych warunków. Nie ma w tym nic złego pod warunkiem, że robisz to z odpowiednim nastawieniem: “kopiuję kod, żeby móc zauważyć części wspólne i przeprowadzić refactoring” (czasem łatwiej mieć dwa dublujące się elementy i na ich podstawie tworzyć uniwersalny element/funkcje niż wymyślać ją od zera).

No i zabierając się za refactoring – w tym wypadku chciałbym zbudować wspólny element (dla tych co raczej siedzą w RoR – partial), który wywołam sobie tam, gdzie potrzebuję – zaczynam widzieć, że teraz oto przyjdzie mi spłacić ów mistyczny dług techniczny. Na czym on polega? Otóż w moim przykładzie funkcja budująca url jest strasznie “sztywna”. Działa w tym i tylko w tym przypadku. Nie ma najmniejszych znamion uniwersalności. Dlatego teraz, zanim zacznę myśleć o elemencie, muszę tą funkcję przebudować na bardziej uniwersalną. Co to znaczy?

Powinna być w stanie złapać wszystkie inputy i selecty, które są istotne przy budowaniu url’a i iterując po nich sprytnie go zbudować. Mogła by wyglądać na przykład tak:

Rowziązanie 2:

function submitform(){
    var params = "";

    $("#car_form input[type!=submit][name!=_method], #car_form select").each(
    	    function(index,element) {
        	    params +=
        	    		  element.name.substr(
        	    				  element.name.lastIndexOf("[")
        	    		  ).slice(1, -1)+
        	    		  ":"+
        	    		  element.value+
        	    		  "/";
    	    }
    );
    window.location.href = 'url('/cars/find/')?>' + params;
    return false;
}

Teraz inputy i selecty są zbierane bardziej automatycznie ("#car_form input[type!=submit][name!=_method], #car_form select"), a nazwy parametrów są brane z parametru name tych inputów i selectów. Dzięki temu powinna działać dla każdego formularza. Mniejsza o szczegóły.

Jednak rozwiązanie 1 nie było złe. Było wręcz idealne (bo proste i zrozumiałe) tak długo, jak było potrzebne w jednym miejscu. Rozwiązanie 2 mogło zająć przynajmniej 4 razy tyle czasu co pierwsze. Dlatego dopóki nie było powtórzeń kodu - było dobre. Dług techniczny nie istniał. Jednak pojawił się w momencie, kiedy zacząłem refaktoring kodu.

Jak to możliwe, że dług techniczny pojawił się w momencie, kiedy zacząłem go "spłacać"?
Czy istniał cały czas i był ukryty, a ja go nagle odkryłem?

Odpowiem na to pytanie nie wprost: Dług techniczny jest tylko pojęciem umożliwiającym zrozumienie ludziom, kŧórzy nie są programistami, dlaczego nie wolno im zachęcać programistów do chodzenia na skróty. Uzmysławia też (mam nadzieję) niedoświadczonym programistom jakie są niebezpieczeństwa związane z chodzeniem na skróty.

Leniwy Dobry, doświadczony programista w ogóle nie musi zagłębiać się w szczegóły czy dług techniczny istnieje, czy nie. Po prostu tworzy dobry kod.