Ogłoszenie publiczne:
Pragnę oświadczyć, że moim zdaniem cechy są prawie zawsze zapachem kodu i należy ich unikać na korzyść kompozycji. Moim zdaniem pojedyncze dziedzictwo jest często nadużywane do tego stopnia, że jest anty-wzorcem, a wielokrotne dziedziczenie tylko pogarsza ten problem. W większości przypadków będziesz lepiej obsługiwany, faworyzując kompozycję zamiast dziedziczenia (zarówno pojedynczego, jak i wielokrotnego). Jeśli nadal interesują Cię cechy i ich związek z interfejsami, czytaj dalej ...
Zacznijmy od tego, że:
Programowanie obiektowe (OOP) może być trudnym do uchwycenia paradygmatem. To, że używasz klas, nie oznacza, że Twój kod jest zorientowany obiektowo (OO).
Aby napisać kod OO, musisz zrozumieć, że OOP naprawdę dotyczy możliwości twoich obiektów. Musisz pomyśleć o zajęciach w kategoriach tego, co mogą zrobić zamiast tego, co faktycznie robią . Jest to wyraźny kontrast z tradycyjnym programowaniem proceduralnym, w którym nacisk kładziony jest na zrobienie odrobiny kodu „zrób coś”.
Jeśli kod OOP dotyczy planowania i projektowania, interfejs jest schematem, a obiekt to w pełni zbudowany dom. Tymczasem cechy są po prostu sposobem na pomoc w budowie domu określonego w planie (interfejsie).
Interfejsy
Dlaczego więc powinniśmy używać interfejsów? Po prostu interfejsy sprawiają, że nasz kod jest mniej kruchy. Jeśli wątpisz w to stwierdzenie, poproś każdego, kto został zmuszony do zachowania starszego kodu, który nie został napisany przeciwko interfejsom.
Interfejs jest umową między programistą a jego kodem. Interfejs mówi: „Tak długo, jak będziesz postępować według moich zasad, możesz mnie zaimplementować w dowolny sposób i obiecuję, że nie złamię twojego innego kodu”.
Jako przykład weźmy pod uwagę rzeczywisty scenariusz (bez samochodów i widżetów):
Chcesz wdrożyć system buforowania aplikacji sieci Web, aby zmniejszyć obciążenie serwera
Zaczynasz od napisania klasy do buforowania odpowiedzi na żądania przy użyciu APC:
class ApcCacher
{
public function fetch($key) {
return apc_fetch($key);
}
public function store($key, $data) {
return apc_store($key, $data);
}
public function delete($key) {
return apc_delete($key);
}
}
Następnie w obiekcie odpowiedzi HTTP sprawdzasz trafienie w pamięci podręcznej przed wykonaniem całej pracy w celu wygenerowania rzeczywistej odpowiedzi:
class Controller
{
protected $req;
protected $resp;
protected $cacher;
public function __construct(Request $req, Response $resp, ApcCacher $cacher=NULL) {
$this->req = $req;
$this->resp = $resp;
$this->cacher = $cacher;
$this->buildResponse();
}
public function buildResponse() {
if (NULL !== $this->cacher && $response = $this->cacher->fetch($this->req->uri()) {
$this->resp = $response;
} else {
// Build the response manually
}
}
public function getResponse() {
return $this->resp;
}
}
To podejście działa świetnie. Ale może kilka tygodni później zdecydujesz, że chcesz użyć systemu pamięci podręcznej opartego na plikach zamiast APC. Teraz musisz zmienić kod kontrolera, ponieważ zaprogramowałeś kontroler do pracy z funkcjonalnością ApcCacher
klasy, a nie z interfejsem, który wyraża możliwości ApcCacher
klasy. Powiedzmy, że zamiast powyższego sprawiłeś, że Controller
klasa polegała na konkretnym CacherInterface
zamiast na konkretnym ApcCacher
:
// Your controller's constructor using the interface as a dependency
public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL)
Aby to zrobić, zdefiniuj swój interfejs w następujący sposób:
interface CacherInterface
{
public function fetch($key);
public function store($key, $data);
public function delete($key);
}
Z kolei masz zarówno swoje, jak ApcCacher
i nowe FileCacher
klasy, CacherInterface
i programujesz swoją Controller
klasę, aby korzystała z możliwości wymaganych przez interfejs.
Ten przykład (mam nadzieję) pokazuje, w jaki sposób programowanie interfejsu pozwala zmienić wewnętrzną implementację twoich klas bez obawy, że zmiany spowodują uszkodzenie innego kodu.
Cechy
Natomiast cechy to po prostu metoda ponownego użycia kodu. Interfejsy nie powinny być traktowane jako wzajemnie wykluczająca się alternatywa dla cech. W rzeczywistości tworzenie cech, które spełniają funkcje wymagane przez interfejs, jest idealnym przykładem użycia .
Tych cech należy używać tylko wtedy, gdy wiele klas ma tę samą funkcjonalność (prawdopodobnie podyktowane tym samym interfejsem). Nie ma sensu używać cechy do zapewnienia funkcjonalności dla jednej klasy: to tylko zaciemnia to, co robi klasa, a lepszy projekt przeniósłby funkcjonalność cechy do odpowiedniej klasy.
Rozważ następującą implementację cechy:
interface Person
{
public function greet();
public function eat($food);
}
trait EatingTrait
{
public function eat($food)
{
$this->putInMouth($food);
}
private function putInMouth($food)
{
// Digest delicious food
}
}
class NicePerson implements Person
{
use EatingTrait;
public function greet()
{
echo 'Good day, good sir!';
}
}
class MeanPerson implements Person
{
use EatingTrait;
public function greet()
{
echo 'Your mother was a hamster!';
}
}
Bardziej konkretny przykład: wyobraź sobie oba FileCacher
i swojeApcCacher
z dyskusji interfejsu używamy tej samej metody, aby ustalić, czy pozycja pamięci podręcznej jest nieaktualna i powinna zostać usunięta (oczywiście tak nie jest w rzeczywistości, ale idź z nią). Możesz napisać cechę i pozwolić obu klasom na użycie jej do wspólnego wymagania interfejsu.
Ostatnie ostrzeżenie: uważaj, aby nie przesadzić z cechami. Często cechy są wykorzystywane jako narzędzie do kiepskiego projektowania, gdy wystarczą wyjątkowe implementacje klasy. Powinieneś ograniczyć cechy do spełnienia wymagań interfejsu dla najlepszego projektu kodu.