Start Kontakt

Zasada SOLID (4/4)

W czwartym, a zarazem ostatnim artykule o SOLID przeanalizujemy zasadę Dependency Inversion Principle, czyli odwrócenia zależności.

Dependency Inversion Principle (zasada odwrócenia zależności)

Zasada odwrócenia zależności mówi, że klasy wysokopoziomowe nie powinny być zależne od klas niskopoziomowych. Wszystkie one powinny zależeć od abstrakcji. Inaczej mówiąc, należy odwrócić zależność: to szczegóły implementacyjne powinny zależeć od abstrakcyjnych definicji, a nie odwrotnie.

Zgodnie ze swoją nazwą, klasy wysokopoziomowe dotyczą obsługi logiki aplikacji i nie powinny mieć bezpośrednio do czynienia z klasami niskopoziomowymi, które zajmują się podstawowymi operacjami takimi jak dostęp do dysku, przesyłanie danych w sieci komputerowej itp.

Omówmy to zagadnienie na przykładzie. W grze będziemy tworzyć obiekty, które odpowiadają graczom realnym - mogą to być gracze lokalni czy też sieciowi. W tym celu zdefiniowaliśmy dwie klasy odpowiadające typom graczy: PlayerLocal i PlayerNetwork. Mamy też główną klasę GameLogic zajmującą się logiką gry i za pomocą jej odpowiednich metod dodajemy graczy do gry. Oto kod:

class PlayerLocal
{
public:

    PlayerLocal(std::string _name) : name{ _name }
    {
        std::cout << "Gracz lokalny dodany: " << name << std::endl;
    }
    std::string getName(void) const { return name;  }

private:

    std::string name;
};

class PlayerNetwork
{
public:

    PlayerNetwork(std::string _name) : name{ _name }
    {
        std::cout << "Gracz sieciowy dodany: " << name << std::endl;
        // obsługa innych parametrów gracza sieciowego jak adres IP itp.
    }
    std::string getName(void) const { return name; }

private:

    std::string name;
    // inne zmienne jak adres IP itd.
};

class GameLogic
{
public:

    void addPlayerLocal(std::string name)
    {
        plLocal.push_back(PlayerLocal(name));
    }

    void addPlayerNetwork(std::string name)
    {
        plNetwork.push_back(PlayerNetwork(name));
    }

    void displayPlayers(void)
    {
        for (const PlayerLocal& pl : plLocal)
            std::cout << pl.getName() << " ";
        for (const PlayerNetwork& pl : plNetwork)
            std::cout << pl.getName() << " ";
        std::cout << std::endl;
    }

private:

    std::vector<PlayerLocal>  plLocal;
    std::vector<PlayerNetwork>  plNetwork;
};


int main()
{
    GameLogic game;
    game.addPlayerLocal("Robert");
    game.addPlayerLocal("Zygmunt");
    game.addPlayerNetwork("Mirek");
    game.displayPlayers();
}

A oto uzyskane wyniki:

Gracz lokalny dodany: Robert
Gracz lokalny dodany: Zygmunt
Gracz sieciowy dodany: Mirek
Robert Zygmunt Mirek

Kod działa, ale nie jest jednak optymalny. Jak widzimy, klasa wysokopoziomowa GameLogic jest zależna od klas niskopoziomowych takich jak PlayerNetwork czy PlayerLocal, które zajmują się czysto technicznymi aspektami, np. adresami IP.

Spróbujmy więc zmienić ten stan i sprawić, by klasy były zależne od abstrakcji. Przez abstrakcję rozumiemy taką klasę, na podstawie której można dopiero implementować konkretne klasy.

Zdefiniujemy więc abstrakcję dla graczy, za pomocą której będziemy implementować klasy dziedziczone:

class Player
{
public:
    std::string getName(void) const { return name; }
    void setName(const std::string _name) { name = _name; }

private:
    std::string name;

};

class PlayerLocal : public Player
{
public:

    PlayerLocal(std::string _name) 
    {   
        setName(_name);
        std::cout << "Gracz lokalny dodany: " << getName() << std::endl;
    }
};

class PlayerNetwork : public Player
{
public:

    PlayerNetwork(std::string _name)
    {
        setName(_name);
        std::cout << "Gracz sieciowy dodany: " << getName() << std::endl;
    }
};

Zmieńmy też odpowiednio klasę GameLogic:

class GameLogic
{
public:

    void addPlayer(Player *pl)
    {
        players.push_back(pl);
    }

    void displayPlayers(void)
    {
        for (const Player* pl : players)
            std::cout << pl->getName() << " ";
        std::cout << std::endl;
    }

private:

    std::vector<Player*>  players;
};

Po uruchomieniu programu uzyskaliśmy te same wyniki, co poprzednio:

int main()
{
    GameLogic game;
    game.addPlayer(new PlayerLocal("Robert"));
    game.addPlayer(new PlayerLocal("Zygmunt"));
    game.addPlayer(new PlayerNetwork("Mirek"));
    game.displayPlayers();
}    
Gracz lokalny dodany: Robert
Gracz lokalny dodany: Zygmunt
Gracz sieciowy dodany: Mirek
Robert Zygmunt Mirek

Widzimy jednak, że klasa wysokopoziomowa GameLogic nie jest już zależna od niskopoziomowych klas odpowiadających poszczególnym typom graczy, lecz od wysokopoziomowej abstracji Player. Dzięki takiemu rozwiązaniu klasa GameLogic nie będzie musiała być w ogóle modyfikowana, jeśli pojawi się nowy rodzaj gracza, na przykład PlayerAI odpowiadający sztucznej inteligencji - w takim przypadku wystarczy po prostu zaimplementować nową klasę PlayerAI na podstawie abstrakcji Player i to wszystko.