Jak programować obiektowo? cz. 2 – atrybuty klasy

Pisanie o jednej składowej struktur wykorzystywanych w programowaniu obiektowym bez poruszania innych jego aspektów nie jest proste, niemniej jednak postaram się w miarę możliwości w każdym wpisie trzymać jednej rzeczy.

Ok, bez zbędnego wstępu – dzisiaj słów kilka o atrybutach.

Let’s start

Wartości atrybutów konkretnego obiektu są czymś, co odróżnia od siebie różne instancje tej samej klasy, umożliwiają one ich identyfikację. Można podzielić je na dwa rodzaje:

  • Typy podstawowe: 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 jego części składowe.

Agregacje dodatkowo dzielimy na:

  • częściowa – oznacza to mniej więcej tyle, że zawieranie nie jest konieczne dla istnienia obiektu zawieranego.
  • 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.

Jako dobry przykład może posłużyć tutaj tworzenie oprogramowania. Nasza aplikacja (konkretna instancja) ma swoją nazwę (atrybut), który ją identyfikuje np. System Szkoleń z OOP. Jak każda aplikacja składa się ona z kodu. Z jednej strony są to biblioteki, frameworki i ORMy, a z drugiej domena oraz kod napisany na potrzeby implementacji tego konkretnego oprogramowania. Te wszystkie biblioteki, frameworki i ORMy to agregacja częściowa, gdyż jak wiadomo nie są one zależne od cyklu życia aplikacji, „żyją własnym życiem” i mogą być z powodzeniem wykorzystywane w innych projektach. Kompozycją jest natomiast kod pisany specjalnie na potrzeby aplikacji, gdyż nie ma on sensu pozostawiony sam sobie i gdy tylko oprogramowanie przestanie być potrzebne – zostanie on razem z nim wyrzucony do kosza.

Jako ćwiczenie polecam zastanowić się jakim rodzajem agregacji jest domena aplikacji? Na odpowiedzi czekam w komentarzach 🙂

A gdzie w tym wszystkim kolekcje?

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 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. W niektórych językach takie rozwiązania są narzucone z góry przez język i choćbyśmy chcieli to nie zmienimy tego. Jest jednak sporo języków, które pozwalają na takie rozwiązanie i stosując je trzeba być w pełni świadomym co i dlaczego się robi.

Na przykład, w dokumentacji PHP często można się spotkać z „typem” mixed, ale Wam raczej doradzam wystrzeganie się go, a to dlatego, że więcej z tym problemów niż potencjalnych korzyści. 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 powinien być domyślnie prywatny i dopiero jeżeli istnieją ku temu konkretne powody powinniśmy się zastanawiać czy nie zwiększyć jej (widoczności). Jakby na to nie patrzeć, nie na darmo jednym z głównych paradygmatów programowania obiektowego jest hermetyzacja (enkapsulacja), co wyraźnie mówi, że nie powinno się udostępniać swoich wnętrzności światu.

Atrybuty publiczne? Cóż, poza obiektami, których jedynym celem jest transfer informacji (Data Transfer Object), to chyba nie spotkałem się z sytuacją, gdy rzeczywiście było logiczne uzasadnienie dla takiego rozwiązania. Najczęściej programiści decydują się na ten typ widoczności z czystego lenistwa. Po prostu łatwiej dobrać się na siłę do wnętrzości obiektu, niż pomyśleć nad poprawnym rozwiązaniem problemu.

Na koniec pora na omówienie atrybutów chronionych. Wielu programistów namiętnie stosuje tą widoczność jako domyślną, co jest raczej przyzwyczajeniem wynikającym z napatrzenia się na kod frameworków, ORMów i innych bibliotek, gdzie do pewnego momentu twórcy zakładali, że wszystko może zostać rozszerzone. W Waszym kodzie ten typ widoczności jednak powinien być wykorzystywany sporadycznie, ponieważ dopóki klasa nie jest rozszerzana w grę wchodzą tylko dwa typu: private i protected. Jeżeli zakładamy, że w przyszłości „może kiedyś, ale jeszcze nie wiem czy na pewno i czy w ogóle” to lepiej też skupcie się na tych dwóch typach. Jeżeli kiedyś będzie potrzeba zmienić private na protected, to jest to zmiana, którą jesteście w stanie zrobić w ciągu kilku sekund. Natomiast użycie protected od samego początku daje powód programiście, który kiedyś będzie patrzył na ten kod, do zastanawiania się jaki był takiego zabiegu cel – czy klasa miała zostać rozszerzona? W jakim celu? Aż w końcu odkryje tą smutną prawdę – nie było żadnego powodu.

Podsumowując, wszystkie atrybuty domyślnie oznaczajcie jako prywatne. Jeżeli będziecie mieli bardzo dobry, ale to naprawdę cholernie dobry powód, to zastosujcie widoczność publiczną. Choć przed tą czynnością, już po tym jak przemyślicie to i upewnicie się co do słuszności tej decyzji, zastanówcie się jeszcze co najmniej dwa razy, ponieważ najczęściej jest to znak, że coś „poszło nie tak”. Widoczności chronionej nie bierzemy pod uwagę jeżeli klasa nie posiada klas potomnych, a jeżeli ten warunek jest spełniony, to zastanawiamy się czy dany atrybut jest niezbędny tej klasie (potomnej) do działania. Jeżeli na to pytanie odpowiemy twierdząco, to zmieńcie widoczność atrybutu.

Jeszcze jedna uwaga na koniec. Pamiętajcie, że zmieniając widoczność z prywatnej na inną przestajecie mieć pełną kontrolę nad tym co jest wartością danego atrybutu – może zostać zmieniony z zewnątrz (atrybuty publiczne) lub przez klasy potomne (atrybuty chronione). Szczególnie bolesne mogą być konsekwencje zastosowania takiego rozwiązania w językach, gdzie nie ma, bądź nie jest wymagane, twarde typowanie.

Gdyby jeszcze Wam było mało na temat widoczności to zapraszam pod ten link.

Nie mieszaj typów

Nawet jeżeli dany język umożliwa przypisywanie do atrybutu/zmiennej wartości różnego typu, to w przypadku atrybutów zdefiniowanych w klasach radzę tego unikać jak ognia. Nie stosowanie się do tej zasady może skończyć się sporą ilością bloków warunkowych rozsianych po Waszym kodzie, bo zależnie od typu wartości danego atrybutu możecie decydować się na różne działania.

Jeżeli Was kusi, żeby jednak zrobić wbrew temu co napisałem, to zadajcie sobie pytanie, czy rzeczywiście ma to sens? Czy nie jest to znak, że coś nie zostało w wystarczającym stopniu przemyślane? Każdy atrybut ma swoją nazwę, a każda nazwa również do czegoś zobowiązuje, jest określeniem (definicją) tego, czym wartość atrybutu być powinna, a zmiana typu jest dowodem zmienności tej definicji.

Odrobina praktyki

Ok, najwyższy czas na krótki przykład. Załóżmy, że naszym zadaniem jest stworzenie oprogramowanie dla firmy transportowej. Jednym z głównych modułów 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ę:

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:

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.

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.

Gdy już mamy taką wstępną strukturę to możnaby się zastanowić, czy nie mamy potrzeby tworzenia kolejnych encji np. NIP, Currency, AccountNumber itp., itd. Wszystko zależy oczywiście od tego, jaką rolę te wartości pełnią w systemie i należy takie decyzje podejmować w oparciu o konkretną aplikację.

Na potrzeby wpisu wydaje mi się jednak, że wystarczy 🙂 Mam również nadzieję, że tym razem lepiej trafiłem z przykładem.

Pozostałe artykuły z cyklu

Jestem fanatykiem obiektowego programowania i nieustannie pogłębiam swoją wiedzę we wszelkich tematach z nim związanych. Wszystko czego się dowiem konfrontuję z rzeczywistością, ponieważ teoria, która nie ma odzwierciedlenia w praktyce, traci swój sens tam, gdzie zaczyna się praca programisty :)

  • Widziałem Orła Cień

    Co za mongoł to pisał.. To czym nazywasz atrybuty, są tak naprawdę modyfikatorami. Pojęcie atrybutów jest zupełnie inne.

    • Przemek

      Nie obrażaj ludzi zwłaszcza że widać że sam nie masz pojęcia o programowaniu.

      • Widziałem Orła Cień

        Buhehe i tyle mam do powiedzenia 😀

        • nrm

          My też, banik będzie, do widzenia. Pomyliłeś serwisy z onetem, tutaj zapraszamy osoby potrafiące się kulturalnie wysłowić.

  • Potfur

    @Sebastian: Już mogłeś phpDoca użyć zamiast liniowych komentarzy 🙂

    • Sebastian Malaca

      Czepiasz się 😛
      Najchętniej to bym typowania użył, ale chcę „pociągnąć” przykłady w PHPie, więc odpada.

  • Marcin Bazanowski

    Świetny przykład teraz dałeś. Takiego omówienia szukałem od jakiegos czasu. Dopiero teraz rozumiem, ze „duzo” klas to nie problem, a zaleta, nawet w malych zadaniach.

    Robilem ostatnio małą wtyczke do wordpressa do dodawania slidera z opcjami. Gdyby tylko ten artykul wyszedl kilka dni wczesniej to napisalbym go lepiej, chociaz i tak jestem z siebie dumny bo poraz pierwszy zrobilem cos w calosci obiektowo 😀

    Czekam na kolejne czesci z niecierpliwoscia 😀

    To moze jako pytanie zadam cos z czym mialem ostatnio problem i pewnie zrobilem to zle:

    Zrobilem sobie klase SliderAdmin, a w niej pole $slides jako tablica obiektow type Slide, oraz pole $settings jako tablica obiektow typu SliderSetting. Oryginalnie slides jest pusta, ale settings chcialbym aby miala takie ustawienia jak: width, height, duration, interval

    Juz nie wnikajac w strukture klasy Settings, to jak przypisac domyslne wartosci do pola $settings? Ja to zrobilem w konstruktorze klasy SliderAdmin, po prostu robiac $this->settings[] = new SliderSetting(‚blabla’); Ale srednio jestem do tego przekonany.

    • Sebastian Malaca

      Cieszę się, że artykuł się podobał i mam nadzieję, że kolejne nie zawiodą oczekiwań 🙂

      Co do Twojego pytania:
      Zakładam, że obecnie masz w kodzie coś takiego:
      $sliderAdmin = new SliderAdmin();

      Może nawet w kilku miejscach i nie chciałbyś za każdym razem przekazywać domyślnych ustawień, czyli:
      $defaultSettings = /** budowanie ustawień domyślnych */
      $sliderAdmin = new SliderAdmin($defaultSettings);

      I stąd decyzja o przeniesieniu tego do konstruktora (brak konieczności tworzenia za każdym razem $defaultSetttings).
      Czy dobrze rozumiem problem?

      Jeżeli tak, to polecałbym wykorzystać wzorzec Factory:
      class SliderAdminFactory {
      public function create() {
      $default = /* budowanie ustawień domyślnych*/
      return new SliderAdmin($defaultSettings);
      }
      }

      Dzięki temu nie ma duplikacji, a Ty nie budujesz obiektu w konstruktorze.

      Jeżeli źle zrozumiałem, to czekam na więcej informacji.

      • Marcin Bazanowski

        Dokładnie o takie cos mi chodzilo. Dzieki, teraz to ma faktycznie ręce i nogi 🙂

  • Jim1961

    „.. twarde typowanie.” – chyba „silne”.
    „.. $contactPerson; //ContactPerson[] ..” – może lepiej $contacts albo $contactPeople.

    Dobrą konwencją jest nazywanie zmiennych prywatnych od znaku podkreślenia.

    • satyr

      > Dobrą konwencją jest nazywanie zmiennych prywatnych od znaku podkreślenia.
      Nie zgadzam się. Np. PSR-2 mówi jasno, że nie powinno się tego robić. Nie jest to konwencja ani dobra ani zła. Zależy od standardu kodowania. Jeśli używasz getterów i setterów to w zasadzie jest to zupełnie obojętne, bo z polami prywatnymi i protected nie masz do czynienia bezpośrenio. Podkreślenia przed nazwą pola to pokłosie php4 gdzie nie było widoczności i trzeba było sobie jakoś pomagać ręcznie.

      • Jim1961

        To standardy są dla mnie, a nie na odwrót. PHP4 na oczy nie widziałem.

        • satyr

          No tak, napisałem przecież że to zależy od standardu kodowania. Możesz sobie oczywiście wymyślić swój standard kodowania ale weź spróbuj do niego przekonać ludzi z którymi pracujesz w projekcie, jeśli wszyscy się posługują np. PSR-2. Nawet w Zend Coding Standards podkreślniki zostały usunięte.

    • Potfur

      Konwencja ta się przydaje tylko w sytuacji gdy nie masz możliwości określania dostępu.
      W każdej innej sytuacji jest całkowicie zbędna.

      • Jim1961

        IMHO to zbędne jest ustawiać wszystko na private: http://stackoverflow.com/a/1641305 lajkowałbym.

        • Sebastian Malaca

          Ciekawe stwierdzenie (a propos komentarza z linku), niemniej jednak jego autor podchodzi do sprawy od strony implementacji, ja natomiast staram się uzasadnić logiczne wykorzystanie tych modyfikatorów.

          Idąc tokiem myślenia autora wpisu, to po co przejmować się tym, czy zdecyduje się na dziedziczenie czy kompozycję? Czy kod będzie czytelny czy nie? Przecież kolega z zespołu (ba, klient!?) może wpaść do kodu i go przerobić? Open/Closed principle? Otwarte dla kogo? Zamknięte dla kogo?

          Jeżeli mamy dostęp do źródła to zawsze możemy zrobić wszystko, ale osobiście jestem wdzięczny każdemu programiście, który tak tworzy komponenty, że czytając API klas wiem wszystko co muszę i nawet mnie nie kusi żeby patrzeć w źródło 🙂

  • satyr

    >> Co jeżeli jednak tablica okazuje się zawierać różne typy i nijak nie da się tego inaczej zrobić?
    Jest jeszcze trzecia metoda, stworzyć dla tych typów interfejs co pozwoli je ujednolicić.

    >> dopóki klasa nie jest rozszerzana w grę wchodzą tylko dwa typu: private i protected
    Niestety jest to zbyt duże i niebezpieczne uproszczenie. W przypadku Doctrine2 jeśli chcemy wrzucić do cache obiekty musimy używać protected, ponieważ obiektów __PROXY__ nie da się zserializować jeśli mają prywatne pola.

    >> Nie mieszaj typów -> radzę tego unikać jak ognia
    Jest na to dość dobra metoda, jeśli typem ma być klasa lub tablica. Wystarzy określić typ w setterze.

    No i właśnie, gdzie informacja o setterach i geterach? Myślę że ten temat jest konieczny w przypadku omawiania pól w klasach. Podobnie zresztą konstruktory. Co do przykładów, pierwszy (z aplikację) tak abstrakcyjny że nawet ja do końca nie rozumiem o co Ci chodzi. Drugi z firmą dużo lepszy. Co do całości artykułu – obawiam się, że początkujący nic z niego nie zrozumieją. Agregacja, kompozycja… za daleko jedziesz z definicjami. Doceniam zaangażowanie, ale wartość Twojego tutoriala będzie niska przy tak skomplikowanym tłumaczeniu prostych rzeczy. To co jest jasne dla nas, nie jest jasne dla osób które się dopiero uczą.

    • Sebastian Malaca

      > Jest jeszcze trzecia metoda, stworzyć dla tych typów
      > interfejs co pozwoli je ujednolicić.
      Czy nie masz na myśli tego samego co ja w punkcie pierwszym? Mógłbyś podać przykład?

      >> dopóki klasa nie jest rozszerzana w grę wchodzą tylko dwa typu:
      >> private i protected
      > Niestety jest to zbyt duże i niebezpieczne uproszczenie.
      > W przypadku Doctrine2 jeśli chcemy wrzucić do cache
      > obiekty musimy używać protected, ponieważ obiektów
      > __PROXY__ nie da się zserializować jeśli mają prywatne pola.
      Świetnie, że zwróciłeś na to uwagę. Oczywiście masz rację.
      Starałem się jedynie uczulić na to, aby nie korzystać z modyfikatora protected na zasadzie domyślnej widoczności. Poza tym, jeżeli trzymamy się logicznego uzasadnienia, to co napisałem jest prawdziwe. Prawda jednak jest taka, że od implementacji nie uciekniemy 🙂 i przy korzystaniu z bibliotek musimy ich niektóre wymagania spełnić, aby otrzymać w zamian to, co potrzebujemy. Nie unikniemy tego, niemniej jednak warto być świadomym wszystkich za i przeciw przy podejmowaniu decyzji.

      >> Nie mieszaj typów -> radzę tego unikać jak ognia
      > Jest na to dość dobra metoda, jeśli typem ma być klasa lub tablica.
      > Wystarzy określić typ w setterze.

      To prawda. Problem jednak nie polega na implementacji (choć to też, ponieważ typów prostych już nie określisz, elementów w tablicy również), a na tym, że programiści sami decydują się na takie rozwiązania i robią to celowo. Ja natomiast starałem się przedstawić wady, które temu towarzyszą.

      Co do metod to na nie przyjdzie jeszcze pora 🙂 W tym wpisie chciałem się skupić jedynie na atrybutach, bo temat jest na tyle obszerny, że warto było poświęcić mu osobny artykuł.

      Co do stopnia skomplikowania, to staram się pisać jak najprościej, czekam jednak na wszelkie sugestie, które mogą wpłynąć na wartość merytoryczną wpisów.
      Od definicji niestety nie będę uciekał, bo warto już od początku zaznajomić się z terminami, aby ich swobodnie używać – usprawnia to komunikację pomiędzy programistami.

  • RPicheta

    Brawa dla autora! Mimo iż jestem na tym portalu świeży, to pochłaniam artykuł za artykułem, świetna robota Panowie 🙂

  • Dobrze by było, gdyby była spójna nawigacja po całej serii – w tym momencie brak jest linku do kolejnej części 🙂

    PS: chyba, że się akurat wylosuje w „Podobne tematy” 😉

    • nrm

      dzięki za info, uzupełnie spis treści w każdym tekscie z tej serii.

  • Grzegorz Wawrzak

    Dobrze napisany artykuł. 🙂
    Jak i część pierwsza.

    Raczej mówimy o silnym typowaniu, niżeli twardym.
    Jedyne co jeszcze mogę dodać, to, że np. do adresu możemy użyć Value Object Pattern, co mocno polecam, ale rozumiem, że celem artykułu, było zaznajomienie usera z atrybutami klasy. 🙂

  • Przyczep

    rozszerzana w grę – lepiej będzie „rozszerzana, w grę” – i poprawnie 😉

Send this to friend

webmastah.weekly
Cotygodniowa porcja linków ze świata WEBDEV BEZ spamu, TYLKO samo mięcho!
Zobacz poprzednie wydania. Dołącz do 2 tysięcy webdeveloperów!
HTML5, CSS3, JS (React, Angular, Ember, Vue), PHP, SQL