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.