Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
🏆 Lubisz podcast Pierwsze kroki w IT? Oddaj swój głos w rankingu: TOP 10 polskich podcastów o IT! 🏆
„Jeden obraz jest wart więcej niż tysiąc słów” – to motto przyświecało twórcom diagramów UML. Są to grafiki, które odwzorowują systemy informatyczne. Dzięki nim możemy analizować logikę aplikacji bez konieczności zaglądania w kod. Abyś mógł stworzyć swój pierwszy diagram, wystarczy Ci kartka papieru, ołówek (koniecznie z gumką!) oraz trochę wiedzy o programowaniu obiektowym. A więc do dzieła!
Diagramy UML (Unified Modelling Language) to uniwersalna metoda służąca opisowi systemów informatycznych. Dzięki niej osoby zarządzające projektami mogą w jasny sposób definiować funkcjonalności i logikę aplikacji. Jako że UML jest jasno wyspecyfikowany, programiści nie mają problemu, by przenieść diagramy na działający kod.
Rozróżniamy aż trzynaście rodzajów diagramów UML. My zajmiemy się tylko diagramami klasowymi. Służą one do projektowania struktury klas (pól i metod) oraz ich relacji między sobą. Dzięki nim będziemy mogli łatwiej tworzyć logikę naszych aplikacji, czy analizować wzorce projektowe. Pomyśl tylko, jaka to oszczędność czasu i naszych szarych komórek!
Aby wykorzystać moc diagramów klasowych, poznajmy najpierw ich budulce, czyli:
Przykładowy diagram klas, źródło: Aleksander Shvets, Wzorce projektowe. Nowoczesny podręcznik.
Do budowy diagramów polecam Ci darmowe narzędzie – diagrams.net. Kod przedstawiony w przykładach możesz pobrać z tego repozytorium.
W UML klasy to po prostu obramowany prostokąt składający się z trzech sekcji:
Spójrz na poniższy przykład klasy Student, która posłuży nam do określenia pól i metod związanych z uczniem.
Interfejs w diagramie klas UML tworzymy przez dodanie słowa <<interface>> nad jego nazwą. Spójrz na przykład interfejsu:
Interfejs ma metody, które klasa może implementować. W JavaScripcie interfejsy nie występują, a ich użycie jest umowne.
Klasy abstrakcyjne tworzymy dodając <<Abstract>> nad nazwą klasy. Jak widzisz na przykładzie poniżej, oprócz metod mogą one posiadać również pola.
W JavaScripcie klasy abstrakcyjne również występują umownie. Różnią się od zwykłych klas tym, że na ich podstawie nie tworzymy nowych obiektów, a jedynie klasy potomne.
Umiemy już tworzyć klasy (w tym abstrakcyjne) i interfejsy. To już całkiem sporo!
Żeby stworzyć kompletny diagram UML, brakuje nam jeszcze relacji między klasami. Wyróżniamy sześć rodzajów relacji, które omówimy wraz z przykładami w JavaScripcie i rysunkami.
Będą to:
Relacje między klasami – od najsłabszej do najsilniejszej, źródło: Aleksander Shvets, Wzorce projektowe. Nowoczesny podręcznik.
Zwróć uwagę, że każda relacja jest oznaczona w inny sposób. Te powiązane z rozszerzaniem klas mają trójkątny grot, natomiast odnoszące się do interferencji – strzałkę. Poza tym zaznaczamy je linią ciągłą z wyjątkiem implementacji i zależności, które reprezentuje linia przerywana. Nauczmy się, co konkretnie oznaczają poszczególne relacje!
Zależność to najsłabszy rodzaj relacji pomiędzy obiektami. Mówimy o niej, jeśli pewne zmiany w jednym z obiektów mogą skutkować zmianami w drugim.
Tak słabych zależności raczej nie prezentujemy na diagramach. Tak naprawdę każda relacja jest zależnością, więc zdecydowanie lepszym pomysłem jest doprecyzowanie, jaki to rodzaj zależności. Duża liczba wprowadzonych na rysunek zależności spowodowałaby jego zaciemnienie i niemałą konsternację czytających.
Implementacja to relacja, w której jedna z klas definiuje pola lub metody zadeklarowane w interfejsie. Po prostu jest to dziedziczenie po interfejsie.
W przykładzie, który Ci pokażę, klasa Student implementuje interfejs Smart określający pilnego ucznia.
Przykład implementacji w kodzie:
class SmartInterface { learn() { console.log('Learning...'); } passTest() { console.log('Passing test...'); } beEarly() { console.log('Wake up at 6 am...'); } } class Student extends SmartInterface { #school; #subjects; constructor(school, subjects) { super(); this.#school = school; this.#subjects = subjects; } } const student = new Student('MIT', ['Math', 'Physics', 'Chemistry']); // dzięki rozszerzeniu interfejsu mamy dostęp do metody learn() student.learn();
W artykule wprowadzającym do wzorców projektowych wspominałem, że pisząc kod obiektowy w JavaScripcie, musimy iść na pewne ustępstwa. JS nie posiada interfejsów, stąd zgodnie z konwencją umawiamy się na klasę SmartInterface, która taki interfejs symuluje.
Dziedziczenie to relacja, w której klasa potomna dziedziczy wszystko, co związane z klasą nadrzędną. Dodatkowo taka klasa może rozszerzyć swojego rodzica.
Prosto rzecz ujmując – jest to relacja analogiczna do implementacji, z tym że dziedziczymy po klasie (oraz interfejsach, które sama implementuje).
Zaprezentuję Ci przykład, w którym klasa Teacher dziedziczy po klasie abstrakcyjnej Person.
Dziedziczenie oparte na powyższym diagramie będzie prezentować się następująco:
class AbstractPerson { #name; #age; constructor(name, age) { this.#name = name; this.#age = age; } getName() { return this.#name; } setName(name) { this.#name = name; } getAge() { return this.#age; } setAge(age) { this.#age = age; } isAdult() { return this.#age >= 18; } } class Teacher extends AbstractPerson { #schoolName; #students; constructor(name, age, schoolName, students) { super(name, age); this.#schoolName = schoolName; this.#students = students; } getSchool() { return this.#schoolName; } } const teacher = new Teacher('John', 30, true, 'MIT', ['John', 'Mary', 'Peter']); console.log(teacher.getName());
Dziedziczenie może opierać się na każdej klasie, niekoniecznie abstrakcyjnej.
Asocjacja to najluźniejsza relacja związana z wzajemnym oddziaływaniem klas. Oznacza ona, że jeden obiekt chwilowo wykorzystuje inny lub wchodzi z nim w jakąś interakcję. Obiekty mogą istnieć niezależnie od siebie.
Spójrz na przykład klasy Teacher. W tej metodzie Teacher może nauczać jakiegoś przedmiotu (klasa Subject), wykorzystując metodę teach.
Zobacz przykład zastosowania asocjacji w kodzie:
class Subject { #name; #level; constructor(name, level) { this.#name = name; this.#level = level; } getName() { return this.#name; } getLevel() { return this.#level; } } class Teacher { #name; constructor(name) { this.#name = name; } teach(subject) { if (!subject instanceof Subject) { throw new Error('Invalid subject'); } console.log( `I'm teaching ${subject.getName()}, at level ${subject.getLevel()}` ); } } const subject = new Subject('Math', 1); const teacher = new Teacher('Matt'); teacher.teach(subject);
Agregacja to rodzaj asocjacji. Różni się od niej tym, że jeden obiekt służy za kontener dla innych obiektów. Dzięki zadeklarowaniu przekazywanego obiektu w konstruktorze, mamy możliwość operacji na przekazanym obiekcie w całym ciele. Mimo to oba obiekty mogą istnieć niezależnie od siebie.
Wiem, że ta definicja może wydawać się trudna, dlatego spójrz na poniższy przykład:
Jak widzisz, powyższa agregacja różni się od asocjacji tym, że klasa Teacher stanowi kontener na obiekty stworzone za pomocą klasy Subject. Mimo to Subject stanowi osobny byt. Nawet jeśli usuniemy nauczyciela, to przedmioty, których może uczyć, nadal będą istnieć w programie.
class Subject { #name; #level; constructor(name, level) { this.#name = name; this.#level = level; } getName() { return this.#name; } getLevel() { return this.#level; } } class Teacher { #subjects; constructor(subjects) { this.#subjects = subjects; } getSubjects() { return this.#subjects; } addSubject(subject) { if (!subject instanceof Subject) { throw new Error('Invalid subject'); } this.#subjects.push(subject); } removeSubject(subject) { if (!subject instanceof Subject) { throw new Error('Invalid subject'); } this.#subjects.filter(s => s !== subject); } teach(subject) { if (!subject instanceof Subject) { throw new Error('Invalid subject'); } console.log( `I'm teaching ${subject.getName()}, at level ${subject.getLevel()}` ); } } const subject = new Subject('Math', 1); const anotherSubject = new Subject('English', 2); const teacher = new Teacher([subject]); teacher.addSubject(anotherSubject); console.log(teacher.getSubjects());
Jak widzisz, konstruktor klasy Teacher przechowuje informacje o przedmiotach (Subject). Dzięki temu możemy np. usunąć konkretny przedmiot z tablicy występującej w obiekcie teacher.
Kompozycja to ostatni i najsilniejszy rodzaj relacji, jakie występują między obiektami. Kompozycja stanowi specjalny rodzaj agregacji.
Jak sama nazwa wskazuje – dany obiekt „komponuje się” z innych, czyli składa się z nich. Usunięcie obiektu nadrzędnego spowoduje usunięcie obiektów, które wchodzą w jego skład. Obiekty te nie mogą istnieć niezależnie od siebie.
Jako przykładu moglibyśmy użyć naszego ciała, w którego skład wchodzi układ nerwowy. Bez ciała nie moglibyśmy mówić o układzie nerwowym i vice versa. Aby zaprezentować Ci przykład kompozycji, posłużę się klasą School, która będzie składać się (komponować) z obiektów powstałych na bazie klasy Classroom.
Aby poznać pełną różnicę między agregacją a kompozycją, spójrz na przykład w kodzie. Zwróć uwagę na konstruktor klasy School.
class Classroom { #name; #capacity; #color; constructor(name, capacity, color) { this.#name = name; this.#capacity = capacity; this.#color = color; } getName() { return this.#name; } setName(name) { this.#name = name; } setCapacity(capacity) { this.#capacity = capacity; } paintWalls(color) { this.#color = color; } } class School { #classrooms; constructor() { this.#classrooms = [ new Classroom('01', 20, 'white'), new Classroom('02', 30, 'yellow'), new Classroom('03', 10, 'blue') ]; } getClassrooms() { return this.#classrooms; } addClassroom(classroom) { if (!classroom instanceof Classroom) { throw new Error('Invalid classroom'); } this.#classrooms.push(classroom); } renovateClassroom(name, color) { const classroom = this.#classrooms.find(c => c.getName() === name); if (!classroom) { throw new Error('Classroom not found'); } classroom.paintWalls(color); } } const school = new School(); school.renovateClassroom('01', 'red'); console.log(school);
Możesz zauważyć, że nowe obiekty z Classroom powstają bezpośrednio w konstruktorze School. Ta klasa stanowi kontener na obiekty. Usunięcie jej spowoduje usunięcie elementów wchodzących w jej skład.
Dzięki istnieniu metody renovateClassroom możemy wpływać na obiekty wchodzące w skład kontenera School.
Poznałeś już cztery filary programowania obiektowego oraz diagramy UML. Dzięki temu wiesz:
Wykorzystaj tę wiedzę – spróbuj napisać obiektowo prosty projekt. Skorzystaj z diagramu UML, by zobrazować logikę swojej aplikacji. To na pewno zrobi wrażenie na Twoim (przyszłym) pracodawcy!
Udostępnij ten artykuł:
Zapisz się i zgarnij za darmo e-book o wartości 39 zł: Jak zostać programistą? Skuteczny przewodnik!
Jak zostać programistą? Skuteczny przewodnik
TERAZ DOSTĘPNY BEZPŁATNIE!
Cena w sklepie to 39 zł, a Ty możesz otrzymać ten e-book bezpłatnie za zapis na newsletter. To ponad 40 stron konkretów o nauce programowania i pracy w IT.