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.

Share Button

Skróć swoje listingi

Pewnie w niemal każdym swoim widoku index.ctp masz fragment kodu podobny do tego:

<?php
$i = 0;
foreach ($tracks as $track):
	$class = null;
	if ($i++ % 2 == 0) {
		$class = ' class="altrow"';
	}
?>
	<tr<?php echo $class;?>>
		<td>
			<?php echo $track['Track']['id']; ?>
		</td>
		<td>
			<?php echo $track['Track']['name']; ?>
		</td>
		<td>
			<?php echo $track['Car']['name']; ?>
		</td>
		<td>
			<?php echo $track['Track']['begin']; ?>
		</td>
		<td>
			<?php echo $track['Track']['end']; ?>
		</td>
		<td>
			<?php echo $track['Track']['length']; ?>
		</td>
		<td class="actions">
			<?php echo $html->link(
			   __('Edycja', true),
			   array(
			      'action' => 'edit',
			      $track['Track']['id'] ,
			      $track['Car']['id']
			   )
			  );
			?>

		</td>
	</tr>
<?php endforeach; ?>
</table>

Chce Ci się pisać ten kod kilkadziesiąt razy w jednym projekcie? Mnie też nie. Dlatego napisałem prosty element, który to trochę automatyzuje (nazwałem go “list”):

// app/views/elements/list.ctp
<?php
   $i = 0;
   foreach($elements as $key => $element):
   $class = null;
   if ($i++ % 2 == 0) {
      $class = ' class="altrow"';
   }
?>
   <tr <?php echo $class?>>
      <?php foreach($fields as $path): ?>
         <td>
            <?php echo Set::classicExtract($element, $path) ?>
         </td>
      <?php endforeach; ?>
         <?php if(isset($links)): ?>
            <td>
               <?php foreach($links as $name => $url): ?>
                  <?php
                     if(!is_array($url["params"])) {
                        $url["params"] = array($url["params"]);
                     }
                     foreach($url["params"] as $key => $path){
                        $url[$key] = Set::classicExtract(
                           $element,
                           $path
                        );
                     }
                     unset($url["params"]);
                  ?>
                  <?php echo $html->link($name, $url)?>
               <?php endforeach; ?>
            </td>
         <?php endif; ?>
   </tr>
<?php endforeach; ?>

Jego użycie jest dość proste. Dla przykładu wygenerujemy ten sam “output” dla pierwszego listingu z tego postu:

echo $this->renderElement(
         "list",
         array(
            "elements" => $tracks,
            "fields" => array(
               "Track.name", "Car.name", "Track.begin",
               "Track.end", "Track.length"
            ),
            "links"=> array(
               "edycja" => array(
                  "controller"=> "tracks", "action"=> "edit",
                  "params" => array(
                     "Track.id", "Car.id"
                  )
               )
            )
         )
      )

Opiszę poszczególne parametry przekazywane do tego elementu:

  • “elements” – kolekcja (w tym wypadku tablica) elementów zwrócona na przykład przez $this->Track->find(“all”)
  • “fields” – pola, które mają się pojawić w komórkach tabeli
  • “links” – tu definiujemy jakie linki mają się pojawić w ostatniej kolumnie (zazwyczaj tam znajdują się “opcje”). Indeksy kolejnych elementów to nazwy linków (tutaj “edycja”).
    • “controller” i “action” są identycznie używane jak w metodzie HtmlHelper::link()
    • “params” jest dość ciekawy. Jeśli chcemy w wygenerowanym linku mieć jeden parametr (najczęściej id), robimy to tak:
                  "edycja" => array(
                     "controller"=> "cars", "action"=> "edit", "params"=>"Car.id"
                  ),
      

      gdy parametrów ma być więcej – ścieżki do nich przekazujemy w tablicy (tak jak w przykładzie):

                      "edycja" => array(  
                         "controller"=> "tracks", "action"=> "edit",  
                         "params" => array(  
                            "Track.id", "Car.id"  
                         )  
                      ) 
      

      Można także używać parametrów typu “named”, wystarczy podać dodatkowo indeksy:

                      "edycja" => array(  
                         "controller"=> "tracks", "action"=> "edit",  
                         "params" => array(  
                           "parametr_1"=> "Track.id", "parametr_2"=> "Car.id"  
                         )  
                      ) 
      

Zachęcam do korzystania z tego elementu i zgłaszania wszelkich niedociągnięć :)

Share Button