praca_magisterska/pytania/questions/pytanie_06.md

13 KiB

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

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

  • Kompozycja (Component Pattern — fundamentalna)
  • Singleton (global managers)
  • Interfejsy (IDamageable, IInteractable)
  • Observer (events/delegates)
  • Pooling (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