## PYTANIE 6: Reużywalność kodu w OOP (PROI) **Omówić metody reużywalności kodu i struktur danych w obiektowych językach programowania.** --- ### Tło pojęciowe — słowniczek **OOP (Object-Oriented Programming / Programowanie obiektowe)** — paradygmat, w którym program składa się z obiektów łączących dane (pola) i zachowanie (metody). Cztery filary: enkapsulacja, dziedziczenie, polimorfizm, abstrakcja. W kontekście pytania — to OOP wprowadza mechanizmy ponownego użycia kodu omawiane poniżej. **Klasa (class)** — „wzorzec" (blueprint) obiektu. Definiuje pola i metody. Obiekt to konkretna instancja klasy. class Dog { // klasa string name; void bark(); }; Dog rex; // obiekt (instancja) **Reużywalność kodu (code reuse)** — możliwość wykorzystania raz napisanego kodu w wielu miejscach bez kopiowania. Zmniejsza ilość duplikatów, ułatwia utrzymanie i redukuje błędy. To główny temat pytania. --- ### Cztery filary OOP **1. Enkapsulacja (encapsulation)** — ukrywanie szczegółów implementacji za interfejsem publicznym. Obiekt kontroluje dostęp do swoich danych przez modyfikatory dostępu: `private` (tylko klasa), `protected` (klasa + pochodne), `public` (wszyscy). class BankAccount { private: double balance; // ukryte — nikt z zewnątrz nie zmieni bezpośrednio public: void deposit(double amt) { if (amt > 0) balance += amt; } // kontrolowany dostęp double getBalance() const { return balance; } }; **Jak enkapsulacja wspiera reużywalność?** Klasa z dobrze zdefiniowanym publicznym interfejsem jest jak „czarna skrzynka" — można ją użyć w DOWOLNYM projekcie bez znajomości implementacji. Zmiana wewnętrznej implementacji (np. zmiana struktury danych z tablicy na drzewo) NIE łamie kodu, który tej klasy używa. Dzięki temu klasa jest bezpiecznie reużywalna — użytkownik zależy od interfejsu, nie od szczegółów. **2. Abstrakcja (abstraction)** — wyodrębnianie ISTOTNYCH cech obiektu i pomijanie szczegółów nieistotnych z perspektywy użytkownika. Abstrakcja odpowiada na pytanie „CO obiekt robi?", nie „JAK to robi?". // Abstrakcja: "kształt ma pole powierzchni" — szczegóły ukryte class Shape { public: virtual double area() = 0; // CO: oblicz pole. JAK? — to zależy od kształtu }; class Circle : public Shape { double r; public: double area() override { return 3.14159 * r * r; } // JAK: π·r² }; **Jak abstrakcja wspiera reużywalność?** Kod operujący na abstrakcji (np. `void printArea(Shape& s)`) działa z KAŻDYM kształtem — kołem, prostokątem, trójkątem — bez modyfikacji. Nowy kształt = nowa klasa implementująca `Shape`, zero zmian w istniejącym kodzie. Abstrakcja tworzy stabilne „punkty podłączenia" (extension points), do których można dołączać nowe implementacje. **Różnica enkapsulacja vs abstrakcja:** Enkapsulacja = UKRYWANIE wnętrza (mechanizm ochrony). Abstrakcja = UPRASZCZANIE interfejsu (mechanizm projektowania). Enkapsulacja chroni dane, abstrakcja modeluje pojęcia. Często współdziałają: klasa abstrakcyjna (abstrakcja) z polami prywatnymi (enkapsulacja). **3. Dziedziczenie (inheritance)** — mechanizm, w którym klasa pochodna (child) przejmuje pola i metody klasy bazowej (parent). Relacja „jest" (is-a): Dog **jest** Animal. class Animal { void eat(); }; class Dog : public Animal { void bark(); }; // Dog ma eat() + bark() **Klasa bazowa / pochodna (base class / derived class)** — bazowa = rodzic, pochodna = dziecko. Pochodna dziedziczy interfejs i implementację bazowej, może dodawać własne lub nadpisywać istniejące metody. **Jak dziedziczenie wspiera reużywalność?** Klasa pochodna otrzymuje CAŁY kod bazowej „za darmo" — wystarczy napisać to, co się różni. Hierarchia klas pozwala współdzielić wspólną logikę w jednym miejscu zamiast kopiować ją do wielu klas. **Dziedziczenie wielokrotne (multiple inheritance)** — klasa dziedziczy po więcej niż jednym rodzicu. Dostępne w C++, ale nie w Java/C# (tam tylko interfejsy). Powoduje ryzyko konfliktu nazw i problem diamentu. **Problem diamentu (diamond problem)** — gdy klasa D dziedziczy po B i C, a oba dziedziczą po A, D ma dwie kopie A. Pytanie: której użyć? A / \ B C \ / D ← dwie kopie A! Rozwiązanie w C++: dziedziczenie wirtualne (`class B : virtual public A`), dzięki czemu istnieje jedna kopia A. **4. Polimorfizm (polymorphism)** — grec. „wiele form". Możliwość traktowania obiektów różnych klas przez wspólny interfejs. Kluczowy dla reużywalności — piszesz kod raz, działa z wieloma typami. Animal* a = new Dog(); a->speak(); // woła Dog::speak(), nie Animal::speak() // To samo wywołanie, różne zachowanie — polimorfizm Realizacja: funkcje wirtualne (`virtual` + `override`) — tablica vtable wskazuje na właściwą implementację. **Jak polimorfizm wspiera reużywalność?** Funkcja `void feed(Animal& a)` działa z Dog, Cat, Parrot — KAŻDĄ klasą pochodną. Nowy typ zwierzęcia NIE wymaga zmiany funkcji `feed`. Kod wywołujący jest reużywalny, bo operuje na abstrakcji (bazowa klasa/interfejs), nie na konkretnym typie. --- **Kompozycja (composition)** — obiekt zawiera inne obiekty jako pola. Relacja „ma" (has-a). Stack **ma** wektor (nie **jest** wektorem). Silniejsza enkapsulacja niż dziedziczenie, bo wnętrze komponentu jest ukryte. class Engine { int hp; }; class Car { Engine engine; // kompozycja: Car "ma" Engine }; **„Favor composition over inheritance"** — zasada GoF: preferuj kompozycję nad dziedziczenie. Dziedziczenie tworzy silne wiązanie (zmiana bazowej łamie pochodne). Kompozycja pozwala wymieniać części w runtime. **Agregacja (aggregation)** — słabsza forma kompozycji: obiekt „używa" innego, ale go nie posiada. Samochód ma kierowcę, ale kierowca istnieje niezależnie. W UML: pusty romb (◇). **Luźne wiązanie (loose coupling)** — komponenty mają minimum zależności między sobą. Zmiana jednego nie wymusza zmian w drugim. Kompozycja daje luźniejsze wiązanie niż dziedziczenie. --- **Programowanie generyczne (generic programming)** — pisanie kodu niezależnego od konkretnego typu danych. Jedna implementacja działa dla int, float, string itd. **Template (szablon, C++)** — mechanizm generyczny w C++. Kompilator generuje osobną wersję kodu dla każdego użytego typu (monomorfizacja). template T max(T a, T b) { return a > b ? a : b; } max(3, 5); // T = int max(1.5, 2.7); // T = double **Generics (Java/C#)** — odpowiednik templates, ale z type erasure (Java) lub reifikacją (C#). `List` — lista przechowująca tylko stringi; bezpieczeństwo typów bez duplikowania kodu. **STL (Standard Template Library)** — biblioteka C++ oparta na templates: kontenery (`vector`, `map`), algorytmy (`sort`, `find`), iteratory. Przykład reużywalności: jeden `sort()` sortuje dowolny kontener. --- **Interfejs (interface)** — kontrakt: zbiór metod bez implementacji. Klasa implementująca interfejs musi dostarczyć ciała wszystkich metod. W C++ → czysto wirtualne metody (`= 0`); w Java/C# → `interface`. // C++ class Drawable { public: virtual void draw() = 0; // pure virtual = interfejs }; **Klasa abstrakcyjna (abstract class)** — klasa, której nie można instancjonować; może mieć zarówno metody abstrakcyjne, jak i z implementacją. Interfejs = 100% abstrakcyjna. **Wzorce projektowe (design patterns)** — sprawdzone, reużywalne rozwiązania typowych problemów projektowych. Opisane jako: Nazwa + Problem + Rozwiązanie + Konsekwencje. **GoF (Gang of Four)** — Gamma, Helm, Johnson, Vlissides — autorzy książki „Design Patterns" (1994) z 23 wzorcami w trzech kategoriach: kreacyjne, strukturalne, behawioralne. **Strategy** — wzorzec: wymień algorytm w runtime przez interfejs. Np. różne strategie sortowania. **Observer** — wzorzec: obiekt powiadamia subskrybentów o zmianach stanu (pub/sub w OOP). **Factory** — wzorzec: tworzenie obiektów bez określania dokładnej klasy (decyzja w runtime). **Decorator** — wzorzec: dodaj zachowanie do obiektu dynamicznie, opakowując go. **Biblioteka (library)** — zbiór reużywalnego kodu wywoływanego przez nasz program (my code calls library). **Framework** — odwrotność: framework wywołuje nasz kod (Inversion of Control). Np. Unity, Django. **Trait / Mixin** — mechanizm współdzielenia kodu między klasami bez dziedziczenia. Trait (Rust, Scala) = zbiór metod do „wmixowania". Mixin (Ruby, Python) = klasa dodająca funkcjonalność przez wielodziedziczenie. --- ### Główne metody ### 1. Dziedziczenie (Inheritance) — relacja „jest" (is-a) - Klasa pochodna przejmuje atrybuty i metody bazowej - Typy: pojedyncze, wielokrotne, wielopoziomowe - Problem diamentu → dziedziczenie wirtualne w C++ - Polimorfizm (virtual, override) ### 2. Kompozycja (Composition) — relacja „zawiera" (has-a) - **„Favor composition over inheritance"** - Stack nie JEST wektorem → Stack ZAWIERA wektor - Silniejsza enkapsulacja, luźne wiązanie - Typy: kompozycja (owns), agregacja (uses), asocjacja (knows) ### 3. Programowanie generyczne (Templates/Generics) - Kod niezależny od typu: `template T max(T a, T b)` - STL jest oparta na templates - Java/C#: Generics (`List`) ### 4. Interfejsy i klasy abstrakcyjne - Kontrakt bez implementacji (pure virtual w C++, interface w Java) - Umożliwiają multiple inheritance bez diamond problem ### 5. Wzorce projektowe (Design Patterns) - Strategy, Observer, Factory, Decorator — reużywalne rozwiązania - GoF (Gang of Four) — 23 wzorce ### 6. Biblioteki, frameworki, traity/mixiny --- ### 🎮 Mostek do pracy magisterskiej — reużywalność w silnikach gier > W pracy magisterskiej implementuję bullet-hell w Unity (C#) i Unreal (C++). Silniki gier to NAJLEPSZY przykład reużywalności przez kompozycję. ![Component Pattern w silnikach gier — Composition over Inheritance](img/q6_component_pattern_engines.png) #### Component Pattern — „król reużywalności" w gamedev Unity i Unreal **odrzuciły** klasyczne dziedziczenie na rzecz **Component Pattern**: // Unity — C# Component Pattern public class EnemyController : MonoBehaviour { [RequireComponent(typeof(Health))] // ← KOMPOZYCJA! [RequireComponent(typeof(Collider2D))] void Start() { var hp = GetComponent(); // ← has-a, nie is-a } } // Unreal — C++ Component Pattern AEnemy::AEnemy() { HealthComp = CreateDefaultSubobject("Health"); MeshComp = CreateDefaultSubobject("Mesh"); } #### Dlaczego NIE dziedziczenie? Klasyczna hierarchia (`GameObject → Enemy → FlyingEnemy → ShootingFlyingEnemy`) prowadzi do: - **Diamond problem** — Shooting + Flying = ??? - **Fragile base class** — zmiana w Enemy psuje 50 podklas - **Combinatorial explosion** — N cech x M typów = NxM klas Kompozycja: Enemy = Transform + Health + AI + Sprite + Collider → **dowolna kombinacja** #### Reużywalność w mojej pracy — konkretne przykłady | Mechanizm OOP | Mój kod Unity | Unreal odpowiednik | |---------------|---------------|---------------------| | Kompozycja | `[RequireComponent(typeof(Health))]` | `CreateDefaultSubobject()` | | Singleton | `GameDirector.Instance` | `UGameInstance` subsystem | | Observer | `C# event OnEnemyDeath` | `FOnEnemyDeath` multicast delegate | | Object Pool | `BulletPool` (generic) | `TPooledObject` (template) | | Interface | `IDamageable` | `UInterface` + `IDamageable` | #### Mnemonik — „KSIOP" = Kompozycja Silnika I Obiektowy Pooling - **K**ompozycja (Component Pattern — fundamentalna) - **S**ingleton (global managers) - **I**nterfejsy (IDamageable, IInteractable) - **O**bserver (events/delegates) - **P**ooling (Object Pool dla performance) Na obronie: *„Moja praca jest doskonałym przykładem 'composition over inheritance' — w Unity każdy obiekt gry składa się z komponentów (MonoBehaviour), a w Unrealu z UActorComponent. Object Pooling to generyczna reużywalność eliminująca GC."* --- ### Etymologia **OOP** — Alan Kay (Smalltalk, 1970s), sam ukuł termin „object-oriented". **GoF** — Gang of Four: Gamma, Helm, Johnson, Vlissides (1994). **Polimorfizm** — grec. „poly" (wiele) + „morphē" (forma) = wiele postaci. **Enkapsulacja** — łac. „capsula" = pudełeczko. **Abstrakcja** — łac. „abstrahere" = odciągać, oddzielać (oddzielanie istoty od szczegółów). **Design Pattern** — z architektury: Christopher Alexander „A Pattern Language" (1977); GoF zaadaptowali do IT. **Kompozycja > Dziedziczenie** — zasada z GoF: „favor object composition over class inheritance". ### Jak zapamiętać - **4 filary OOP:** Enkapsulacja (ukrywanie) → Abstrakcja (upraszczanie) → Dziedziczenie (przejmowanie) → Polimorfizm (wielopostaciowość) - Enkapsulacja = czarna skrzynka → bezpieczna reużywalność. Abstrakcja = punkty rozszerzenia → otwarta reużywalność - **„Kompozycja > Dziedziczenie"** — najważniejsza zasada - Dziedziczenie: silne wiązanie, krucha klasa bazowa, diamond problem - Kompozycja: elastyczna, testowalna, preferowana - Granica: dziedziczenie dla prawdziwego „is-a" z polimorfizmem; kompozycja dla reszty