Создание библиотек многократного использования

Автор: Rob Tougher
Перевод: Андрей Киселев


1. Введение
2. Библиотека должна быть простой в использовании
2.1 Простота
2.2 Непротиворечивость
2.3 Интуитивность
3. Тщательное тестирование
4. Детализация сообщений об ошибках
5. Заключение

1. Введение

Библиотеки расширяют возможности разработчиков программного обеспечения. Они содержат код, который разработчики могут использовать в своих проектах. Программные библиотеки, предназначенные для Linux, обычно доступны как в исходных текстах, так и в виде бинарных файлов.

Хорошо проработанная библиотека:

В этой статье описываются все вышеупомянутые принципы создания библиотек и приводятся примеры на C++

Эта статья для вас?

Прежде чем заняться разработкой новой библиотеки задайте себе пару вопросов:

  • Понадобится ли кому-нибудь (включая и вас самого) эта библиотека?
  • Если да, то не существует ли подобная библиотека?

Нет смысла разрабатывать библиотеку, если она никому не нужна или, если подобная библиотека уже существует.

2. Библиотека должна быть простой в использовании.

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

Основное правило при проработке интерфейса:

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

Придерживайтесь следующих рекомендаций и успех вам будет обеспечен.

2.1 Простота

Чем сложнее библиотека, тем труднее ею пользоваться.

Недавно я столкнулся с библиотекой, написанной на C++, которая содержала один единственный класс. В этом классе было определено 150 методов. 150 методов! Разработчик, по всей видимости, был ветераном языка C и использовал класс C++ как обычный сишный модуль. Класс получился настолько сложным, что разобраться в нем оказалось очень непростой задачей.

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

2.2 Непротиворечивость.

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

Давайте рассотрим пример использования методов, предоставляющих доступ к скрытым (приватным) полям класса:

class point
{
public:
  int get_x() { return m_x; }
  int set_x ( int x ) { m_x = x; }

  int y() { return m_y; }

private:
  int m_x, m_y;
};

Видите несоответствия? Доступ к полю m_x выполняется через метод с именем "get_x()", а к полю m_y -- через "y()". Такая несогласованность вынуждает пользователя всякий раз обращаться к определениям методов перед тем как использовать их.

Еще один пример неудачной реализации интерфейса:

class DataBase
{
public:

  recordset get_recordset ( const std::string sql );
  void RunSQLQuery ( std::string query, std::string connection );

  std::string connectionString() { return m_connection_string; }

  long m_sError;

private:

  std::string m_connection_string;
};

Вы можете самостоятельно найти ошибки? По-крайней мере, я заметил следующие:

Вот пересмотренная версия класса с исправленными ошибками:

class database
{
public:

  recordset get_recordset ( const std::string sql );
  void run_sql_query ( std::string sql );

  std::string connection_string() { return m_connection_string; }
  long error() { return m_error; }

private:

  std::string m_connection_string;
  long m_error;
};

Разрабатывайте интерфейсы настолько непротиворечивыми, насколько это возможно - такие интерфейсы запоминаются намного легче.

2.3 Интуитивность

Подходите к разработке интерфейса с точки зрения пользователя, а не с точки зрения внутреннего устройства.

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

Давайте разберем небольшой пример. Недавно, размышляя о создании криптографической библиотеки, основанной на OpenSSL, я написал такой код:

crypto::message msg ( "My data" );
crypto::key k ( "my key" );

// blowfish algorithm
msg.encrypt ( k, crypto::blowfish );
msg.decrypt ( k, crypto::blowfish ):

// rijndael algorithm
msg.encrypt ( k, crypto::rijndael );
msg.decrypt ( k, crypto::rijndael ):

Это помогло мне взглянуть на будущую библиотеку глазами пользователя. Если я решусь на создание такой библиотеки, то эти идеи будут для меня отправной точкой при ее реализации.

3. Тщательное тестирование

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

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

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

#include "crypto.hpp"

int main ( int argc, int argv[] )
{
  //
  // 1. Зашифровать, расшифровать и проверить
  //
  crypto::message msg ( "Hello there" );
  crypto::key k ( "my key" );

  msg.encrypt ( k, crypto::blowfish );
  msg.decrypt ( k, crypto::blowfish );

  if ( msg.data() != "Hello there" )
    {
      // Ошибка!
    }

  //
  // 2. Зашифровать по одному алгоритму,
  //    расшифровать по другому алгоритму
  //    и проверить.
  //

  // и т.д....
}

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

4. Детализация сообщений об ошибках

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

Библиотеки, написанные на C++, как правило, используют механизм исключений для передачи сообщения об ошибке. Рассмотрим следующий пример:

#include <string>
#include <iostream>


class car
{
public:
  void accelerate() { throw error ( "Could not accelerate" ); }
};


class error
{
public:
  Error ( std::string text ) : m_text ( text ) {}
  std::string text() { return m_text; }
private:
  std::string m_text;
};


int main ( int argc, int argv[] )
{
  car my_car;

  try
    {
      my_car.accelerate();
    }
  catch ( error& e )
    {
      std::cout << e.text() << "\n";
    }
}

Класс car использует ключевое слово throw для возбуждения исключительной ситуации и передачи сообщения об ошибке в вызвавшую функцию. Вызывающая функция "ловит" исключение с помощью конструкции try ... catch и обрабатывает его.

5. Заключение

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

Rob Tougher

Роб -- пишущий на C++ программист из Нью-Йорка.
Copyright (C) 2002, Rob Tougher.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 81 of Linux Gazette, August 2002

Команда переводчиков:
Владимир Меренков, Александр Михайлов, Иван Песин, Сергей Скороходов, Александр Саввин, Роман Шумихин, Александр Куприн, Андрей Киселев, Игорь Яровинский, Юрий Прушинский

Со всеми предложениями, идеями и комментариями обращайтесь к Александру Куприну ([email protected]). Убедительная просьба: указывайте сразу, не возражаете ли Вы против публикации Ваших отзывов в рассылке.

Сайт рассылки: http://gazette.linux.ru.net
Эту статью можно взять здесь: http://gazette.linux.ru.net/lg81/tougher.html
Архивы выпусков можно взять здесь: http://gazette.linux.ru.net/archive/