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ę:
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:
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.
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.
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
}
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
- Jak programować obiektowo? cz. 9 – klasy abstrakcyjne
- Jak programować obiektowo? cz. 8 – interfejsy
- Jak programować obiektowo? cz. 7 – final
- Jak programować obiektowo? cz. 6 – wartości stałe
- Jak programować obiektowo? cz. 5 – ach ten static…
- Jak programować obiektowo? cz. 4 – metod ciąg dalszy
- Jak programować obiektowo? cz. 3 – czym jest metoda?
- Jak programować obiektowo? cz. 2 – atrybuty klasy
- Jak programować obiektowo? cz. 1 – wstęp