вторник, 17 февраля 2009 г.

В поиске решения "Можно ли заставить компилятор проверять изменение принципов работы компонентов?"

Ранее я озадачился тем, можно ли как-то оформить контракты для асинхронных сообщений так, чтобы компилятор отслеживал их нарушения еще во время компиляции. Это еще не решение, но, возможно, один из намеков на то, как его можно получить.

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

Итак, вот суть задачи в переложении на синхронные вызовы. Есть некий интерфейс s_iface_t, с методом query_state. При обращении к этому методу у объекта типа mailslot_t должен быть вызван метод send_state_notification. Причем не у абы какого объекта mailslot_t, а только у того, который не меняется в течение всего времени жизни объекта, реализующего интерфейс s_iface_t.

Т.е. мы должны иметь что-то вроде (примеры написаны на C++ без учета проблем сборки мусора):

// Интерфейс почтового ящика.
class mailslot_t {
  public :
    virtual void send_state_notification() = 0;
};

class s_iface_t {
  public :
    virtual КАКОЙ-ТО-ТИП query_state() = 0;
};

class s_component_t : public s_iface_t {
  public :
    s_component_t( mailslot_t & m );

    virtual КАКОЙ-ТО-ТИП query_state();
};

И какой же тип должен возвращать s_iface_t::query_state()? Например, такой:

// Сигнал о том, что был задействован почтовый ящик,
// который не может измениться.
class nonchangeable_mailslot_used_signal_t {
  // Создавать экземпляр этого типа нужно очень хитро.
  // Поэтому его создание делегируется специальному
  // вспомогательному классу.
  friend class nonchangeable_mailslot_holder_t;

  // А конструктор поэтому закрыт.
  nonchangeable_mailslot_used_signal_t( mailslot_t & m ) {
    m.send_state_notification();
  }

public :
  // Зато деструктор открыт.
  ~nonchangeable_mailslot_used_signal_t() {}
};

Т.е. конструирование объекта nonchangeable_mailslot_used_signal_t невозможно без вызова send_state_notification. Т.о. мы гарантируем, что раз s_iface_t::query_state() вызывается, значит следует вызов send_state_notification у какого-то mailslot-а. Осталось только обеспечить, чтобы send_state_notification вызывался у того mailslot-а, который был нам передан в конструкторе. Это уже достигается реализацией класса nonchangeable_mailslot_holder_t:

class nonchangeable_mailslot_holder_t {
  // Поскольку это ссылка, то после инициализации она
  // измениться не может.
  mailslot_t & mailslot_;

protected :
  // Конструктор защищен, значит его может вызвать только
  // производный от nonchangeable_mailslot_holder_t класс.
  // А это значит, что производный класс должен получать
  // в конструкторе ссылку на mailslot.
  nonchangeable_mailslot_holder_t( mailslot_t & m )
    : mailslot_(m)
    {}

  // Этот метод так же защищен, значит его может вызвать
  // только производный класс.
  nonchangeable_mailslot_used_signal_t
  send_state_notify() {
    return nonchangeable_mailslot_used_signal_t( mailslot_ );
  }
};

Теперь все, что остается сделать - это унаследовать s_component_t от nonchangeable_mailslot_holder_t:

class s_component_t
  : public s_iface_t
  , protected nonchangeable_mailslot_holder_t {
public :
  s_component_t( mailslot_t & m )
    : nonchangeable_mailslot_holder_t(m)
    {}

  virtual nonchangeable_mailslot_used_signal_t
  query_state() {
    return send_state_notify();
  }
};

Решение далеко не идеальное и оставляет пользователю возможности для обхода контракта (например, можно сделать вспомогательный класс, унаследованный от nonchangeable_mailslot_holder_t и использовать его временные объекты в новых версиях s_component_t::query_state). Но все эти фокусы нужно делать осознано, а контракты в данной задаче были предназначены как раз для того, чтобы нельзя было их нарушить случайно (по незнанию или недосмотру).

2 комментария:

Dmitry Vyukov комментирует...

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

class s_iface_t
{
public:
void query_state()
{
// скелет алгоритма, включая контракт, зашиты в базовый класс
...
query_state_impl();
...
send_state_notify();
...
}

private:
// производный класс может лишь определить какие-то детали
// нарушить контракт он не может
virtual void query_state_impl() = 0;
};

class s_component_t : public s_iface_t
{
private:
virtual void query_state_impl()
{
...
}
};

eao197 комментирует...

Здесь фишка не столько в том, что можно позволить производным от s_iface_t классам. Сколько в том, как сам s_iface_t и его единственная реализация s_component_t могут измениться со временем.

Смотри какая ситуация: есть два компонента M и S. Компонент M видит S через интерфейс s_iface_t. Для интерфейса s_iface_t есть только одна реализация -- s_component_t (другие могут быть только в отладочных целях). Так вот, нужно на уровне интерфейса выразить зависимость M от конкретной логики работы s_component_t.

Если M видит s_iface_t:query_state() как void-метод, то в какой-то очередной версии S какой-то другой разработчик может изменить логику поведения query_state другим образом. Грубо говоря, выбросит виртуальный метод query_state_impl() и переделает реализацию query_state(). В результате поведение компонента S может измениться. А компонент M об этом даже не узнает.

А вот такой вариант, с возвратом из query_state() специального типа, от таких изменений защищает.

Совсем другой вопрос -- нужны ли эти навороты и должен ли M зависеть от логики поведения S. По-хорошему, наверное, не нужны и не должен. Но раз уж я однажды попал в такую ситуацию, то почему бы не придумать способ описания таких зависимостей на будущее? ;)