Sytuacja kobiet w IT w 2024 roku
18.11.20197 min
Peter Matisko

Peter MatiskoIT specialist / Full-stack developerCyberma s.r.o.

Czym są zapachy kodu w PHP? Czysty kod z podejściem OOP

Sprawdź, jak wykryć najwięcej zapachów kodu i napisać czystszy i bezpieczniejszy kod, korzystając z tej wiedzy, na przykładzie PHP.

Czym są zapachy kodu w PHP? Czysty kod z podejściem OOP

Wielu programistów bardzo szybko zyskuje zbyt dużą pewność siebie odnośnie tego, w jaki sposób powinni programować. Czują się pewnie, ignorując ryzyko błędów. Moje podejście do kodowania polega na przestrzeganiu jak największej ilości obiektywnych zasad i polecanych jako najlepsze praktyk. 

Programowanie jest ulepszane od dziesięcioleci i istnieje wiele sprawdzonych koncepcji. Oszczędzamy sobie dużo czasu, ucząc się ich jeszcze przed użyciem po raz pierwszy w kodzie. Właściwie już kilka podstawowych zasad OOP pomaga nam pisać znacznie lepszy (bezpieczniejszy) kod - wygoogluj dla SOLID, DRY, SoC, wzorce, antywzorce.

Zapach kodu to termin opisujący podejrzane konstrukcje w kodzie. Mogą to nie być standardowe błędy, ale najprawdopodobniej kwestie problematyczne z perspektywy programowania obiektowego (OOP). W tym artykule wyróżnię te najczęstsze i pokażę na prostych przykładach, jak ulepszyć kod.

1. Zbyt wiele “and” w opisie funkcji

Za każdym razem, gdy opisujesz cel swoich klas, zwróć uwagę, ile razy używasz „and”. W miarę rozwoju kodu naturalnie mamy tendencję do umieszczania zbyt wielu funkcji w jednej klasie lub zbyt wielu funkcji w jednej metodzie.

Problem z dużymi klasami polega na tym, że są one trudne do zrozumienia, trudne do przetestowania i niezwykle trudne do rozszerzania. Pamiętaj, że jedną z najważniejszych zasad OOP jest zasada otwarte-zamknięte. Klasy powinny być zamknięte dla modyfikacji, ale otwarte dla rozszerzania. Każda klasa, która robi za dużo, jest bardzo trudna do rozszerzania przy jednoczesnym zachowaniu tej samej złożonej oryginalnej funkcji.

Przykład

Nie przepadam za systemem ORM Laravela - Eloquent, ponieważ modele Laravela robią o wiele za dużo. Przenosi dane i komunikuje się z bazą danych oraz mapują dane bazy danych na atrybuty modelu, obsługują zdarzenia itp. To zbyt wiele obowiązków dla jednej klasy.

Lepszym rozwiązaniem może być podział zadań klasy na następujące warstwy:

Kontroler - sprawdza poprawność danych przychodzących
Usługa - zawiera logikę biznesową
Model - zawiera dane
Repozytorium - przygotowuje dane do bazy danych
DBMapper - mapuje modele na wiersze bazy danych przy użyciu ModelMap, który prawidłowo odwzorowuje atrybuty modelu na kolumny bazy danych.

Modele żądań przychodzących nie powinny być ponownie wykorzystywane w repozytorium lub DBMapper. Powinny być wyłączną własnością kontrolera. Niezależnie od tego, jakie dalsze przetwarzanie musi nastąpić, mapując te modele na modele biznesowe i odwrotnie.

2. Słowo kluczowe „new” wewnątrz klas

Ilekroć widzisz “new” w klasie, prawdopodobnie jest to zapach kodu. Utworzenie klasy wewnątrz innej klasy tworzy bardzo ścisłe powiązanie między nimi. Trudno jest przetestować klasy osobno, ponieważ nie można zmienić zachowania klasy wewnętrznej. Ponadto, jeśli kiedykolwiek będziesz musiał rozszerzyć funkcję takiej klasy, trudno będzie Ci ją bezpiecznie zaimplementować.

Przykład

class WebSearch
{
   protected $httpClient;
   
   public function __constructor()
   { 
       $this->httpClient = new HttpClient();
   }
}


Jest to typowy przykład, w jaki sposób powstaje silne powiązanie między dwiema lub większą ilością klas. Właściwym sposobem użycia obiektu w innym obiekcie jest przekazanie go do konstruktora.

class WebSearch
{
   protected $httpClient;
   public function __constructor(HttpClient $httpClient)
   {
      $this->httpClient = $httpClient;
   }
}


Jest to właściwy sposób OOP, który zapewnia wiele korzyści w zakresie testowania, rozszerzania i czytelności. Jeśli korzystasz z dowolnego środowiska z kontenerem wstrzykiwania zależności (np. Laravel), może on automatycznie zapewniać wszystkie zależności dla Twoich klas. Dlatego w poprawnie napisanym projekcie w Laravel nie zobaczysz zbyt wielu wystąpień "new".

3. Naruszenie zasady mów, nie pytaj

Programowanie obiektowe uczy nas o klasach. Na początku może nie być jasne, jak korzystać z danych wewnętrznych klasy na zewnątrz. Wydaje się naturalne, że klasa dostarcza dane, edytuje je i odkłada z powrotem. Np.:

class User
{
   public $physicalData = [];
   public $age = 0;
}
$user = new User();
user->physicalData['height'] = 180;
user->physicalData['weight'] = 85;
$user->age++;


Takie podejście może wydawać się intuicyjne, ale w wielu przypadkach sprawia problemy. Jest to sytuacja, w której przydaje się zapamiętanie zasady „Mów, nie pytaj”. Dla naszej klasy reguła oznacza, że nie powinniśmy prosić klasy o dane, a następnie manipulować nimi poza klasą. O wiele lepiej jest powiedzieć klasie, co ma zrobić z danymi. Oto rozwiązanie powyższego przykładu:

class User
{
   private $physicalData = [];
   private $age = 0;
   public function increaseAge()
   {
      $this->age++;
   }
   public function getAge()
   {
      return age;
   }
   public function setPhysicalData(string $parameter, $value)
   {
      $this->physicalData[$parameter] = $value;
   }
   public function getPhysicalData($parameter)
   {
      return array_key_exists($parameter, $this->physicalData)
         ? $this->physicalData[$parameter]
         : null;
   }
}
$user = new User();
user->setPhysicalData('height', 180);
user->setPhysicalData('weight', 85);
$user->increaseAge();


W drugim przykładzie mówimy klasie, aby manipulowała danymi za nas. Przestrzeganie tej zasady jest znaczące. Nasza klasa najprawdopodobniej utrzyma ten sam interfejs. Klient klasy nie powinien dbać o wewnętrzną strukturę. Klient chce np. przechowywać niektóre dane, takie jak waga, wzrost itp. Nie musi wiedzieć, w jaki sposób dane są obsługiwane przez klasę. Może to być klasa, tablica, niezależne zmienne itp. Może to być nawet plik na dysku.

Aby powyższy przykład był jeszcze lepszy, możemy podzielić dane na osobne zmienne. Pozwoli nam to wprowadzić bezpieczeństwo typu i określić bardziej wprost, co mają robić funkcje.

class User
{
   private $height;
   private $weight;
   public function setHeight(int $height)
   {
      $this->height = $height;
   }
   public function setWeight(float $weight)
   {
      $this->weight = $weight;
   }
 
    … getters and other code
}

4. Kod jest trudny do odczytania

Zawsze mówię moim klientom, którzy zwykle nie są programistami, że powinni być w stanie odczytać kod w pewnym stopniu. By tak się stało, programista powinien odpowiednio komentować kod albo umiejętnie dzielić go na wiele funkcji z opisowymi nazwami.

Przykład

Porównaj te dwie wersje kodu i odpowiedz sobie, która z nich jest łatwiejsza do odczytania.

public function login($email, $user)
{
   …
   if(preg_match("/^([a-z0–9\+_\-]+)(\.[a-z0–9\+_\-]+)*@([a-z0–   9\-]+\.)+[a-z]{2,6}$/ix", $email) {
      Auth::login($user);
   }
   …
}


czy

public function login($email, $user)
{
   …
   if($this->isEmailValid()) { 
      Auth::login($user);
   } 
   …
}
private function isEmailValid($email) : bool
{
    return (preg_match("/^([a-z0–9\+_\-]+)(\.[a-z0–9\+_\-]+)*@([a-  z0–9\-]+\.)+[a-z]{2,6}$/ix", $email));
}


Może się wydawać, że tworzenie krótkich funkcji nie ma sensu. Dlaczego nie umieścić kodu w głównej funkcji? Jednak podział kodu popłaca lepszą czytelnością. Nie tylko dla kolegów z zespołu, testerów, ale nawet dla Ciebie, gdy spojrzysz na kod po kilku miesiącach.

Przykład

Bardzo często piszemy linie kodu, które podążają za naszymi myślami i nie poświęcamy zbyt dużo czasu na przemyślenie ich. Bardzo często powstaje coś takiego:

public function login($email, $user)
{
   if($this->isEmailValid()) {
      if(!$this->registerUser($email)) {
        echo('Registration error');
      }
   }
   return 'Error';
}


Widziałem kod z 5–7 zagnieżdżonymi poziomami if, foreach, while i większą ilością ifów. W przypadku tak złożonych konstrukcji zagnieżdżonych, możemy stwierdzić, że nie można tego łatwo zrozumieć. Jednak wielokrotnie możemy skutecznie zmniejszyć liczbę zagnieżdżonych warunków, stosując zanegowanie logiczne.

Możemy zakodować powyższy przykład w ten sposób:

public function login($email, $user)
{
   if(!$this->isEmailValid()) {
      return 'Error';
   }
   if(!$this->registerUser()) {
      echo('Registration error');
   }
}


Po prostu zmieniamy pierwszy warunek na negatywny i eliminujemy jeden poziom zagnieżdżenia. Kod jest teraz znacznie łatwiejszy do odczytania.

5. Kopiuj/wklej

Ilekroć chcesz użyć funkcji kopiuj/wklej, przestań klikać i pomyśl. Czy naprawdę musisz skopiować kod? Nie ma lepszego sposobu? Możliwość ponownego użycia kodu jest jedną z kluczowych zalet OOP. Zasada DRY - nie powtarzaj się, powinna zawsze być w naszej pamięci.

Widziałem witrynę z około trzydziestoma stronami, gdzie każda z nich miała ten sam nagłówek HTML. Co by się stało, gdyby klient zdecydował się zmienić favicon? Programiści musieliby edytować 30 plików! Każda praca manualna jest bardzo podatna na to, że coś pominiemy. Jest wysoce prawdopodobne, że podczas edycji trzydziestu stron coś Cię rozproszy i jedna lub więcej stron pozostanie niezaktualizowana. Wystarczy utworzyć osobny nagłówek i dołączyć go do wszystkich innych plików.

6. Kod jest za długi

Programiści często piszą kod tak, jak o nim myślą. Jest to naturalny sposób kodowania, który prowadzi do długich funkcji i kodu spaghetti. Trzy proste wskazówki znacznie ułatwiają odczytanie kodu.

Po pierwsze

Nie pisz funkcji dłuższych niż wysokość Twojego monitora. Jeśli musisz użyć przewijania w obrębie jednej funkcji, najprawdopodobniej jest ona zdecydowanie za długa.

Po drugie

Unikaj zagnieżdżonych cykli i warunków. Jeśli potrzebujesz więcej niż trzech poziomów zagnieżdżenia, to kod wymaga ponownego przemyślenia, bo najprawdopodobniej można to napisać lepiej.

Po trzecie

W opisie funkcji nie należy używać „and”, a przynajmniej niezbyt wiele razy.

Ostatnio widziałem funkcję, która: odbierała dane wejściowe, weryfikowała je, tworzyła i zapisywała modele oraz wysyłała powiadomienia e-mail. Za dużo „and”. Pamiętaj o SRP - zasadzie pojedynczej odpowiedzialności (ang. Single Responsibility Principle).

7. Tablice zamiast klas

Pisząc kod, oszczędzamy czas, używając prostych rozwiązań, nawet jeśli nie są najlepsze z punktu widzenia OOP. Jedną z takich intuicyjnych koncepcji jest użycie tablicy na wypadek, gdybyśmy musieli zwrócić więcej wartości. Tak jak tutaj:

Przykład

public function login($email, $user)
{
   if(!$this->isEmailValid()) {
      return 'Error';
   }
   if(!$this->registerUser()) {
      echo('Registration error');
   }
}


PHP nie dba o to, co zawiera tablica. Może to być wszystko - liczby, ciąg znaków, klasy, inna tablica - cokolwiek. To kolejny zapach kodu. Jeśli tablica zawiera kilka różnych typów, prawdopodobnie powinna być klasą. Tablice używane zamiast klas są niezwykle trudne do rozszerzenia. Nie dają żadnej funkcjonalności, a tylko przenoszą niektóre dane.

W naszym przykładzie możemy przetestować wynik pod kątem błędu:

$loginStatus = $this->login('[email protected]');
if($loginStatus->haserror()) {
   …do something
}


Ponieważ tablica nie może tego zrobić, będziemy zmuszeni zawsze testować określony element tablicy w ten sposób:

$loginStatus = $this->login('[email protected]');
if($loginStatus['error']) {
  …do something
}


Teraz nie można zmieniać nazw elementów tablicy. Może nie być oczywiste, co teraz faktycznie robi, a my mamy bardzo ograniczone opcje rozszerzania. Co więcej, każda próba aktualizacji obiektu z błędem, będzie problematyczna. Może nawet dojść do tego, że w całym kodzie będzie kilka obiektów z błędami.

Moje doświadczenie mówi mi, że ilekroć wpadnę w pułapkę „to tylko prosta tablica”, kończy się to źle. Polecam, aby nie być leniwym przy tworzeniu prostych klas. Utrzymujemy kod łatwiejszy do odczytania, przestrzegamy zasady otwarte-zamknięte i oszczędzamy dużo pracy w przyszłości.

<p>Loading...</p>