Około miesiąc temu pisałem o jednym z filarów programowania obiektowego (a konkretnie o abstrakcji). W poście zdefiniowałem abstrakcję jako:
Zamiast tego będziemy abstrahować pomysły do ich klas. I tutaj jest kluczowa idea: klasa powinna reprezentować rzeczownik.
I choć to wciąż prawda, idea klas abstrakcyjnych jest czymś innym w programowaniu obiektowym.
Brzmi dezorientująco, prawda? To znaczy:
- na jednym poziomie mamy abstrakcję definiowaną jako pomysł, że bierzemy ideę i reprezentujemy ją w klasie,
- na innym poziomie mamy klasy abstrakcyjne, które pomagają zdefiniować funkcje, które muszą zaimplementować podklasy.
A jeśli to nie jest wystarczająco zagmatwane, mieszamy to z interfejsami, które dostarczają kontraktu, które muszą następować, a następnie mieszamy z klasami abstrakcyjnymi, które definiują metody, które również muszą być zaimplementowane, ale mogą również implementować własne metody.
Zdezorientowany? Bez obaw. Celem kolejnych trzech postów jest wykonanie następujących czynności:
- Zdefiniuj czym są klasy abstrakcyjne,
- Opisz różnice w klasach abstrakcyjnych i interfejsach,
- Pomóż zdecydować, kiedy chcesz użyć jednego z drugim.
Powiedziawszy to, oto cała idea klas abstrakcyjnych.
Abstrakcyjne zachowanie
Przede wszystkim istnieje różnica w klasach abstrakcyjnych i abstrakcyjnych. Pierwsza odnosi się do idei reprezentowania czegoś w programowaniu; to ostatnie odnosi się do rzeczywistego sposobu pisania kodu.
A jednym z najlepszych sposobów, jakie znalazłem na myślenie o klasach abstrakcyjnych w programowaniu obiektowym, jest myślenie o nich w następujący sposób:
Klasy abstrakcyjne są substytutem implementacji.
Być może innym sposobem myślenia o nich są symbole zastępcze. Ostatecznie zapewniają zachowanie, które podklasy muszą zaimplementować.
Czym to się różni od interfejsu? Pamiętaj, że interfejs definiuje sygnaturę funkcji (nazwę funkcji, jej argumenty i jej modyfikatory widoczności), którą klasa musi zaimplementować.
Z drugiej strony abstrakcja zapewnia substytut implementacji, którą musi zaimplementować podklasa. Ale być może najlepiej jest to zademonstrować za pomocą kodu.
Abstrakcja w praktyce
Załóżmy, że pracujesz nad projektem i odkrywasz, że masz funkcjonalność, która istnieje w więcej niż jednym miejscu. Oprócz naruszania całej idei DRY, ma również potencjał, aby być miejscem, w którym można wyabstrahować funkcjonalność do klasy bazowej i użyć jej ponownie.
Rozważmy to w kontekście systemu wydawniczego. Niekoniecznie w taki sposób implementuje go WordPress, ale wykorzystuje ideę, którą znamy: taksonomie.
W WordPressie przypomnij sobie, że mamy Tagi i kategorie. Istnieją subtelne różnice między tymi dwoma (na przykład, czy jeden, jeśli jest hierarchiczny, czy nie), ale mają również podobne atrybuty, takie jak posiadanie imienia i ślimaka.
Abstrakcja taksonomiczna
Możemy więc zacząć od napisania abstrakcyjnej klasy Taxonomy, która abstrahuje wspólną funkcjonalność do własnej klasy.
<?php
abstract class Taxonomy
{
private $taxonomyName;
private $taxonomySlug;
public function __construct($name) {
$this->taxonomyName = $name;
$this->taxonomySlug = strtolower(str_ireplace(' ', '-', $this->taxonomyName));
}
public function getName() {
return $this->taxonomyName;
}
public function getSlug() {
return $this->taxonomySlug;
}
abstract protected function isHierarchical();
abstract protected function isCategory();
abstract protected function isTag();
}
W powyższym kodzie zobaczysz, że wykonałem następujące czynności:
- zadeklarował abstrakt klasy
- zdefiniował kilka atrybutów, które zostaną ustawione w konstruktorze
- realizował kilka funkcji publicznych,
- dodano kilka chronionych metod.
Kluczem od spojrzenia na tę klasę jest to, że każda klasa, która implementuje tę klasę abstrakcyjną, automatycznie będzie miała funkcjonalność zdefiniowaną w konstruktorze, funkcji getName i funkcji getSlug.
Nie będą jednak miały implementacji funkcji abstrakcyjnych. To jest to, co pozostało do zaimplementowania przez podklasy (które za chwilę udostępnię).
Konkretna taksonomia: tag
Teraz, gdy mamy zdefiniowaną klasę abstrakcyjną, można faktycznie zaimplementować abstrakcję. Na przykład :
<?php
class Tag extends Taxonomy
{
protected function isHierarchical() {
return false;
}
protected function isCategory() {
return $this->isHierarchical;
}
protected function isTag() {
return !$this->isHierarchical;
}
}
Zauważ, że w powyższym kodzie klasa zapewnia implementację funkcji abstrakcyjnych zdefiniowanych w klasie abstrakcyjnej (co jest określone przez funkcję extends w definicji klasy).
W dalszej części tego artykułu podzielę się, jak przetestować ten kod, ale zauważ, że powyższe nie tylko oferuje funkcjonalność, którą widzisz, ale także funkcjonalność klasy Taxonomy .
Konkretna taksonomia: kategoria
Zanim przyjrzę się temu w akcji, chcę również zdefiniować kategorię. Obejmuje to kod, który implementuje funkcje z klasy abstrakcyjnej, ale także funkcje własne.
<?php
class Category extends Taxonomy
{
private $parentId = -1;
protected function isHierarchical() {
return true;
}
protected function isCategory() {
return $this->isHierarchical;
}
protected function isTag() {
return !$this->isHierarchical;
}
public function setParentId($parentId) {
$this->parentId = $parentId;
}
public function getParentId() {
return $this->parentId;
}
}
Tutaj mamy wszystko, co pochodzi z klasy Taxonomy, ale zdefiniowaliśmy również naszą właściwość dla jej identyfikatora rodzica oraz metod pobierających i ustawiających. Choć w tym przypadku banalna, pokazuje, jak mogą funkcjonować kategorie, które są hierarchiczne.
Ponadto, jeśli kategoria nie ma rodzica, identyfikator jest ustawiony na -1, co ułatwia pisanie do automatycznego testowania, a nawet sprawdzanie, czy ma rodzica.
Widząc to w akcji
Aby zademonstrować cały kod, mam sedno, które zawiera cały kod w jednym pliku. Jako najlepsza praktyka nie polecam tego. Zamiast tego każda klasa powinna być przechowywana w osobnym pliku, a każda klasa powinna należeć do przestrzeni nazw.
Ale ponieważ służy to wyłącznie celom demonstracyjnym, wystarczy.
<?php
abstract class Taxonomy
{
private $taxonomyName;
private $taxonomySlug;
public function __construct($name) {
$this->taxonomyName = $name;
$this->taxonomySlug = strtolower(str_ireplace(' ', '-', $this->taxonomyName));
}
public function getName() {
return $this->taxonomyName;
}
public function getSlug() {
return $this->taxonomySlug;
}
abstract protected function isHierarchical();
abstract protected function isCategory();
abstract protected function isTag();
}
/*--*/
class Tag extends Taxonomy
{
protected function isHierarchical() {
return false;
}
protected function isCategory() {
return $this->isHierarchical;
}
protected function isTag() {
return !$this->isHierarchical;
}
}
/*--*/
class Category extends Taxonomy
{
private $parentId = -1;
protected function isHierarchical() {
return true;
}
protected function isCategory() {
return $this->isHierarchical;
}
protected function isTag() {
return !$this->isHierarchical;
}
public function setParentId($parentId) {
$this->parentId = $parentId;
}
public function getParentId() {
return $this->parentId;
}
}
/*- Tag Demo ----------------------------*/
$tag = new Tag('Acme Tag');
echo $tag->getName();
echo $tag->getSlug();
/*- Category Demo -----------------------*/
$category = new Category('Acme Category');
echo $category->getName();
echo $category->getSlug();
echo $category->getParentId();
$category->setParentId(100);
echo $category->getparentId();
Po uruchomieniu tego w konsoli powinieneś zobaczyć coś takiego jak następujące dane wyjściowe:
Być może będziesz musiał dodać kilka stwierdzeń echa, aby upewnić się, że tworzy nowe linie, ale to zależy od Ciebie.
A co z interfejsami?
W tym momencie:
- mamy roboczą definicję tego, czym są klasy abstrakcyjne,
- mamy przykład jak wyglądają klasy abstrakcyjne,
- i mamy działające demo tego, jak mogą działać.
Następnie zagłębię się w omówienie różnic między klasami abstrakcyjnymi a interfejsami, kiedy możesz chcieć użyć jednego z drugim lub kiedy możesz chcieć użyć ich w połączeniu ze sobą.