praca_magisterska/pytania/odpowiedzi/06-reużywalność-kodu-oop.md

21 KiB

Pytanie 6: Metody reużywalności kodu w językach obiektowych

Pytanie

"Omówić metody reużywalności kodu i struktur danych w obiektowych językach programowania."

Przedmiot: PROI (Programowanie Obiektowe)


📚 Odpowiedź główna

Wprowadzenie

Reużywalność kodu (code reuse) to fundamentalna zasada inżynierii oprogramowania - "nie wynajduj koła na nowo". W programowaniu obiektowym mamy kilka mechanizmów umożliwiających wielokrotne wykorzystanie kodu.

Główne metody reużywalności

┌─────────────────────────────────────────────────────────────────┐
│                    METODY REUŻYWALNOŚCI                         │
├─────────────────┬─────────────────┬─────────────────────────────┤
│   DZIEDZICZENIE │    KOMPOZYCJA   │      PROGRAMOWANIE          │
│   (Inheritance) │   (Composition) │      GENERYCZNE             │
├─────────────────┼─────────────────┼─────────────────────────────┤
│   INTERFEJSY    │   DELEGACJA     │      BIBLIOTEKI             │
│   (Interfaces)  │   (Delegation)  │      (Libraries)            │
├─────────────────┼─────────────────┼─────────────────────────────┤
│     MIXINY      │    TRAITY       │   WZORCE PROJEKTOWE         │
│    (Mixins)     │   (Traits)      │   (Design Patterns)         │
└─────────────────┴─────────────────┴─────────────────────────────┘

1. Dziedziczenie (Inheritance)

Definicja

Dziedziczenie to mechanizm, w którym klasa pochodna (child) przejmuje atrybuty i metody klasy bazowej (parent).

Typy dziedziczenia

Typ Opis Języki
Pojedyncze Jedna klasa bazowa Java, C#
Wielokrotne Wiele klas bazowych C++, Python
Wielopoziomowe A → B → C Wszystkie
Hierarchiczne A → B, A → C Wszystkie

Przykład (C++)

// Klasa bazowa
class Pojazd {
protected:
    std::string marka;
    int predkosc;
    
public:
    Pojazd(const std::string& m) : marka(m), predkosc(0) {}
    
    virtual void jedz() {
        std::cout << marka << " jedzie " << predkosc << " km/h\n";
    }
    
    virtual ~Pojazd() = default;
};

// Klasa pochodna - dziedziczy i rozszerza
class Samochod : public Pojazd {
private:
    int liczbaDrzwi;
    
public:
    Samochod(const std::string& m, int drzwi) 
        : Pojazd(m), liczbaDrzwi(drzwi) {}
    
    void jedz() override {
        predkosc = 120;
        Pojazd::jedz();  // wywołanie metody bazowej
        std::cout << "Drzwi: " << liczbaDrzwi << "\n";
    }
    
    void parkuj() { /* nowa metoda */ }
};

// Użycie
Samochod auto1("BMW", 4);
auto1.jedz();  // Polimorfizm - wywołuje Samochod::jedz()

Zalety i wady dziedziczenia

Zalety Wady
Naturalne modelowanie hierarchii Silne wiązanie (tight coupling)
Polimorfizm Problem kruchej klasy bazowej
Łatwe rozszerzanie Problemy z wielodziedziczeniem (diamond)
Współdzielenie implementacji Narusza enkapsulację

Problem diamentu (Diamond Problem)

       A
      / \
     B   C
      \ /
       D
class A { public: void metoda() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};  // Która A::metoda()?

D d;
// d.metoda();  // BŁĄD: niejednoznaczne!
d.B::metoda();  // OK - jawne wskazanie

Rozwiązanie w C++: Dziedziczenie wirtualne

class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};  // Jedna kopia A

2. Kompozycja (Composition)

Definicja

Kompozycja to budowanie złożonych obiektów z prostszych komponentów. Relacja "zawiera" (has-a) zamiast "jest" (is-a).

Zasada: "Favor composition over inheritance"

// ZŁE - dziedziczenie
class Stack : public std::vector<int> {
    // Stack NIE JEST wektorem!
    // Użytkownik może wywołać insert(), erase()...
};

// DOBRE - kompozycja
class Stack {
private:
    std::vector<int> elements;  // Stack ZAWIERA wektor
    
public:
    void push(int x) { elements.push_back(x); }
    int pop() { 
        int top = elements.back();
        elements.pop_back();
        return top;
    }
    bool empty() const { return elements.empty(); }
    // Tylko te metody, które mają sens dla stosu
};

Typy relacji obiektowych

Relacja Siła Cykl życia Przykład
Kompozycja Silna Zależny (owns) Samochód → Silnik
Agregacja Słaba Niezależny (uses) Uniwersytet → Student
Asocjacja Luźna Niezależny Klient ↔ Zamówienie
// Kompozycja - silnik "umiera" z samochodem
class Samochod {
private:
    Silnik silnik;  // Obiekt wewnętrzny
public:
    Samochod() : silnik() {}  // Tworzy silnik
    // ~Samochod() niszczy silnik automatycznie
};

// Agregacja - student istnieje niezależnie od uniwersytetu
class Uniwersytet {
private:
    std::vector<Student*> studenci;  // Wskaźniki/referencje
public:
    void dodajStudenta(Student* s) { studenci.push_back(s); }
    // ~Uniwersytet() NIE niszczy studentów
};

3. Programowanie generyczne (Generic Programming)

Definicja

Programowanie generyczne to pisanie kodu niezależnego od konkretnych typów danych, używając szablonów (templates) lub generyków.

Szablony w C++

// Szablon funkcji
template<typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Użycie - kompilator generuje wersje dla każdego typu
int m1 = maximum(3, 5);           // int
double m2 = maximum(3.14, 2.71);  // double
std::string m3 = maximum("abc", "xyz");  // string

// Szablon klasy
template<typename T, size_t N>
class Array {
private:
    T data[N];
public:
    T& operator[](size_t i) { return data[i]; }
    constexpr size_t size() const { return N; }
};

Array<int, 10> arr;     // Tablica 10 intów
Array<double, 5> darr;  // Tablica 5 double'i

Generyki w Java/C#

// Java
public class Box<T> {
    private T value;
    
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

// Ograniczenia typów (bounded type parameters)
public <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}
// C#
public class Repository<T> where T : class, IEntity, new() {
    public T Create() => new T();
    public void Save(T entity) { /* ... */ }
}

Zalety programowania generycznego

Zaleta Opis
Type safety Błędy wykrywane w czasie kompilacji
Brak duplikacji Jeden kod dla wielu typów
Wydajność C++: specjalizacja w kompilacji, brak rzutowania
Czytelność Jawne wymagania typów

4. Interfejsy (Interfaces)

Definicja

Interfejs definiuje kontrakt (zestaw metod), który klasa musi zaimplementować. Oddziela "co" od "jak".

Przykład

// Java - interfejs
public interface Drawable {
    void draw();
    default void clear() { /* domyślna implementacja */ }
}

public interface Resizable {
    void resize(int width, int height);
}

// Klasa implementuje wiele interfejsów
public class Rectangle implements Drawable, Resizable {
    @Override
    public void draw() { /* implementacja */ }
    
    @Override
    public void resize(int w, int h) { /* implementacja */ }
}
// C++ - klasa abstrakcyjna jako interfejs
class IDrawable {
public:
    virtual void draw() = 0;  // Pure virtual
    virtual ~IDrawable() = default;
};

class IResizable {
public:
    virtual void resize(int w, int h) = 0;
    virtual ~IResizable() = default;
};

class Rectangle : public IDrawable, public IResizable {
public:
    void draw() override { /* ... */ }
    void resize(int w, int h) override { /* ... */ }
};

Interfejsy vs Klasy abstrakcyjne

Cecha Interfejs Klasa abstrakcyjna
Wielodziedziczenie TAK NIE (Java/C#)
Pola NIE (do Java 8) TAK
Konstruktor NIE TAK
Implementacja metod default (Java 8+) TAK
Cel Definiuje kontrakt Współdzieli implementację

5. Delegacja (Delegation)

Definicja

Delegacja to przekazywanie odpowiedzialności za wykonanie operacji do innego obiektu.

// Bez delegacji - dziedziczenie
class LoggingList : public std::list<int> {
public:
    void push_back(int x) {
        log("Adding: " + std::to_string(x));
        std::list<int>::push_back(x);
    }
};

// Z delegacją - kompozycja
class LoggingList {
private:
    std::list<int> delegate;  // Delegat
    
public:
    void push_back(int x) {
        log("Adding: " + std::to_string(x));
        delegate.push_back(x);  // Delegacja
    }
    
    size_t size() const { 
        return delegate.size();  // Delegacja
    }
};

Wzorzec strategii (Strategy Pattern)

// Interfejs strategii
class SortStrategy {
public:
    virtual void sort(std::vector<int>& data) = 0;
    virtual ~SortStrategy() = default;
};

class QuickSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override { /* quicksort */ }
};

class MergeSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override { /* mergesort */ }
};

// Kontekst deleguje sortowanie do strategii
class Sorter {
private:
    std::unique_ptr<SortStrategy> strategy;
    
public:
    void setStrategy(std::unique_ptr<SortStrategy> s) {
        strategy = std::move(s);
    }
    
    void performSort(std::vector<int>& data) {
        strategy->sort(data);  // Delegacja
    }
};

6. Mixiny i Traity

Mixiny (Mixins)

Klasy dostarczające funkcjonalność do "wmieszania" do innych klas.

# Python - mixiny przez wielodziedziczenie
class JSONSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class XMLSerializableMixin:
    def to_xml(self):
        # implementacja...
        pass

class User(JSONSerializableMixin, XMLSerializableMixin):
    def __init__(self, name, email):
        self.name = name
        self.email = email

user = User("Jan", "jan@example.com")
print(user.to_json())  # {"name": "Jan", "email": "jan@example.com"}

Traity (Traits)

// Rust - traits
trait Drawable {
    fn draw(&self);
}

trait Movable {
    fn move_to(&mut self, x: i32, y: i32);
}

struct Circle {
    x: i32,
    y: i32,
    radius: i32,
}

impl Drawable for Circle {
    fn draw(&self) { /* ... */ }
}

impl Movable for Circle {
    fn move_to(&mut self, x: i32, y: i32) {
        self.x = x;
        self.y = y;
    }
}
// PHP - traits
trait Timestampable {
    public $createdAt;
    public $updatedAt;
    
    public function touch() {
        $this->updatedAt = new DateTime();
    }
}

trait SoftDeletable {
    public $deletedAt;
    
    public function softDelete() {
        $this->deletedAt = new DateTime();
    }
}

class Article {
    use Timestampable, SoftDeletable;
    
    public $title;
}

7. Biblioteki i frameworki

Poziomy reużywalności

┌─────────────────────────────────────────────────────┐
│                    FRAMEWORK                         │
│    (IoC, definiuje architekturę aplikacji)          │
├─────────────────────────────────────────────────────┤
│                    BIBLIOTEKA                        │
│    (kolekcja klas/funkcji, wywołujesz Ty)           │
├─────────────────────────────────────────────────────┤
│                    MODUŁ/PAKIET                      │
│    (logicznie powiązane klasy)                      │
├─────────────────────────────────────────────────────┤
│                    KLASA                             │
│    (jednostka enkapsulacji)                         │
├─────────────────────────────────────────────────────┤
│                    FUNKCJA                           │
│    (najmniejsza jednostka reużywalna)               │
└─────────────────────────────────────────────────────┘

Przykłady

Poziom Przykłady
Framework Spring, .NET, Unity, Django
Biblioteka STL, Boost, jQuery, NumPy
Moduł java.util, System.IO
Klasa ArrayList, HttpClient

8. Wzorce projektowe (Design Patterns)

Wzorce wspierające reużywalność

Wzorzec Typ Cel
Factory Method Kreacyjny Delegacja tworzenia obiektów
Abstract Factory Kreacyjny Rodziny powiązanych obiektów
Prototype Kreacyjny Klonowanie obiektów
Adapter Strukturalny Dopasowanie interfejsów
Decorator Strukturalny Dynamiczne rozszerzanie
Strategy Behawioralny Wymienne algorytmy
Template Method Behawioralny Szkielet algorytmu z krokami

Przykład: Template Method

// Szkielet algorytmu w klasie bazowej
class DataParser {
public:
    // Template method - definiuje szkielet
    void parse(const std::string& data) {
        openFile();
        readHeader();
        processData(data);  // Krok abstrakcyjny
        closeFile();
    }
    
protected:
    virtual void openFile() { /* domyślna impl */ }
    virtual void readHeader() { /* domyślna impl */ }
    virtual void processData(const std::string& data) = 0;  // PURE
    virtual void closeFile() { /* domyślna impl */ }
};

// Konkretne implementacje
class JSONParser : public DataParser {
protected:
    void processData(const std::string& data) override {
        // Parsowanie JSON
    }
};

class XMLParser : public DataParser {
protected:
    void processData(const std::string& data) override {
        // Parsowanie XML
    }
};

📊 Porównanie metod reużywalności

Metoda Wiązanie Elastyczność Złożoność
Dziedziczenie Statyczne Niska Średnia
Kompozycja Dynamiczne Wysoka Niska
Interfejsy Statyczne* Wysoka Niska
Generyki Statyczne Wysoka Średnia
Delegacja Dynamiczne Wysoka Średnia
Mixiny/Traity Statyczne Średnia Średnia

*ale implementacja może być dynamiczna (polimorfizm)


🧠 Mnemoniki

"SOLID" - zasady dobrego OOP:

  • Single Responsibility - jedna odpowiedzialność
  • Open/Closed - otwarte na rozszerzenia, zamknięte na modyfikacje
  • Liskov Substitution - podtyp zastępuje typ bazowy
  • Interface Segregation - wiele małych interfejsów > jeden duży
  • Dependency Inversion - zależność od abstrakcji, nie konkretu

"HAS-A przed IS-A":

  • HAS-A = kompozycja (Samochód HAS-A Silnik)
  • IS-A = dziedziczenie (Samochód IS-A Pojazd)
  • Preferuj HAS-A!

"DRIED" dla reużywalności:

  • Don't Repeat - nie powtarzaj kodu
  • Interfaces - definiuj kontrakty
  • Encapsulate - ukrywaj szczegóły
  • Delegate - przekazuj odpowiedzialność

"GIT" dla generyków:

  • Generic - niezależne od typu
  • Invariant - sprawdzane w kompilacji
  • Type-safe - bezpieczne typowanie

Możliwe pytania dodatkowe (follow-up)

Q1: "Kiedy dziedziczenie, a kiedy kompozycja?"

Odpowiedź:

Użyj dziedziczenia gdy: Użyj kompozycji gdy:
Relacja "jest" (is-a) Relacja "zawiera" (has-a)
Potrzebujesz polimorfizmu Potrzebujesz elastyczności
Klasa pochodna JEST typem bazowym Chcesz zmieniać zachowanie w runtime
Zasada Liskov jest spełniona Chcesz uniknąć problemów z hierarchią

Przykład decyzji:

  • Ptak IS-A Zwierzę → dziedziczenie OK
  • Samochód HAS-A Silnik → kompozycja!
  • Stack IS-A Vector? → NIE! Kompozycja.

Q2: "Co to jest zasada substytucji Liskov?"

Odpowiedź:

"Obiekty klasy bazowej powinny być zastępowalne obiektami klas pochodnych bez zmiany poprawności programu."

Naruszenie LSP:

class Rectangle {
public:
    virtual void setWidth(int w) { width = w; }
    virtual void setHeight(int h) { height = h; }
    int area() { return width * height; }
protected:
    int width, height;
};

class Square : public Rectangle {  // PROBLEM!
public:
    void setWidth(int w) override { width = height = w; }
    void setHeight(int h) override { width = height = h; }
};

// Kod klienta
void resize(Rectangle& r) {
    r.setWidth(5);
    r.setHeight(10);
    assert(r.area() == 50);  // FAIL dla Square!
}

Rozwiązanie: Kwadrat nie powinien dziedziczyć po prostokącie (matematycznie IS-A, programistycznie NIE).


Q3: "Czym różnią się szablony C++ od generyków Java?"

Odpowiedź:

Cecha C++ Templates Java Generics
Implementacja Kompilacja (code generation) Type erasure (runtime)
Specjalizacja TAK NIE
Prymitywy TAK (int, double...) NIE (tylko obiekty)
Sprawdzanie typu W czasie instancjacji W czasie kompilacji
Ograniczenia Concepts (C++20) Bounded types
Wydajność Optymalna Rzutowanie w runtime
// C++ - specjalizacja szablonu
template<typename T>
class Container { /* ogólna implementacja */ };

template<>
class Container<bool> { /* specjalna dla bool - bitset */ };

Q4: "Co to jest Dependency Injection?"

Odpowiedź:

Dependency Injection (DI) = wzorzec przekazywania zależności z zewnątrz zamiast tworzenia ich wewnątrz.

// BEZ DI - silne wiązanie
class OrderService {
private:
    MySQLDatabase db;  // Zależność "zahardkodowana"
public:
    OrderService() : db() {}
};

// Z DI - luźne wiązanie
class OrderService {
private:
    IDatabase& db;  // Zależność od abstrakcji
public:
    OrderService(IDatabase& database) : db(database) {}  // Injection
};

// Użycie
MySQLDatabase mysql;
PostgresDatabase postgres;

OrderService service1(mysql);    // Możemy łatwo zmienić
OrderService service2(postgres);  // implementację

Typy DI:

  1. Constructor Injection (preferowany)
  2. Setter Injection
  3. Interface Injection

Q5: "Jak wzorce projektowe wspierają reużywalność?"

Odpowiedź:

Wzorzec Mechanizm reużywalności
Factory Oddziela tworzenie od użycia
Strategy Wymienna rodzina algorytmów
Decorator Dynamiczne dodawanie funkcji
Adapter Reużycie niekompatybilnych klas
Template Method Reużycie szkieletu algorytmu
Composite Jednolite traktowanie obiektów i kolekcji

Q6: "Wyjaśnij różnicę między agregacją a kompozycją"

Odpowiedź:

Cecha Kompozycja Agregacja
Siła związku Silna ("owns") Słaba ("uses")
Cykl życia Zależny Niezależny
UML Wypełniony romb ◆ Pusty romb ◇
Przykład Samochód → Silnik Firma → Pracownik
// Kompozycja - silnik jest częścią samochodu
class Car {
    Engine engine;  // Engine tworzony z Car, niszczony z Car
};

// Agregacja - pracownik może istnieć bez firmy
class Company {
    std::vector<Employee*> employees;  // Wskaźniki - nie "posiada"
};

🎯 Kluczowe punkty do zapamiętania

  1. Kompozycja > Dziedziczenie - elastyczność, luźne wiązanie
  2. Interfejsy oddzielają kontrakt od implementacji
  3. Generyki eliminują duplikację kodu dla różnych typów
  4. SOLID - 5 zasad dobrego OOP
  5. Delegacja - przekazuj odpowiedzialność
  6. Wzorce projektowe - sprawdzone rozwiązania typowych problemów

📖 Źródła do pogłębienia

  1. Gamma et al. - "Design Patterns: Elements of Reusable OO Software" (GoF)
  2. Martin, R. - "Clean Code" i "Clean Architecture"
  3. Meyers, S. - "Effective C++"
  4. Bloch, J. - "Effective Java"