praca_magisterska/pytania/questions/pytanie_06.md

244 lines
13 KiB
Markdown

## 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<typename T>
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<String>` — 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<typename T> T max(T a, T b)`
- STL jest oparta na templates
- Java/C#: Generics (`List<T>`)
### 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<Health>(); // ← has-a, nie is-a
}
}
// Unreal — C++ Component Pattern
AEnemy::AEnemy() {
HealthComp = CreateDefaultSubobject<UHealthComponent>("Health");
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>("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<UHealthComp>()` |
| Singleton | `GameDirector.Instance` | `UGameInstance` subsystem |
| Observer | `C# event OnEnemyDeath` | `FOnEnemyDeath` multicast delegate |
| Object Pool | `BulletPool<Bullet>` (generic) | `TPooledObject<ABullet>` (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