Я использую протокол, который в основном является протоколом запроса и ответа по TCP, как и другие линейные протоколы (SMTP, HTTP и т.д.).
Протокол имеет около 130 различных методов запросов (например, логин, пользовательский ввод, обновление пользователя, получение журнала, информация о файле, информация о файлах,...). Все эти методы не так хорошо сопоставляются с широкими методами, которые используются в HTTP (GET, POST, PUT,...). Такие широкие методы приведут к некоторым непоследовательным поворотам фактического значения.
Но методы протокола могут быть сгруппированы по типу (например, управление пользователями, управление файлами, управление сеансом,...).
Текущая реализация на стороне сервера использует class Worker
с методами ReadRequest()
(читает запрос, состоящий из списка параметров метода плюс), HandleRequest()
(см. ниже) и WriteResponse()
(записывает код ответа и фактические данные ответа).
HandleRequest()
вызовет функцию для фактического метода запроса - используя хэш-карту имени метода для указателя функции-члена к фактическому обработчику.
Фактический обработчик - это простая функция-член, по одному для каждого метода протокола: каждый проверяет свои входные параметры, делает все, что ему нужно, и устанавливает код ответа (успех да/нет) и данные ответа.
Пример кода:
class Worker {
typedef bool (Worker::*CommandHandler)();
typedef std::map<UTF8String,CommandHandler> CommandHandlerMap;
// handlers will be initialized once
// e.g. m_CommandHandlers["login"] = &Worker::Handle_LOGIN;
static CommandHandlerMap m_CommandHandlers;
bool HandleRequest() {
CommandHandlerMap::const_iterator ihandler;
if( (ihandler=m_CommandHandlers.find(m_CurRequest.instruction)) != m_CommandHandler.end() ) {
// call actual handler
return (this->*(ihandler->second))();
}
// error case:
m_CurResponse.success = false;
m_CurResponse.info = "unknown or invalid instruction";
return true;
}
//...
bool Handle_LOGIN() {
const UTF8String username = m_CurRequest.parameters["username"];
const UTF8String password = m_CurRequest.parameters["password"];
// ....
if( success ) {
// initialize some state...
m_Session.Init(...);
m_LogHandle.Init(...);
m_AuthHandle.Init(...);
// set response data
m_CurResponse.success = true;
m_CurResponse.Write( "last_login", ... );
m_CurResponse.Write( "whatever", ... );
} else {
m_CurResponse.Write( "error", "failed, because ..." );
}
return true;
}
};
Итак. Проблема в том, что мой рабочий класс теперь имеет около 130 "методов обработчика команд". И каждый из них нуждается в доступе к:
- параметры запроса
- объект ответа (для записи данных ответа)
- различные другие локальные объекты сеанса (например, дескриптор базы данных, дескриптор запросов авторизации/разрешения, ведение журнала, обращение к различным подсистемам сервера и т.д.).
Что такое хорошая стратегия для лучшего структурирования этих методов обработчика команд?
Одна из идей заключалась в том, чтобы иметь один класс для каждого обработчика команд и инициализировать его ссылками на запросы, объекты ответа и т.д., но накладные расходы не приемлемы (на самом деле это добавит косвенность для любого отдельного доступа к все, что требуется обработчику: запрос, ответ, объекты сеанса,...). Это может быть приемлемым, если это даст реальное преимущество. Однако это не кажется разумным:
class HandlerBase {
protected:
Request &request;
Response &response;
Session &session;
DBHandle &db;
FooHandle &foo;
// ...
public:
HandlerBase( Request &req, Response &rsp, Session &s, ... )
: request(req), response(rsp), session(s), ...
{}
//...
virtual bool Handle() = 0;
};
class LoginHandler : public HandlerBase {
public:
LoginHandler( Request &req, Response &rsp, Session &s, ... )
: HandlerBase(req,rsp,s,..)
{}
//...
virtual bool Handle() {
// actual code for handling "login" request ...
}
};
Хорошо, HandlerBase может просто взять ссылку (или указатель) самому рабочему объекту (вместо ссылок на запрос, ответ и т.д.). Но это также добавит еще одну косвенную (this- > worker- > session вместо this- > session). Это направление было бы в порядке, если бы оно приобрело какое-то преимущество в конце концов.
Некоторая информация об общей архитектуре
Рабочий объект представляет собой один рабочий поток для фактического TCP-соединения с некоторым клиентом. Каждый поток (так, каждый рабочий) нуждается в собственном дескрипторе базы данных, дескрипторе авторизации и т.д. Эти "дескрипторы" - это потоковые объекты, которые разрешают доступ к некоторой подсистеме сервера.
Вся эта архитектура основана на какой-то инъекции зависимостей: например, для создания объекта сеанса необходимо предоставить "дескриптор базы данных" для конструктора сеанса. Затем объект сеанса использует этот дескриптор базы данных для доступа к базе данных. Он никогда не будет вызывать глобальный код или использовать одиночные игры. Таким образом, каждый поток может работать без изменений сам по себе.
Но стоимость заключается в том, что вместо того, чтобы просто вызывать одиночные объекты - рабочий и его обработчики команд должны обращаться к любым данным или другому коду системы через такие специфичные для потока дескрипторы. Эти дескрипторы определяют контекст выполнения.
Резюме и разъяснение: мой фактический вопрос
Я ищу элегантную альтернативу текущему ( "рабочий объект с огромным списком методов обработчика" ): он должен быть поддержанным, иметь низкую нагрузку и не должен требовать слишком много написания клея. Кроме того, он ДОЛЖЕН все еще разрешать каждому отдельному методу управлять очень разными аспектами его выполнения (это означает: если метод "super flurry foo" хочет сбой, когда включена полная луна, тогда для этой реализации это должно быть возможно), Это также означает, что я не хочу, чтобы какой-либо объект абстракции (создание/чтение/обновление/удаление XFoo-типа) на этом архитектурном уровне моего кода (он существует на разных уровнях моего кода). Этот архитектурный уровень - чистый протокол, и ничего больше.
В конце концов, это, безусловно, будет компромиссом, но меня интересуют любые идеи!
Бонус AAA: решение со взаимозаменяемыми реализациями протокола (а не только текущее class Worker
, которое отвечает за разбор запросов и написание ответов). Возможно, может быть взаимозаменяемый class ProtocolSyntax
, который обрабатывает данные синтаксиса протокола, но все еще использует наши новые блестящие структурированные обработчики команд.