sobota, 8 października 2011

Jak programować obiektowo cz. 2 - atrybuty klasy

wstęp

Na potrzeby wszystkich wpisów z tej serii zdecydowałem się, w miarę możliwości, skupić je wokół jednego przykładu. Oczywiście nie będę przeprowadzał żadnej analizy wymagań, ani specjalnie rozwodził się nad całą strukturą i pełną funkcjonalnością, będę poruszał jedynie te aspekty aplikacji, które będą bezpośrednio powiązane z danym wpisem. Dzięki takiemu praktycznemu przykładowi mam nadzieję, że uda mi się przedstawić myślenie, które kryje się za większością decyzji projektowych oraz pozbędę się zbędnego teoretyzowania i przykładów, które nijak nie pokrywają się z rzeczywistością.
Dzisiaj chciałbym skupić się na atrybutach klasy, więc na początek trochę teorii.

let's start

Atrybuty klasy są czymś, co odróżnia od siebie jej konkretne instancje, umożliwiają one identyfikację obiektów.
Można podzielić je na dwa rodzaje:
  • typów podstawowych: boolean, string, integer, float
  • obiekty, które realizowane są jako agregacja, częściowa lub całkowita (kompozycja)
Atrybuty typów podstawowych są to dane bezpośrednio dotyczące instancji obiektu, natomiast agregacje są to swego rodzaju jego części składowe np. atrybutem budynku jest jego wysokość (typ podstawowy), budynek jednak zawiera też pewną ilość mieszkań (obiekty).
Agregację dodatkowo można podzielić na:
  • częściowa - oznacza to mniej więcej tyle, że zawieranie nie jest konieczne dla istnienia obiektu głównego i zawieranego np. jabłko zawiera robaka (i jabłko i robak mogą sobie spokojnie istnieć bez siebie:)
  • kompozycja - oznacza to, że obiekty składowe nie mogą istnieć bez obiektu głównego ani nie mogą być współdzielone z innymi obiektami. Zostają one usunięta wraz z obiektem głównym np. blok z mieszkaniami.

a gdzie array?

Może niektórzy z Was zwrócili uwagę, że wypisując dwa rodzaje atrybutów, nie uwzględniłem tam typu array. Dlaczego? Ponieważ tablica jest po prostu zbiorem innych typów. Jeżeli tworzymy klasę powinniśmy zadbać o to, aby atrybut, który jest tablicą składał się z elementów tylko jednego typu. Rozumiem, że w dokumentacji PHP często można się spotkać z 'typem' mixed, ale u Was nie powinno być dla niego miejsca. Co jeżeli jednak tablica okazuje się zawierać różne typy i nijak nie da się tego inaczej zrobić? Może to oznaczać dwie rzeczy:
  • należy utworzyć klasę, a elementy tablicy powinny być jej atrybutami. Oczywiście to rozwiązanie można zastosować tylko wtedy, gdy taka klasa będzie posiadała logiczny sens
  • należy usiąść do projektu jeszcze raz i zastanowić się, gdzie jest błąd:)

widoczność atrybutów

Ogólnie jestem zdania, że każdy atrybut klasy powinnien być prywatny. W końcu jednym z głównych paradygmatów progamowania obiektowego jest hermetyzacja, co wyraźnie mówi, że nie powinno się udostępniać swoich wnętrzości światu.
Stosowanie atrybutów chronionych, jeżeli wiemy, że klasa będzie rozszerzana? Takie coś również do mnie nie przemawia, wolę stworzyć chroniony getter i/lub setter, a atrybut i tak zostawić prywatny, nawet jeżeli jest to klasa abstrakcyjna. Ale to już moje osobiste zdanie i niejedną dyskusję na ten temat toczyłem i wiem, że w takich przypadkach wielu programistów stosuje jednak atrybut protected.
A co z atrybutami publicznymi? Coś takiego byłbym w stanie zrozumieć, jeżeli miałbym klasę z x liczbą atrybutów, która wymagałaby setterów i getterów dla każdego z nich. W takim wypadku rzeczywiście rozsądniej byłoby uczynić je publicznymi. Jednak w czasie swojej kariery programisty nigdy nie spotkałem się z sytuacją, aby obiekt wymagał osobnego gettera i settera dla każdego atrybutu i ciężko mi wyobrazić sobie, aby taka funkcjonalność mogła mieć jakieś logiczne uzasadnienie. Wyjątkiem są DTO (Data Transfer Object), które służą do przekazywania danych i wszystkie pola mają publiczne.

nie mieszaj typów

Zdaję sobię sprawę, że PHP umożliwia przypisywanie do jednej zmiennej wartości różnych typów, ale jest to coś, czego nie powinno się robić, a już z pewnością nie w odniesieniu do atrybutów klas. Jeżeli mam jakiś atrybut typu string, to powinnien on być tego typu, nie integer, float, czy object, a tylko i wyłącznie string. Wiem, że dla wielu to oczywiste, ale mimo wszystko...

odrobina praktyki

Załóżmy, że naszym zadaniem jest stworzenie systemu dla firmy transportowej. Jednym z głównych części składowych ma być baza kontrahentów. Przy dodawaniu kontrahenta użytkownik musi określić jego NIP oraz unikalną nazwę. Musi istnieć możliwość wystawiania i przesyłania drogą mailową faktur za usługi firmy oraz musi być możliwość wykonania przelewu na konto kontrahenta, z którego usług korzystała firma. Firma jest międzynarodowa, więć musi istnieć możliwość obsługi różnych walut. Niektórzy kontrahenci mają kilka osób, do których chcą aby były wysyłane faktury na maila. Dodatkowo powinna istnieć możliwość określenia adresu siedziby kontraheta.
Na podstawie powyższego spróbujmy określić atrybuty dla klasy kontrahent.
Pierwszą rzeczą, którą wiemy jest to, że kotrahent musi posiadać NIP i nazwę:
<?php
class Contractor 
{
  private $_nip; //string
  private $_name; //string
}
Kontrahent może posiadać wiele kont przypisanych do przelewów w różnych walutach. Ponieważ numer konta i waluta są ściśle ze sobą powiązane decydujemy się na utworzenie kolejnej klasy reprezentującej konto bankowe:
<?php
class BankAccount
{
  private $_number; //string
  private $_currency; //string
}

class Contractor 
{
  private $_nip; //string
  private $_name; //string
  private $_bankAccounts; //BankAccount[]
}
Ponieważ faktury mogą być wysyłane na adres mailowy firmy, musimy dodać kolejny atrybut. Jednak w dalszej części wymagań możemy się dowiedzieć, że osób kontaktowych firmy może być dużo więcej, a więc dobrym zabiegiem będzie umieszczenie adresu mailowego w klasie reprezentującej osoby kontaktowe.
<?php
class ContactPerson
{
  private $_name; //string
  private $_email; //string
}

class Contractor 
{
  private $_nip; //string
  private $_name; //string
  private $_bankAccounts; //BankAccount[]
  private $_contactPerson; //ContactPerson[]
}
Istnieje również możliwość określenia adresu siedziby kontrahenta. Ponieważ zbiór takich informacji jest w miarę złożony (nazwa ulicy, numer domu, kod pocztowy itd.) oraz spójny logicznie warto umieścić je w innej klasie.
<?php
class Address
{
  private $_street; //string
  private $_houseNumber; //float
  private $_zipCode; //string
  private $_city; //string
  private $_country; //string
}

class Contractor 
{
  private $_nip; //string
  private $_name; //string
  private $_bankAccounts; //BankAccount[]
  private $_contactPerson; //ContactPerson[]
  private $_address; //Address
}
Oczywiście jest to dość prosty przykład i można go bez trudu rozbudować, ale wydaje mi się, że powinno wystarczyć:)
Kwestię podejmowania decyzji na temat widoczności atrybutów opiszę w następnym wpisie przy omawianiu metod klas.