DEV Community

RYU JAEMIN
RYU JAEMIN

Posted on

[BlindSpot] Log 04. Let's follow the SOLID principles : DIP

SOLID Principles

Last time, we learned about SRP, one of the SOLID principles, and refactored our code based on it. This time, we'll refactor our code based on the DIP principle.

DIP(Dependency Inversion Principle)

DIP is the principle that higher-level modules should depend on abstractions (interfaces) rather than on the concrete implementations of lower-level modules.
Failure to adhere to this principle can lead to increased dependencies between classes, such that modifying one class can lead to subsequent modifications to other classes affected by the change. Furthermore, if a test class lacks an interface, a new class must be created to perform the same functionality.
In my code, there are currently a large number of Manager/Service objects that use the Singleton pattern. If I were applying DIP, it might be easier to use something like TestAuthManager to load dummy data for testing before applying the DB later. I'll split Manager classes out to adhere to the Dependency Independent Programming (DIP) principle.

Why only Managers?

Currently, both the Manager and Service classes are singletons, using no interfaces. I'll create an interface for the Manager class here. This is because, if we later implement sign-up/login functionality, Managers will likely need to interact with the database, requiring a complete overhaul of the entire logic. If we were to use the Manager object directly instead of an interface, other code would also need to be completely overhauled. Since the Service object is responsible for the actual running logic, I decided that creating an interface was a low priority.
However, dependency injection will also be applied to service and handler classes without interface.

Let's follow the DIP principle

Making interfaces for managers

First, I made interfaces for managers(Auth,Player,Room,Session).
It contains functions from existing classes.

//IAuthManager.h

class IAuthManager {
public:
    virtual ~IAuthManager() = default;

    virtual int32_t GetPlayerIdByToken(const std::string& token) = 0;
    virtual std::string GenerateToken() = 0;
    virtual void RemoveToken(const std::string& token) = 0;
    virtual void RegisterToken(const std::string& token, int32_t playerId) = 0;
};
//... Make other interfaces the same
Enter fullscreen mode Exit fullscreen mode

Inheriting an interface

I inherited the interface from the manager.
At this point, copy constructors and copy assignment operators are prevented.

AuthManager(const AuthManager&) = delete;
AuthManager& operator=(const AuthManager&) = delete;
Enter fullscreen mode Exit fullscreen mode

This is because the mutex used by AuthManager cannot be copied, and even if there is no mutex, the manager object must remain intact.

//AuthManager.h
#include "../Core/Interfaces/IAuthManager.h"

class AuthManager : public IAuthManager{

public:
    AuthManager() = default;
    virtual ~AuthManager() = default;


    int32_t GetPlayerIdByToken(const std::string& token) override;
    std::string GenerateToken() override;
    void RemoveToken(const std::string& token) override;
    void RegisterToken(const std::string& token, int32_t playerId) override;
private:
    AuthManager(const AuthManager&) = delete;
    AuthManager& operator=(const AuthManager&) = delete;

    std::mutex token_mutex_;
    std::map<std::string, int32_t> tokenToPlayerId_;
    std::mutex name_mutex_;
    std::map<int32_t, std::string> playerIdToName_;
};
Enter fullscreen mode Exit fullscreen mode

Injecting dependencies

Now I should inject dependencies to Service.
This should apply to all services, but I'll just use AuthService as an example.
First, I created a manager interface pointer required for private.

class AuthService {
public:

private:
    std::shared_ptr<IAuthManager> authMgr_;
    std::shared_ptr<ISessionManager> sessionMgr_;
    std::shared_ptr<IPlayerManager> playerMgr_;
};
Enter fullscreen mode Exit fullscreen mode

And, And I created a constructor that includes those managers. As you can see from the code, the Service does not use static variables.

//AuthService.h
class AuthService {
public:

    AuthService(std::shared_ptr<IAuthManager> authMgr,
        std::shared_ptr<IPlayerManager> playerMgr,
        std::shared_ptr<ISessionManager> sessionMgr)
        : authMgr_(authMgr), playerMgr_(playerMgr), sessionMgr_(sessionMgr) {
    }
    void Login(std::shared_ptr<Session> session, blindspot::LoginRequest& pkt);

private:
    std::shared_ptr<IAuthManager> authMgr_;
    std::shared_ptr<ISessionManager> sessionMgr_;
    std::shared_ptr<IPlayerManager> playerMgr_;
};
Enter fullscreen mode Exit fullscreen mode

The singleton pattern using the Instance() function is no longer used. Therefore, it can be used by calling the manager from within the internal function.

//Before
newPlayer->name = PlayerManager.Instance().GetPlayerNameById(playerId);
//After
newPlayer->name = playerMgr_->GetPlayerNameById(playerId);
Enter fullscreen mode Exit fullscreen mode

The service has been injected with a manager, and that service will now be injected with a handler.

using PacketFunc = std::function<void(std::shared_ptr<Session>, uint8_t*, uint16_t)>;

class ServerPacketHandler {
public:
    ServerPacketHandler(std::shared_ptr<AuthService> authService, std::shared_ptr<RoomService> roomService);

    void Init();
    void HandlePacket(std::shared_ptr<Session> session, uint16_t id, uint8_t* payload, uint16_t size);

private:
    void Handle_LOGIN_REQUEST(std::shared_ptr<Session> session, blindspot::LoginRequest& pkt);
    void Handle_JOIN_ROOM_REQUEST(std::shared_ptr<Session> session, blindspot::JoinRoomRequest& pkt);
    void Handle_MAKE_ROOM_REQUEST(std::shared_ptr<Session> session, blindspot::MakeRoomRequest& pkt);

private:
    std::shared_ptr<AuthService> authService_;
    std::shared_ptr<RoomService> roomService_;

    PacketFunc packet_handlers_[UINT16_MAX];
};
Enter fullscreen mode Exit fullscreen mode

Making Composition Root

Finally, we'll create a Composition Root, which is where we'll create and inject the managers, services, and handlers we'll actually use. In this project, the Server class will fulfill this role.
When the Server class's constructor is executed, it creates managers, and then creates services with those managers as constructor arguments. Finally, it creates handlers that inject those services.

//main.cpp
int main() {

    try {
        boost::asio::io_context io_context;

        std::cout << "Server starting on port "<< PORT << "..." << std::endl;
        Server s(io_context, PORT);   //Managers, services, and handlers are created and injected here.

        io_context.run(); // Start the server event loop
    }
    catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

//Server.cpp
Server::Server(boost::asio::io_context& io_context, short port)
    : acceptor_(io_context, tcp::endpoint(tcp::v4(), port))
{
    sessionMgr_ = std::make_shared<SessionManager>();
    roomMgr_ = std::make_shared<RoomManager>();
    authMgr_ = std::make_shared<AuthManager>();
    playerMgr_ = std::make_shared<PlayerManager>();
    authService_ = std::make_shared<AuthService>(authMgr_, playerMgr_, sessionMgr_);
    roomService_ = std::make_shared<RoomService>(roomMgr_);
    packetHandler_ = std::make_shared<ServerPacketHandler>(authService_, roomService_);

    packetHandler_->Init();

    std::cout << "Server initialized on port " << port << std::endl;

    DoAccept();
}
Enter fullscreen mode Exit fullscreen mode

When the main function is executed, the server constructor takes action, and the server constructor creates all managers, services, and handlers.

In Conclusion

This time, I tried refactoring the code to follow the DIP. It doesn't seem to fully adhere to the SOLID pattern yet, but I've prioritized addressing the SRP and DIP issues, which would require significant time and effort to fix later.
Next, I'll tackle the actual in-game implementation.

Top comments (0)