пятница, 8 апреля 2016 г.

Правильный интерфейс...

Есть много всяких принципов и практик. Часто в интерфейсе класса можно учуять множество всяких запахов. Абсолютно правильно наверное не бывает, это всегда компромисс.

Наверное последнее время я достаточно мало пишу кода. Я чуствую, что мне не хватает набитой руки. И вокруг меня как-то мало крутого и правильного кода. И мне кажется, что посади меня сейчас плотно программировать - я бы делал это медленно. Слишком много бы думал. Хотя руководящая нагрузка снялась, кто ж знает что было бы?

Но сейчас про код...

Есть неторопливая фоновая задача, и я ее думаю. Задача заключается примерно в следующем: Есть модуль, отвечающий за взаимодействие между узлами, он сообщения через TLS гоняет. Но есть определенные неудобства в том плане, что пока соединение не установится - мы не знаем куда слать сообщение. Их нужно хранить в ожидании подключений. Сейчас там все достаточно криво и очередь существует толко у уже подключенных соединений.

Нужно сделать буфер, который будет независим от соединений. Соединения сами по себе, а буффер единый и живет своей жизнью. И этот буфер может хранить сообщения для отправки и дефрагментировать сообщения, которые валятся к нам.

Это было краткое вступление.

Сущесвующие классы жестко провязаны через коллбеки, и  это не очень удобно. В случае с сетевым буффером вполне можно сделать чистый класс, который почти ни от кого не зависит и все действия осуществляются через его интерфейс. То есть добавляем сообщения на отправку, извлекаем получившиеся фрагменты и отправляем. Аналогично поступаем на вход. Загружаем туда фрагменты - а оттуда достаем собранные сообщения и пробрасываем в систему. Поскольку вся система построена на асинхронщине - такой интерфейс вполне удобен. Вопрос в деталях.

Отправляющая часть.

Сам по себе буфер достаточно умный - умеет следить за своим размером, чистит память если надо. Никто не обещал, что сообщения будут жить вечно. Поэтому в плане добавления сообщений проблем никаких нет.
buffer.addOutgoingMessage(m);
Вопрос встает в плане извлечения. Поскольку этим занимается другой поток, он точно не знает - есть сообщения или нет. Но поскольку речь идет о потоках - следующий вариант серьезно не рассматривается. Считаю что каждый метод должен быть самодостаточным. Но мы к нему еще вернемся.
if (buffer.hasOutgoingFragment()) {
    const auto f = buffer.getOutgoingFragment();
}
Если посмотреть на tbb::concurrent_queue, нам предлагается такой вариант:
bool try_pop( T& destination );
Такой подход помоему нарушает сразу два принципа.
Fragment f;
if (buffer.getOutgoingFragment(f)) {
    ...
}
Во первых он нарушает RAII. Это приводит к тому, что сперва срабатывает конструктор по умолчанию, а потом произойдет копирование объекта. Я не сторонник преждевременной оптимизации, но это не значит, что я должен конструировать каждый объект дважды.

Во вторых этот код нарушает принцип Command/Query Separation. Один метод пытается что-то нам вернуть, и одновременно пытается донести до нас - смог он это сделать или не смог. Меня немного коробит от осознания того факта, что f может остаться неинициализированным.

Есть другой способ, не намного лучший, вернуть tuple.
const auto fpair = buffer.getOutgoingFragment();
if (get<0>(fpair)) {
    // Можно работать с get<1>(pair)
}
Или даже так:
const auto fpair = buffer.getOutgoingFragment();
if (get(fpair)) {
    // Можно работать с get(fpair)
}
Или можно даже имея переменные забиндить их через tie, но в этом случае мы возвращаемся к проблеме RAII.
bool have;
Fragment f;
tie(have, f) = buffer.getOutgoingFragment();
if (have) {
    // Можно работать с f
}
И как-то это все многословно. Хочется чище... Да какого черта, вот чистый и безопасный вариант:
try {
    const auto f = getOutgoingFragment();
    ...
} catch (const Buffer::Empty &) {
    ...
}
Это похоже на логику на исключениях. Да это она.

Кто-то скажет про скорость, что исключения дескать медленные. Тут есть один ньюанс. Когда сообщений много и идет нагрузка - скорость крайне важна. Но когда сообщений нет, нам пофиг производительность.

Я провел небольшое сравнение, и в случае возвращения значений не заметил разницы между вариантом с исключениями и всеми остальными. Кажется что вариант с референсом немного проигрывает. Но это все на уровне погрешности измерений.

Другое дело, когда сообщений нет и последний вариант бросает исключения. В этом случае он работает в 1000 раз дольше, чем остальные. Но не все ли равно?

Это не все равно, в плане приемной части. Потому что фрагментов может быть на порядки больше, чем собранных сообщений. В этом случае можно совместить два подхода. Метод проверки наличия и метод извлечения с исключением. Это будет безопасно в плане многопоточности.

В принципе для единообразия можно и отправку тоже снабдить методом проверки. Он может быть полезен...
class Buffer {
public:
    void addOutgoingMessage(const Message &m);
    bool hasOutgoingFragment() const;
    Fragment getOutgoingFragment();

    void addIncomingFragment(const Fragment &f);
    bool hasIncomingMessage() const;
    Message getIncomingMessage();
};
Здесь нужно понимать, что один сценарий достаточно оптимален, в то время как другой крайне неоптимален. По какому сценарию используется класс? Насколько оптимально его использование?

Логика на исключениях... наверное я буду гореть за это в аду...