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.

14 komentarzy:

  1. Co do atrybutów i rozpoznawalności obiektów, to generalnie obiekty dzielą się na 4 podstawowe rodzaje:

    1. DTO - Data Transfer Object, coś jak struktura danych w C, wszystkie pola ma publiczne używamy ich do przekazywania danych

    2. Value Objects - obiekty, które rozróżniamy po ich atrybutach, tj. dwa Value Object są identyczne, jeżeli wszystkie ich atrybuty są identyczne, zwykle takie obiekty są też immutable

    3. Encje - obiekty które rozróżniamy po tożsamości - np. dwa obiekty klasy Contractor są równe, jeżeli mają równy $_nip, który je unikalnie identyfikuje

    4. Usługi - obiekty, które nie mają tożsamości, zwykle enkapsulują jakieś operacje IO (dostęp do bazy danych etc)

    To tak na początek, jeżeli chcesz pisać też o programowaniu obiektowym, to powinieneś wspomnieć, że stosowanie getterów i setterów jest zaprzeczeniem enkapsulacji i powrotem do programowania proceduralnego :)

    OdpowiedzUsuń
  2. @Wojciech Soczyński: Chciałeś napisać **błędne** używanie getterów / setterów jest zaprzeczeniem enkapsulacji. Powrotem do proceduralnego stylu to też raczej nie będzie, bo nadal dane i operacje na nich są ze sobą ściśle powiązane - czyli podstawa paradygmatu zostaje zachowana.

    OdpowiedzUsuń
  3. @Crozin:

    jeżeli mamy pole prywatne i do niego getter i setter to jest tak jak by było publiczne. Zamiast udostępniać getter i setter, powinno udostępniać konkretne operacje, dzięki temu ukrywa swoją wewnętrzną implementację a o to dokładnie chodzi w stylu obiektowym.

    OdpowiedzUsuń
  4. @Crozin:

    dla przykładu. Nie prosisz obiektu Wojtek klasy Człowiek o to żeby Ci zwrócił, obiekty klasy "serce, układ nerwowy i nogi", i później ustawiasz im parametry, tylko, wywołujesz metodę "biegnij". Gettery i settery mają tylko sens w ujęciu DTO, gdy obiekt jest tylko kontenerem na dane.

    OdpowiedzUsuń
  5. @Wojciech Soczyński: Czyli tak jak się spodziewałem swój argument oparłeś na błędnym użyciu getterów. Oczywistym jest, że powinna być jakaś metoda w stylu biegnij(), ale jeżeli aplikacja wymaga dostępu do powiedzmy serca (bo w końcu można je przeszczepić albo dorzucić wersję ekstra z rozrusznikiem) to korzysta się z getterów mimo iż nie mamy do czynienia z obiektem typu DTO.

    No chyba, że stworzysz dosłownie setki metod w stylu przeszczepSerce(), kondycjaSerca(), tempoPracySerca() itd. w obiekcie człowieka, ale to będzie nieporównanie gorsze od getterów.

    OdpowiedzUsuń
  6. @Crozin:

    i tu dochodzimy do sedna, tj. styku między rzeczywistością a modelowaniem. Jeżeli popatrzymy na sprawę z punktu widzenia rzeczywistości - jeżeli chcemy zrobić coś z sercem to lekarz rozcina klatę piersiową pacjenta. To samo można zrobić w świecie wirtualnym tj. przy użyciu refleksji można przełamać enkapsulacje (w PHP/Javie metoda setAccessible) i operować bezpośrednio na "wnętrznościach", jednak robimy to na własną odpowiedzialność z zastrzeżeniem, że wiemy, że te "bebechy" mogą się zmienić.

    Jest jeszcze drugi sposób. W ujęciu DDD natrafiamy na koncepcję tzw. Bound Context, czyli w uproszczeniu na zależności klas od kontekstu użycia. I tak, zdefiniujmy dwa konteksty:
    1. Treningowy
    2. Medyczny
    W kontekście treningowym klasa "Człowiek" będzie miała np. metody "biegnij", "skacz", "podnośCiężary".
    W kontekście medycznym, będzie miała metody "przeszczepSerce" etc.
    Mamy więc tak naprawdę dwie osobne klasy "Człowiek" występujące w dwóch kontekstach ale np. korzystające z pewnych samych danych (imię, nazwisko, wiek, waga etc).

    Dzięki takiemu podejściu:
    1. enkapsulacja jest zachowana, nie ma getterów i setterów
    2. istnieją metody odpowiadające logice biznesowej danego kontekstu
    3. klasa istniejąca w danym kontekście zawiera tylko minimalny zestaw metod biznesowych potrzebnych do odwzorowania logiki biznesowej w danym kontekście

    OdpowiedzUsuń
  7. Jak widzę ciekawa rozmowa się wywiązała:)

    Osobiście wydaje mi się, że gettery i settery nie są złem samym w sobie. Stają się czymś takim, gdy zaczyna się ich nadużywać, co niestety jest rozwiązaniem dość często stosowanym przez programistów.

    Szczególnie encje są bardzo podatne na coś takiego, ponieważ wiele osób nadal uważa za bardziej rozsądne wywołać kilka setterów/getterów niż jedną, logicznie spójną metodę.
    Wydaje mi się, że wynika to z faktu, że pomija się bądź umniejsza znaczenie projektu logiki aplikacji. Takie twory najczęściej powstają 'w praniu' tzn. w trakcie pisania kodu, a nie na etapie projektu, bo tam nie ma dla nich miejsca.

    OdpowiedzUsuń
  8. Użycie refleksji byłoby niemal z definicji złe w takim przypadku.

    Bounded Context rzeczywiście wydaje się być ciekawą alternatywą (zresztą DDD samo w sobie wprowadza masę takich), której będę musiał się bliżej przyjrzeć. Na pewno mogłoby się to dobrze sprawdzić w omwianym tutaj przykładzie (nieszczęsnego człowieka), jednak mam w wrażenie że w pewnych przypadkach może to po prostu sprawiać więcej zachodu nie dając realnych korzyści.

    Trzeba w końcu pamiętać, że nie zawsze idealnie "poprawny" kod jest lepszy od tego, który czasami pójdzie na łatwiznę. Piszę to oczywiście w kontekscie dużych projektów, bo tylko tam te reguły mają jakąkolwiek rację bytów.

    OdpowiedzUsuń
  9. Nie wiem też czy aby przypadkiem w pewnych przypadkach nie wyszedłby z tego zwykły setter, który byłby opakowany w nieco ładniejsze i bardziej naturalne (jeżeli chodzi o czytelność kodu) opakowanie.

    Jednak jak już wspomniałem, nie zgłębiłem jeszcze dostatecznie dobrze tematu, więc mogę mieć (i miłoby było gdyby tak było :]) błędne wyobrażenie całego konspektu.

    OdpowiedzUsuń
  10. Wkrótce coś napiszę u siebie, co do refleksji to była po prostu chęć pokazania zbieżności pomiędzy światem rzeczywistym a wirtualnym.

    Jeżeli chodzi o użycie getterów i setterów oraz konkretnych metod zamiast nich to nasuwa się jedna konkluzja - kod takiej metody tak czy inaczej trzeba będzie napisać, tylko, że jeżeli użyjemy przy okazji getterów i setterów to kod owej metody będzie rozrzucony po całym systemie, dzięki rezygnacji z nich, będzie on znajdował się bliżej danych których operuje i łatwiej go będzie znaleźć. Mówię to przede wszystkim z doświadczenia, a nie z powodu jakiś teoretycznych dywagacji.

    Crozin, jeżeli zainteresowałem Cię hasłem "bound context" to polecam jeszcze pogoglować o Data Context Interaction ;)

    OdpowiedzUsuń
    Odpowiedzi
    1. he he... tyle lat temu :)
      dodam od siebie: jeżeli interesuje mnie ocena danej osoby z matematyki na maturze, to nie żądam od szkoły "odpisu oceny z matematyki" a żądam odpisu "świadectwa maturalnego" i wtedy poznam tę ocenę, w efekcie: mam jedno proste API a tyle wywołań ile ocen, żadnych get/set na atrybutach i ich nazwach i typach.

      Usuń
  11. Witam
    Mógłbyś opisać w jaki sposób umieszczasz kod php na blogu?

    OdpowiedzUsuń
    Odpowiedzi
    1. Prosty skrypcik, który na podstawie słów kluczowych lub wzorców dodaje odpowiednie style. Oczywiście idealny nie jest, ale na moje potrzeby wystarczający:)

      Usuń