Mija sześćdziesiąt lat od chwili, gdy w językach programowania wysokiego poziomu pojawiły się struktury: złożone typy danych wiążące kilka odrębnych pól. Powiązanie to działa na poziomie logicznym, gdyż pola dotyczą wspólnie jednego obiektu, oraz fizycznym, gdyż wymusza, by zawsze występowały razem i były traktowane jako jedność. Jednak mimo długiego stażu struktur w inżynierii oprogramowania, a także awansu do miana klasy i obiektu, programiści – nawet ci bardziej doświadczeni – nie mają wyrobionego nawyku tworzenia złożonych typów danych. Mści się to w momencie pierwszej rozbudowy, gdy prosta przeróbka zmienia się w obszerną refaktoryzację.
Podczas pisania aplikacji wymagającej podania danych kontaktowych osoby, takich jak imię, nazwisko i adres, może korcić, by zapisać je w następującej strukturze:
struct Person { std::string first_name; std::string last_name; std::string street_name; std::string building_no; std::string locum_no; };
Takie podejście może wydawać się sensowne w rozumieniu idei YAGNI: stworzyliśmy najprostszy kod spełniający wymagania. Ta implementacja powinna jednak wzbudzać niepokój programisty: z logicznego punktu widzenia adres osoby jest oddzielną klasą danych. I faktycznie, niedługo potem może pojawić się wymóg, by osoba – oprócz adresu zamieszkania – miała również zapisany opcjonalny adres korespondencyjny.
Oczywiście, problem można „rozwiązać” dodając kolejne pola do klasy Person
. Takie podejście bardzo szybko się jednak zemści, a jeżeli wymogi zaczną obejmować przypisywanie dowolnej liczby adresów do jednej osoby, przestanie być możliwe. Właściwym sposobem wybrnięcia z problemu, który powinien był zostać wybrany już na etapie pisania klasy Person
, jest wydzielenie klasy adresu:
struct Address { std::string street_name; std::string building_no; std::string locum_no; }; struct Person { std::string first_name; std::string last_name; Address address; };
Wprowadzenie adresu korespondencyjnego ogranicza się w takim przypadku do dodania jednego pola do klasy Person
:
struct Person { std::string first_name; std::string last_name; Address address; std::optional<Address> correspondence_address; };
Czasem koncepcję wydzielania odrębnych typów złożonych dla logicznie powiązanych danych opłaca się również stosować dla pojedynczych wartości. W pierwszym momencie może się to wydawać nielogiczne i niepotrzebnie skomplikowane. Jeżeli jednak mamy uzasadnione podejrzenie, że niedługo wartość ta ulegnie uzupełnieniu o dalsze elementy składowe, dodatkowy nakład pracy może się zwrócić.
Jako przykład posłuży opis marginesów kartki papieru:
struct Page { float margin_left; float margin_right; float margin_top; float margin_bottom; };
Wydawałoby się, całkiem normalna struktura, a ewentualne zmiany mogłyby dotyczyć jedynie zastąpienia czterech odrębnych pól jakąś kolekcją. Przewidujący programista może jednak zmienić powyższy zapis na następujący:
struct Distance { float value; }; struct Page { Distance margin_left; Distance margin_right; Distance margin_top; Distance margin_bottom; };
Wbrew pozorom ma to sens. Między liczbą a odległością jest subtelna różnica: pierwsza jest bezwymiarowa, druga mianowana. Dopóki umówimy się, że w całej aplikacji stosowana jest jedna wspólna jednostka miary, możemy przedstawiać odległości jako liczby typu float
. Jeżeli jednak każda może mieć inną jednostkę, konieczne staje się rozbudowanie klasy Distance
o dodatkowe pole:
class Unit { ……… }; struct Distance { float value; Unit unit; };
Zmiana niby niewielka, ale jeżeli wcześniej nie pomyślało się o wydzieleniu zapisu odległości w postaci oddzielnej klasy, będzie powiązana z poważną refaktoryzacją. Ponadto, zapisywanie odległości nie jako liczby float
lecz obiektu Distance
ma dodatkową, miłą cechę: kompilator sprawdza za nas zgodność typów i zaprotestuje, jeżeli do pola odległości spróbujemy wpisać ciężar lub liczność jakiegoś przedmiotu.
Oczywiście, nie można popaść w paranoję i dla każdego pola tworzyć nowego typu złożonego. Warto jednak zastanowić się nad każdym przypadkiem i stosować typy proste tylko wtedy, gdy ma się pewność, że nic nie przemawia za bardziej skomplikowaną opcją.