czwartek, 14 lipca 2011

Abstrakcja vs Interfejs


Klasy abstrakcyjne i interfejsy są bytami, które nie tylko ułatwiają pisanie kodu, ale także sprawiają, że kod jest czytelniejszy i bardziej przejrzysty, a logika aplikacji prostsza do zrozumienia. Ich poprawne i przemyślane zastosowanie z pewnością opłaca się, gdy aplikacja zaczyna się rozrastać i/lub zmieniać.
O tym co to jest interfejs oraz klasa abstrakcyjna można przeczytać w wikipedii. I choć zrozumienie tego, czym są nie jest wcale takie skomplikowane, to poprawne ich stosowanie nie jest czasami oczywiste.

Nie chcę tytułować się żadnym ekspertem w tej dziedzinie, ale po pewnym czasie spędzonym nad projektowaniem aplikacji i wdrażaniem tych rozwiązań, chciałem się podzielić wnioskami.
Kiedy więc powinno się stosować interfejsy i/lub klasy abstrakcyjne?
Pozwolę sobie to wszystko zaprezentować na prostym przykładzie:
<?php
class Man
{
  public function sleep() {/*...*/}
  public function eat() {/*...*/}
  public function doMenStuff() {/*...*/}
}

class Woman
{
  public function sleep() {/*...*/}
  public function eat() {/*...*/}
  public function doFemaleStuff() {/*...*/}
} 

W powyższym przykładzie mamy dwie klasy (Man i Woman), w których jesteśmy w stanie wyodrębnić pewną część wspólną. W takiej sytuacji warto rozważyć utworzenie nadklasy abstrakcyjnej po której będą dziedziczyły obie klasy. W tym wypadku takie rozwiązanie jest idealne:
<?php
abstract class Human
{
  public function sleep() {/*...*/}
  public function eat() {/*...*/}
}

class Man extends Human
{
  public function doMenStuff() {/*...*/}
}

class Woman extends Human
{
  public function doFemaleStuff() {/*...*/}
}

W końcu i mężczyźni i kobiety śpią i jedzą w ten sam sposób, przynajmniej w większości :) Jeżeli przyjrzymy się tym klasom bliżej, to możemy dojść do wniosku, że metody doMenStuff() i doFemaleStuff() są logicznie podobne, obie odpowiadają za wykonywanie czynności, jednak ich działanie jest zależne od konkretnej klasy. W takim wypadku można by się pokusić o jeszcze jedno przeprojektowanie kodu:
<?php
abstract class Human
{
  public function sleep() {/*...*/}
  public function eat() {/*...*/}
  abstract public function doStuff();
}

class Man extends Human
{
  public function doStuff() {/*...*/}
}

class Woman extends Human
{
  public function doStuff() {/*...*/}
}

Teraz na pierwszy rzut oka widzimy, że każda klasa dziedzicząca po Human musi zaimplementować ciało metody doStuff(). Oczywiście klasy mogą posiadać jeszcze atrybuty wspólne i rozłączne (pomijam wszelkiego rodzaju gettery i settery):
<?php
abstract class Human
{
  private $_age;
  public function sleep() {/*...*/}
  public function eat() {/*...*/}
  abstract public function doStuff();
}

class Man extends Human
{
  public function doStuff() {/*...*/}
}

class Woman extends Human
{
  private $_pregnanciesNumber;
  public function doStuff() {/*...*/}
}


Mamy teraz ładną hierarchię klas, spójną i logiczną. Na podstawie klasy abstrakcyjnej widzimy, że każda klasa pochodna musi posiadać zaimplementowaną metodę doStuff() oraz, że posiada kilka dodatkowych metod, które dziedziczy po klasie bazowej.
Załóżmy jednak, że przyszło nam do głowy dodać do całego kodu klasę Bed:
<?php
class Bed
{
  public function use($user)
  {
    $user->sleep();
  }
}

Oczywiście zakładamy, że z łóżka mogą korzystać tylko obiekty, na których jesteśmy w stanie wykonać metodę sleep(). W takim wypadku możemy kod klasy Bed zmienić na:
<?php
class Bed
{
  public function use(Human $user)
  {
    $user->sleep();
  }
}

Już lepiej, ale czy tak naprawdę parametr powinien być typu Human? Nam przecież zależy, żeby można było na tym obiekcie wykonać metodę sleep(), a w Human jest ich trochę więcej. I tutaj z pomocą przychodzą interfejsy, które powinny być używane wszędzie tam, gdzie należy zadeklarować metody niezbędne dla logiki działania. Oczywiście można do tego celu użyć również klasy abstrakcyjnej z tym, że ma to sens tylko, gdy wszystkie metody publiczne klasy abstrakcyjnej są wykorzystywane w danym miejscu aplikacji, czyli nasza klasa Human musiałaby wyglądać tak:
<?php
abstract class Human
{
  public function sleep() {/*...*/}
}

W innym wypadku (jeżeli zostaniemy przy aktualnym projekcie klasy Human z trzema metodami, w tym jedną abstrakcyjną) programista, który również chciałby napisać klasę korzystającą z obiektów klasy Bed uważałby, że do działania metody use() mogą być niezbędne również metody eat() i doStuff() klasy Human, co oczywiście byłoby błędne. Aby nie doprowadzić do takich nieporozumień stosuje się interfejsy. I tak nasz kod po całej tej analizie wygląda tak:
<?php
interface AbleToSleep
{
  public function sleep();
}

abstract class Human implements AbleToSleep
{
  private $_age;
  public function sleep() {/*...*/}
  public function eat() {/*...*/}
  abstract public function doStuff();
}

class Man extends Human
{
  public function doStuff() {/*...*/}
}

class Woman extends Human
{
  private $_pregnanciesNumber;
  public function doStuff() {/*...*/}
}

class Bed
{
  public function use(AbleToSleep $user)
  {
    $user->sleep();
  }
}

Teraz już na pierwszy rzut oka widać, że obiekty klasy Human mogą korzystać z instancji klasy Bed oraz jakie metody muszą implementować przyszłe klasy, aby również z nich korzystać.

2 komentarze:

  1. Samo słowo abstrakcja odstrasza od tego, jednak sposób i przykład w jaki to zobrazowałeś jest wręcz idealny, łatwo to zrozumieć.

    Oczywiście ma to sens tylko w wielkich aplikacjach, bo na tak małym przykładzie moim zdaniem tylko pogarsza się czytelność zamiast się poprawić.

    OdpowiedzUsuń
  2. Poprawne używanie OOP zawsze ułatwia zrozumienie kodu, a im dokładniej klasy odzwierciedlają rzeczywistość, typ prostsze staje się odnalezienie we wszystkim.

    Jeżeli całość jest poprawnie zaprojektowana, to nawet przy dużej ilości klas, nie ma większego problemu ze zrozumieniem logiki. A gdy posiadasz do tego wszystkiego odpowiednie diagramy, to praca z kodem jest czystą przyjemnością:)

    Oczywiście nie należy na siłę tworzyć niezliczonej rzeszy klas i interfejsów, bo w taki sposób prosta metoda może zamienić się w skomplikowany twór wymagający do działania setek obiektów.

    OdpowiedzUsuń