четверг, 3 сентября 2009 г.

[comp.prog] О простоте программирования в транзакционном стиле

В заметке, посвященной исключениям, остался без ответа комментарий jazzer-а:

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

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

Итак, есть некий класс, который захватывает ресурс и производит некоторые операции с помощью этого ресурса:

 class Sample {
  public :
    void init( const std::string &  resource );
    void perform_action( const std::string & action_data );
  ...
};
 

Используется это все достаточно просто:

 Sample action_performer;
...
while( true ) {
  const std::string resource_name = next_resource();
  action_performer.init( resource_name );

  const std::string data = next_data();
  action_performer.perform_action( data );
}
...
 

Дальше выяснилось, что в процессе работы resource_name очень редко меняются. И можно сэкономить время за счет операций освобождения/захвата ресурсов в методе init. Поэтому, было решено в классе Sample реализовать сохранение имени ресурса, чтобы выполнять переинициализацию только при изменении этого имени. После чего код метода Sample::init() быстренько принял вид:

 void Sample::init(
  const std::string & resource_name )
  {
    if( m_current_resource_name != resource_name )
      {
        m_current_resource_name = resource_name;
        free_resource( m_resource );
        m_resource = alloc_resource();
        if( !m_resource )
          throw ...;
      }
  }
 

Внимательный читатель, наверное, уже понял, в чем здесь проблема. Этот код будет нормально работать только в случае, если после возникновения исключения экземпляр Sample будет разрушен. А иначе может произойти следующее:

  • где-то в программе будет создан объект Sample;
  • где-то для него будет вызван метод init в который будет передано, скажем, имя "A". На данный момент это имя не правильное и будет выброшено исключение;
  • исключение будет где-то поймано, будут предприняты какие-то действия по исправлению ситуации (например, ресурс с именем "A" будет создан);
  • где-то опять будет вызван метод init для того же самого объекта Sample с тем же самым именем "A";
  • но этот метод ничего не сделает, т.к. m_current_resource_name будет равно resource_name!

Казалось бы, это детская ошибка. Но ведь программирование как раз и переполнено ошибками, в том числе и детскими. И в этом одна из самых больших сложностей нашего занятия.

Кстати, исправление этой ошибки не так тривиально, как могло бы показаться. Самый первый вариант, который приходит в голову:

 void Sample::init(
  const std::string & resource_name )
  {
    if( m_current_resource_name != resource_name )
      {
        free_resource( m_resource );
        m_resource = alloc_resource();
        if( !m_resource )
          throw ...;
        m_current_resource_name = resource_name;
      }
  }
 

так же ошибочен. И чтобы написать более-менее корректный вариант init, нужно что-то вроде:

 void Sample::init(
  const std::string & resource_name )
  {
    if( m_current_resource_name != resource_name )
      {
        m_current_resource_name.clear();
        free_resource( m_resource );
        m_resource = alloc_resource();
        if( !m_resource )
          throw ...;
        m_current_resource_name = resource_name;
      }
  }
 

Кстати говоря, в этой версии так же есть проблемы, но их обнаружение я оставляю в качестве упраждения читателям ;)

Так что повторюсь еще раз: программирование с исключениями и расчетом на восстановление после исключения требует повышенного внимания от программиста. Даже к мелочам. Или лучше сказать -- особенно к мелочам.

PS. Да, класс Sample мог бы быть спроектирован по другому. Многое из того, с чем мы сталкиваемся могло бы быть сделано гораздо лучше. Но приходится иметь дело с тем, что есть.

Отправить комментарий