понедельник, 30 августа 2010 г.

[prog] Случай, где была бы уместна логика на исключениях

У С++ников есть две страшные страшилки – использование goto и реализация логики на исключениях.

Ну, про goto все понятно – goto considered harmful и все такое ;) И действительно, в C++, в отличии от С (в котором goto часто используется для переход в блок очистки ресурсов в конце функции) есть достаточно средств без проблем обойтись без goto. Более того, в присутствии исключений именно такими средствами, а не goto и нужно пользоваться. Поэтому goto, действительно, harmful и о goto речи больше не будет.

А вот вторая страшилка действительно пострашнее и посерьезнее. Это использование исключений для логики программы.

Под катом много букв для тех, кто заинтересовался.

Если объяснять на пальцах, то логика на исключениях приводит вот к такому коду:

void process_data( const some_data_t & data )
  {
    try
      {
        do_some_processing( data );
      }
    catch( const everything_allright_t & )
      {
        finish_successful_processing();
      }
    catch( const something_bad_t & x )
      {
        finish_failed_processing( x );
      }
    catch( const shit_happened_t & x )
      {
        try_get_face_back_from_shit( x );
      }
  }

там, где можно было бы обойтись чем-то вроде:

enum data_processing_result_t
  {
    SUCCESSFUL,
    FAILED,
    SHIT_HAPPENED
  };

void process_data( const some_data_t & data )
  {
    const data_processing_result_t result = do_some_processing( data );
    switch( result )
      {
        case SUCCESSFUL:
          finish_successful_processing(); break;
        case FAILED:
          finish_failed_processing(); break;
        case SHIT_HAPPENED:
          try_get_face_back_from_shit(); break;
        default:
          throw_some_exception();
      }
  }

Противники использования логики на исключениях говорят, что подобные приемы усложняют понимание и сопровождение программ, код становится менее очевидным. Да и вообще нехрен, не для того исключения создавались.

Они правы. Исключения – сильнодействующая штука. А посему пользоваться ей нужно с большими предосторожностями. Временами это граната, которую нельзя давать в руки обезъя начинающим программистам (хотя, с другой стороны, а как иначе убедиться, что штука это опасная?).

Однако, из любого правила есть исключения и исключения здесь не исключение ;) Для начала расскажу про ситуацию, с которой я столкнулся в последние несколько дней.

Есть процедура трансформации пакета данных. В результате ее работы либо пакет добавляется в список хороших пакетов, либо же в список плохих пакетов нужно добавить описание проблемы данного пакетом.

Логика этой процедуры проста – последовательность независимых друг от друга шагов, после каждого шага становится известно, нужно ли отвергать этот пакет или нет. Если решать эту задачу в лоб, то получится что-то подобное:

void
process_package(
  const source_package_t & src,
  good_package_list_t & good_receiver,
  bad_package_list_t & bad_receiver )
  {
    package_ptr_t pkg = make_package_from_source( src );

    failure_description_auto_ptr_t result;

    result = do_first_transformation( *pkg );
    if( result.get() )
      {
        // Пакет признан негодным.
        bad_receiver.add_failed_package( src, *result );
        return;
      }

    result = do_second_transformation( *pkg );
    if( result.get() )
      {
        bad_receiver.add_failed_package( src, *result );
        return;
      }
    ... // Еще несколько точно таких же действий.

    // Поскольку добрались сюда, значит пакет годный для дальнейшей
    // обработки.
    good_receiver.add( pkg );
  }

Что мне не нравится в этом, так это забор из однотипных if-ов. Для написания такой функции с ходу напрашивается замечательный прием copy-and-paste со всеми последующими прелестями. Чего, понятное дело, хотелось бы избежать.

В моем случае было просто – все функции трансформации пакетов имели одинаковый формат, поэтому я обошелся вектором указателей на них и циклом:

void
process_package(
  const source_package_t & src,
  good_package_list_t & good_receiver,
  bad_package_list_t & bad_receiver )
  {
    package_ptr_t pkg = make_package_from_source( src );

    typedef failure_description_auto_ptr_t (*pfn_transformator_t)(package_t &);
    pfn_transformator_t transformators[] =
      {
        &do_first_transformation,
        &do_second_transformation,
        &do_third_transformation,
        ...
      };
    for( std::size_t i = 0,
        i_max = sizeof(transformators)/sizeof(transformators[0]);
        i != i_max;
        ++i )
      {
        failure_description_auto_ptr_t result = (*transformators[i])( *pkg );
        if( result.get() )
          {
            // Пакет признан негодным.
            bad_receiver.add_failed_package( src, *result );
            return;
          }
      }

    // Поскольку добрались сюда, значит пакет годный для дальнейшей
    // обработки.
    good_receiver.add( pkg );
  }

Но мне повезло с тем, что все трансформаторы получали одинаковый набор параметров. Если бы это было не так, пришлось бы придумывать что-то другое.

А вот если бы использовалась логика на исключениях, то и код был бы компактнее, и у трансформаторов могли бы быть совершенно разные прототипы:

void
process_package(
  const source_package_t & src,
  good_package_list_t & good_receiver,
  bad_package_list_t & bad_receiver )
  {
    try
      {
        package_ptr_t pkg = make_package_from_source( src );

        do_first_transformation( *pkg );
        do_second_transformation( *pkg );
        do_third_transformation( *pkg );
        ...

        // Поскольку добрались сюда, значит пакет годный для дальнейшей
        // обработки.
        good_receiver.add( pkg );
      }
    catch( const transformation_failed_ex_t & x )
      {
        // Пакет признан негодным.
        bad_receiver.add_failed_package( src, x.failure_description() );
      }
  }

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

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

В языке с паттерн-матчингом здесь не было бы проблемы. Скажем, на Scala (насколько я помню ее синтаксис) это можно было бы написать так:

abstract class PkgAnalysingResult
case class SuccessfulResult extends PkgAnalysingResult
case class IllegalFieldsResult(field: String, length: Int) extends PkgAnalysingResult
case class NonsupportedEncodingResult(encodingName: String) extends PkgAnalysingResult
case class UnknownDestinationResult(destName: Destination) extends PkgAnalysingResult
case class DeniedDestinationResult(
    destName: Destination,
    reason: String,
    errorCode: Int) extends PkgAnalysingResult

def analyzePkg(pkg: Package) { ... }

def processPkg(pkg: Package) {
  analyzePkg(pkg) match {
    case SuccessfulResult => ...
    case IllegalFieldsResult => ...
    case NonsupportedEncodingResult => ...
    case UnknownDestinationResult => ...
    case DeniedDestinationResult => ...
  }
  ...
}

Но в C++ так не напишешь (если только не задействовать какой-то из многочисленных VARIANT-ов). Зато в C++ можно задействовать исключения:

class analyzing_result_t : public std::exception { ... };
class illegal_fields_result_t : public analyzing_result_t { ... };
class nonsupported_encoding_result_t : public analyzing_result_t { ... };
class unknown_destination_result_t : public analyzing_result_t { ... };
class denied_destination_result_t : public analyzing_result_t { ... };

// Порождает исключение в случае каких-то проблем.
void analyze_pkg( const package_t & pkg ) { ... }

void process_pkg( const package_t & pkg )
  {
    try
      {
        analyze_pkg( pkg );
        ... // Дальнейшая нормальная обработка
      }
    catch( const illegal_fields_result_t & x ) { ... }
    catch( const nonsupported_encoding_result_t & x ) { ... }
    catch( const unknown_destination_result_t & x ) { ... }
    catch( const denied_destination_result_t & x ) { ... }
    catch( const analyzing_result_t & x )
      {
        ... // Какие-то действия на случай, если забыли сюда
            // добавить обработку еще одного варианта.
      }
  }

Конечно, с паттерн-матчингом не сравнить, но хоть какой-то хлеб.

Так что, логика на исключениях – это не есть хорошо и правильно. Но в C++ ее временами приходится использовать. Почти так же, как в чистом C goto.

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