piątek, 20 lipca 2012

Które metody umieścić w interfejsie?

krótki wstęp, a w nim o tym, czym jest interfejs

Do czego wykorzystuje się interfejsy? Do zapewnienia, że obiekty danej klasy będą posiadały określone metody. Po co chcemy mieć tą pewność? Ponieważ w danym miejscu ich posiadanie jest niezbędne do bezbłędnego wykonania pewnych instrukcji. Dlaczego wszystkie metody interfejsu są abstrakcyjne? Gdyż nie wymagamy określonego ich działania, wymagamy jedynie ich istnienia.

definicja definicją, ale co dalej?

Wstęp wyczerpująco wyjaśnia istotę interfejsu. I chociaż powyższa definicja nie jest długa, ani nazbyt skomplikowana, to umiejętność poprawnego stosowania interfejsów już wcale taka prosta nie jest.
Jednak warto jej się nauczyć. Mimo, że interfejsy to jedynie deklaracje metod, to są naprawdę potężną bronią w rękach doświadczonego programisty.

słowa słowami, ale daj nam kod!

Wyobraźmy sobie, że w naszej aplikacji, po zmianie adresu mailowego klienta, zostaje wysłany do niego email testowy, który wymaga potwierdzenia:
<?php
class Controller
{
  public function editCustomerEmailAddress(EmailAddress $emailAddress, Customer $customer)
  {
    $customer->changeEmailAddress($emailAddress)
    
    CustomersStore ::update($customer);

    MailService::sendTestEmail($customer);
  }
}


class CustomersStore extends Store
{
  public static function update(Customer $customer)
  {
    parent::updateRecord('Customers', $customer);
  }
}


abstract class Store
{
  public static function updateRecord($tableName, $customer)
  {
    self::getTable($tableName)->update($customer->getDataAsArray());
  }
}


class MailService
{
  public static function sendTestEmail($customer)
  { 
    self::sendEmail($customer->getEmailAddress(), $subject, $content);
  }
}
Co powyższy kod robi?
Najpierw wykonujemy na instancji klasy Customer metodę changeEmailAddress() w celu ustawienia nowe adresu emailego.
Następnie aktualizujemy informacje na temat klienta wykorzystując do tego celu klasę, która obsługuje nasz magazyn danych.
Na samym końcu wysyłamy mail na nowy adres w celu jego weryfikacji. W przykładzie pomijam budowanie tematu i treści maila testowego, ponieważ są one mało istotne.

gdzie te interfejsy?

W powyższym przykładzie, parametry metod nie zawierają żadnych informacji o typie. I to jest miejsce, w którym interfejsy pokażą swoją potęgę:)

no to wyślijmy maila na początek:)

Zacznijmy od metody MailService::sendTestEmail(). Oczywiście najprostszym rozwiązaniem jest ustawienie typu parametru na klasę Customer, ale czy rzeczywiście jest to dobre rozwiązanie?
Jak widzimy w przykładzie, wewnątrz metody wykorzystujemy tylko jedną metodę klasy Customer - getEmailAddress(). Tylko ta metoda jest nam niezbędna do wykonania wszystkich instrukcji, pozostałe - są zbędne, a więc nie ma żadnej potrzeby, aby typem parametru został Customer.

Co teraz?

To jest dobre miejsce, żeby skorzystać z interfejsu:
<?php
interface EmailReceiver
{
  /**
   * @return EmailAddress 
   */
  public function getEmailAddress();
}


class Customer implements EmailReceiver
{
  /* ... */
}


class MailService
{
  public static function sendTestEmail(EmailReceiver $emailReceiver)
  { 
    /* ... */
  }
}
Dzięki takiemu rozwiązaniu wiemy, że metoda sendTestEmail() (analogiczne typowanie powinno być dla sendEmail()) otrzymuje obiekty, które dostarczają wymaganą funkcjonalność, bez żadnej zbędnej nadmiarowości.

Dodatkowo, jeżeli któregoś pięknego dnia zechcemy w podobny sposób weryfikować zmiany adresu mailowego użytkowników naszej aplikacji, to nic nie stoi na przeszkodzie:)

te dane trzeba gdzieś zapisać!

Metoda update() klasy CustomersStore przyjmuje parametr typu Customer. Czy jest to dobre rozwiązanie skoro sama nie wykonuje na obiekcie żadnych operacji, tylko przekazuje go dalej, do metody rodzica? Tak, nie ma w tym nic złego, ponieważ klasa CustomersStore reprezentuje magazyn danych dotyczący klientów, więc odpowiednie typowanie parametrów przyjmowanych przez metody oraz przez nie zwracanych, jest jak najbardziej wskazane.

Co jednak z klasą Store? Jest ona abstrakcją, która (jak się domyślacie:) jest rodzicem dla całej masy potomków typu {model}Store. Tutaj trzeba również zastosować typowanie, ale na tyle dobre, aby nie było problemu z zapisem innych modeli.

Co takiego dzieje się wewnątrz niej? Pobiera konkretną tabelę i zapisuje w niej odpowiednie dane, które pobiera z obiektu za pomocą metody getDataAsArray(). I ta metoda powinna być częścią wspólną wszystkich naszych modeli, które możemy aktualizować:
<?php
interface Updateable
{
  /**
   * @return array
   */
  public function getDataAsArray();
}


class Customer implements EmailReceiver, Updateable
{
  /* ... */
}


abstrct class Store
{
  public static function updateRecord($tableName, Updateable $updateableModel)
  { 
    /* ... */
  }
}
Teraz mamy pewność, że każdy obiekt, który wyląduje w metodzie updateRecord() może być modyfikowany.

Dzięki temu rozwiązaniu, w łatwy sposób możemy dodatkowo tworzyć modele tylko do odczytu. Wystarczy, że nie będą one implementowały powyższego interfejsu:)

co dalej?

Dalej pozostaje już tylko praktyka:)

I taka moja mała rada na koniec - nie bójcie się interfejsów! Często odnoszę wrażenie, że programiści odczuwają mimowolny strach, że może ich być za dużo, zastanawiają się, czy można ich uniknąć bądź zmniejszyć ich ilość. Boją się interfejsów, ponieważ, według nich, one nic nie robią.

Może i nie robią, ale dają kontrolę, a to naprawdę jest coś, co warto mieć na uwadze projektując i pisząc swój kod.