пятница, 27 ноября 2009 г.

[comp.prog.boasting] Ход одного рефакторинга

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

Итак, нужно было написать разбор конфига, один из тегов которого мог иметь следующие разрешенные варианты:

|| Вариант первый: используется только HTTP-аутентификация.
{auth_params "http_basic"
  {user_name "name"}
  {user_password "password"}
}

|| Вариант второй: HTTP-аутентификация + SSL с проверкой сервера.
{auth_params "http_ssl_server"
  {user_name "name"}
  {user_password "password"}
  {ca_file "path/to/ca/file"}
}

|| Вариант третий: SSL с проверкой и клиента, и сервера.
{auth_params "ssl_client_server"
  {ca_file "path/to/ca/file"}
  {client_cert_and_key_file "path/to/pem/file"}
  {client_private_key_password "password"}
}

Нужно было написать функцию, которая после завершения парсинга тега {auth_params} проверяла бы корректность сочетания вложенных тегов.

Первый вариант получался вот таким:

/* Вариант I */
void
tag_auth_cfg_t::on_finish(
  const cls_3::parser_context_info_t & context )
  {
    base_type_t::on_finish( context );

    const std::string & auth_type_name = base_type_t::query_value();
    auth_cfg_t::auth_type_t auth_type = get_auth_type( auth_type_name );

    // В режимах http_basic и http_and_ssl_server имя пользователя
    // и пароль обязательно должны быть определены. А в режиме
    // ssl_client_server -- не должны.
    if( autg_cfg_t::auth_ssl_client_server_check != auth_type )
      {
        if( !m_user_name.is_defined() )
          throw cls_3::semantic_ex_t(
              "mandatory tag should be defined: " +
              m_user_name.query_name() );
        if( !m_user_password.is_defined() )
          throw cls_3::semantic_ex_t(
              "mandatory tag should be defined: " +
              m_user_password.query_name() );
      }
    else
      {
        if( m_user_name.is_defined() )
          throw cls_3::semantic_ex_t(
              "tag should not be used: " +
              m_user_name.query_name() );
        if( m_user_password.is_defined() )
          throw cls_3::semantic_ex_t(
              "tag should not be used: " +
              m_user_password.query_name() );
      }
    ... // Дальнейшие проверки.
  }

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

/* Вариант II */
void
mandatory_tag_not_defined(
  const ... & tag, const std::string & auth_mode_name )
  { ... /* Порождение исключения с нужным сообщением. */ }

void
disabled_tag_not_defined(
  const ... & tag, const std::string & auth_mode_name )
  { ... /* Порождение исключения с нужным сообщением. */ }

void
tag_auth_cfg_t::on_finish(
  const cls_3::parser_context_info_t & context )
  {
    ... /* Начало точно такое же. */
    if( autg_cfg_t::auth_ssl_client_server_check != auth_type )
      {
        if( !m_user_name.is_defined() )
          mandatory_tag_not_defined( m_user_name, auth_type_name );
        if( !m_user_password.is_defined() )
          mandatory_tag_not_defined( m_user_password, auth_type_name );
      }
    else
      {
        if( m_user_name.is_defined() )
          disabled_tag_defined( m_user_name, auth_type_name );
        if( m_user_password.is_defined() )
          disabled_tag_defined( m_user_password, auth_type_name );
      }
    ... // Дальнейшие проверки.
  }

Уже лучше, но писать вызовы is_defined() и disabled_tag_defined() мне быстро надоело.  Во-первых, проверку наличия значения у тега можно переместить во вспомогательные функции. Во-вторых, зачем постоянно передавать имя режима аутентификации, если это можно сделать один раз? Стало получаться что-то вроде:

/* Вариант III */
class presence_checker_t
  {
  private :
    const std::string & m_auth_mode_name;
  public :
    presence_checker_t( const std::string & auth_mode_name )
      : m_auth_mode_name( auth_mode_name )
      {}
    void
    check_mandatory_tag( const ... & tag ) const
      { ... /* Проверка значения и порождение исключения. */ }
    void
    check_disabled_tag( const ... & tag ) const
      { ... /* Проверка значения и порождение исключения. */ }
  };

void
tag_auth_cfg_t::on_finish( const cls_3::parser_context_info_t & context )
  {
    base_type_t::on_finish( context );

    const std::string & auth_type_name = base_type_t::query_value();
    auth_cfg_t::auth_type_t auth_type = get_auth_type( auth_type_name );
    presence_checker_t checker( auth_type_name );

    // В режимах http_basic и http_and_ssl_server имя пользователя
    // и пароль обязательно должны быть определены. А в режиме
    // ssl_client_server -- не должны.
    if( autg_cfg_t::auth_ssl_client_server_check != auth_type )
      {
        checker.check_mandatory_tag( m_user_name );
        checker.check_mandatory_tag( m_user_password );
      }
    else
      {
        checker.check_disabled_tag( m_user_name );
        checker.check_disabled_tag( m_user_password );
      }

    // В режиме http_basic должны быть определены только два тега.
    if( auth_cfg_t::auth_http_only == auth_type )
      {
        checker.check_disabled_tag( m_ca_file );
        checker.check_disabled_tag( m_client_cert_and_key_file );
        checker.check_disabled_tag( m_client_private_key_password );
      }
    else
      {
        // В оставшихся режимах тег {ca_file} всегда должен быть определен.
        checker.check_mandatory_tag( m_ca_file );

        // Оставшиеся два тега должны быть определены только в
        // одном случае.
        if( auth_cfg_t::auth_ssl_client_server_check == auth_type )
          {
            checker.check_mandatory_tag( m_client_cert_and_key_file );
            checker.check_mandatory_tag( m_client_private_key_password );
          }
        else
          {
            checker.check_disabled_tag( m_client_cert_and_key_file );
            checker.check_disabled_tag( m_client_private_key_password );
          }
      }
  }

Этот вариант уже гораздо лучше. Хотя бы потому, что он позволяет обозреть весь метод on_finish – код понятен и легко проверяется.

Но все равно я вошел во вкус – мне казалось, что можно сделать еще короче:

/* Вариант IV */
class presence_checker_t
  {
  private :
    const std::string & m_auth_mode_name;
  public :
    presence_checker_t( const std::string & auth_mode_name )
      : m_auth_mode_name( auth_mode_name )
      {}
    void
    check( const ... & tag, bool is_mandatory )
      {
        // Если is_mandatory == true, то требует, чтобы тег
        // имел значение. Если is_mandatory == false, то тег
        // не должен иметь значения.
        ...
      }
  };

void
tag_auth_cfg_t::on_finish( const cls_3::parser_context_info_t & context )
  {
    base_type_t::on_finish( context );

    const std::string & auth_type_name = base_type_t::query_value();
    auth_cfg_t::auth_type_t auth_type = get_auth_type( auth_type_name );
    presence_checker_t checker( auth_type_name );

    // В режимах http_basic и http_and_ssl_server имя пользователя
    // и пароль обязательно должны быть определены. А в режиме
    // ssl_client_server -- не должны.
    const bool is_user_name_and_password_mandatory =
      (auth_cfg_t::auth_ssl_client_server_check != auth_type);
    checker.check( m_user_name, is_user_name_and_password_mandatory );
    checker.check( m_user_password, is_user_name_and_password_mandatory );

    // При работе по SSL тег {ca_file} обязательно должен быть задан.
    const bool is_ca_mandatory = (auth_cfg_t::auth_http_only != auth_type);
    // Параметры клиента должны быть определены только в режиме
    // ssl_client_server.
    const bool is_client_cert_mandatory =
      auth_cfg_t::auth_ssl_client_server_check == auth_type;

    checker.check( m_ca_file, is_ca_mandatory );
    checker.check( m_client_cert_and_key_file, is_client_cert_mandatory );
    checker.check( m_client_private_key_password, is_client_cert_mandatory );
  }

Вот этот вариант у меня и остался. Поскольку он намного компактнее всех предыдущих. Хотя и воспринимается чуть сложнее, чем предыдущий. Что не есть хорошо. Но дальше думать уже лень ;) Хотя я не сомневаюсь, что можно сделать еще проще.

PS. Кстати о количестве строк, производимых программистом. Вариант IV оказался самым компактным (всего на несколько строк короче варианта III и значительно короче варианта II). Вариант I я вообще не дописал, т.к. его размер превосходил все допустимые размеры. Однако, времени на написание окончательного варианта потребовалось раза в три больше, чем на работу над двумя первыми вариантами (грубо говоря: вариант II у меня получился через 20 минут после начала работы, а вариант IV – через час после начала). Что подтверждает мое старое наблюдение – чем спокойнее и неторопливее ход работы, тем меньше по объему получается код. Как следствие – чем спокойнее программист работает, тем меньше строк он пишет. Так что, смею предположить, у хороших программистов производительность в килостроках вообще никакая ;)

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

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

Про код ничего не скажу - много букаф. Но всё равно молодец :-)

Зато расскажу, как на прошлой работе с меня потребовали отчёт о производительности в строках кода.

Я недолго думая посчитал всё - и HTML-страницы, шаблоны, perl-код, Delhi-код, SQL-запросы и даже автоматически сгенерированный моим кодом код.

Предоставил полученные данные, после чего до конца моей работы в этом заведении ко мне с такими просьбами не обращались :-)

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

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

Ты, похоже, тогда поступил именно таким образом :)