Jak programować nieobiektowo programując obiektowo

przez | 6 grudnia 2019

Kolejna ciekawa sytuacja, z którą prawdopodobnie nie jeden programista się prędzej czy później zetknie, albo nawet sam doprowadzi do jej zaistnienia – pozbycie się zalet programowania obiektowego.

Kiedy idziemy na rozmowę kwalifikacyjną i zacznie się temat programowania obiektowego zostaniemy zapytani o to co to jest polimorfizm i dziedziczenie. Oczywiście mamy na taką sytuację przygotowaną wykutą na blachę regułkę, którą wyrecytujemy jedynie odrobinę wolniej niż nasz imię i nazwisko (a są i tacy którym może się to udać nawet szybciej http://hoaxes.org/weblog/comments/worlds_longest_surname). Rozmowa przebiega pomyślnie, zaczynamy pracować w projekcie, na początku naprawiamy błędy, potem implementujemy nowe funkcjonalności. Zdobywamy doświadczenie, stajemy się lepsi.

Pewnego dnia robimy porządki w kodzie i znajdujemy taki kwiatek:

class BadBase {
public:
    enum class BadType {
        BadT0,
        BadTA,
        BadTB,
        BadTC
    };
    BadBase(BadType type = BadType::BadT0) : mType(type) {}
    BadType getType() { return mType; }
protected:
    const BadType mType;
};

class BadA : public BadBase {
public:
    BadA() : BadBase(BadType::BadTA) {}
};

class BadB : public BadBase {
public:
    BadB() : BadBase(BadType::BadTB) {}
    bool isActionValid() { return true; }
};

class Container {
    vector<BadBase *> mBads = { new BadA(), new BadB()};
public:
    Container() {}
    ~Container() {
        for (auto bad : mBads) {
            delete bad;
        }
    }

    void badExample() {
        for (auto bad : mBads) {
            if (bad->getType() == BadBase::BadType::BadTB) {
                auto badb = static_cast<BadB *>(bad);
                if (badb->isActionValid()) {
                    cout << "BadB should do some stuff" << endl;
                }
            }
        }
    }
};

Metoda badExample przechodzi przez wszystkie elementy wektora mBads i sprawdza typ obiektu na który wskazuje wskaźnik, jeżeli jest to obiekt odpowiedniego typu, rzutujemy wskaźnik na właściwy typ i wołamy metodę tej klasy. A przecież można by to było rozwiązać bardziej elegancko:

class GoodBase {
public:
    GoodBase() {}
    virtual bool isActionValid() const { return false; }
};

class GoodA : public GoodBase {
public:
    GoodA() {};
};

class GoodB : public GoodBase {
public:
    GoodB() {};
    bool isActionValid() const override { return true; }
};

class Container {
    vector<GoodBase *> mGoods = { new GoodA(), new GoodB()};
public:
    Container() {}
    ~Container() {
        for (auto good : mGoods) {
            delete good;
        }
    }

    void goodExample() {
        for (auto good: mGoods) {
            if (good->isActionValid()) {
                cout << "Good should do some stuff" << endl;
            }
        }
    }
};

Ja przesiadłem się dość niedawno z projektu gdzie praktycznie w ogóle nie programowałem obiektowo (brzmi jak słaba wymówka) i kod, który podałem jako zły przykład wyglądał zupełnie normalnie i logicznie (w gruncie rzeczy nie jest to błędna implementacja z punktu widzenia użytkownika, który jedynie używa tej funkcjonalności), kolega który ma znacznie więcej doświadczenia, ode mnie, w programowaniu obiektowym zwrócił mi na to uwagę, kiedy grzebałem gdzieś w okolicy podobnie zepsutego kodu.

Znajmy podstawy i pamiętajmy o nich, bo po jakimś czasie zapominamy o tych podstawowych zasadach na rzecz fikuśnych template’ów czy innych funkcji lambda.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *