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

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

C++ изобилует интерфейсами. Интерфейсы функций. Интерфейсы классов. Интерфейсы шаблонов. Каждый интерфейс – это средство, посредством которого пользователь взаимодействует с вашим кодом. Предположим, что вы имеете дело с разумными людьми, которые стремятся хорошо сделать свою работу. Они хотят применять ваши интерфейсы корректно. Если случится, что они применят какой-то из них неправильно, то часть вины за это ляжет на вас. В идеале, при попытке использовать интерфейс так, что пользователь не получит ожидаемого результата, код не должен компилироваться. А если компилируется, то должен делать то, что имел в виду пользователь.

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

class Date {

public:

Date(int month, int day, int year);

...

};

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

Date(30, 3, 1995); // должно быть “3, 30”, а не “30, 3”

Во-вторых, номер месяца или дня может быть указан неверно:

Date(2, 20, 1995); // Должно быть “3, 30”, а не “2, 20”

(Последний пример может показаться надуманным, но вспомните, что на клавиатуре «2» находится рядом с «3». Такие опечатки случаются сплошь и рядом.)

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

struct Day { struct Month { struct Year {

explicit Day(int d) explicit Month(int m) explicit Year(int y)

: val(d) {} : val(m) {} : val(y) {}

int val; int val; int val;

}; }; };

class Date {

public:

Date(const Month& m, const Day& d, const Year& y(;

...

};

Date d(30, 3, 1995); // ошибка! неправильные типы

Date d(Day(30), Month(3), Year(1995); // ошибка! неправильные типы

Date d(Month(3), Day(30), Year(1995)); // порядок, типы корректны

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

После того как определены правильные типы, иногда имеет смысл ограничить множество принимаемых ими значений. Например, есть только 12 допустимых значений месяцев, что и должен отразить тип Month. Один из способов сделать это – применить перечисление (enum) для представления месяца. Но перечисления не так безопасны по отношению к типам, как хотелось бы. Например, перечисления могут быть использованы как значения типа int (см. правило 2). Более безопасное решение – определить набор допустимых месяцев:

class Month {

public:

static Month Jan() {return Month(1);} // функции возвращают все

static Month Feb() {return Month(2);} // допустимые значения Month.

... // Cм. ниже, почему это функции,

static Month Dec() {return Month(12);} // а не объекты

... // прочие функции-члены

private:

explicit Month(int m); // предотвращает создание новых

// значений Month

... // специфичные для месяца данные

};

Date d(Month::Mar(), Day(30), Year(1995));

Идея применения функций вместо объектов для представления месяцев может показаться вам необычной. Но вспомните о ненадежности инициализации нелокальных статических объектов. Правило 4 поможет освежить вашу память.

Другой способ предотвратить вероятные ошибки клиентов – ограничить множество разрешенных для типа операций. Общий способ установить ограничения – добавить const. Например, в правиле 3 объясняется, как добавление модификатора const к типу значения, возвращаемого функцией operator*, может предотвратить следующую ошибку клиента:

if(a *b = c)... // имелось в виду сравнение

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

Избегать неоправданных расхождений с поведением встроенных типов необходимо для того, чтобы обеспечить согласованность интерфейсов. Из всех характеристик простых для применения интерфейсов согласованность – наверное, самая важная. И наоборот, несогласованность – прямая дорога к ухудшению качества интерфейса. Интерфейсы STL-контейнеров в большинстве случаев согласованы (хотя и не идеально), и это немало способствует простоте их использования. Например, каждый STL-контейнер имеет функцию-член size, которая сообщает, сколько объектов содержится в контейнере. Напротив, в языке Java для массивов используется свойство length, для класса String – метод length, а для класса List – метод size. Также и в. NET: класс Array имеет свойство Length, а класс ArrayList – свойство Count. Некоторые разработчики считают, что интегрированные среды разработки (IDE) делают эти несоответствия несущественными, но они ошибаются. Несоответствия мешают программисту продуктивно работать, и ни одна IDE это не компенсирует.

Любой интерфейс, который требует, чтобы пользователь что-то помнил, может быть использован неправильно, ибо пользователь вполне способен забыть, что от него требуется. Например, в правиле 13 представлена фабричная функция, которая возвращает указатель на динамически распределенный объект в иерархии Investment:

Investment *createInvestment(); // из правила 13: параметры

// для простоты опущены

Чтобы избежать утечки ресурсов, указатель, возвращенный createInvestment, обязательно должен быть удален. Следовательно, пользователь может совершить, по крайней мере, две ошибки: забыть удалить указатель либо удалить его более одного раза.

Правило 13 показывает, как клиенты могут поместить значение, возвращенное createInvestment, в «интеллектуальный» указатель наподобие auto_ptr или tr1::shared_ptr, возложив тем самым на него ответственность за вызов delete. Но что, если клиент забудет применить «интеллектуальный» указатель? Во многих случаях для предотвращения этой проблемы лучше было бы написать фабричную функцию, которая сама возвращает «интеллектуальный» указатель:

std::tr1::shared_ptr<Investment> createInvestment();

Тогда пользователь будет вынужден сохранять возвращаемое значение в объекте типа tr1::shared_ptr, и ему не придется помнить о том, что объект Investment по завершении работы с ним необходимо удалить.

Фактически возврат значения типа tr1::shared_ptr позволяет проектировщику интерфейса предотвратить и многие другие ошибки, связанные с освобождением ресурса, потому что, как объяснено в правиле 14, tr1::shared_ptr допускает привязку функции-чистильщика к интеллектуальному указателю при его создании (auto_ptr не имеет такой возможности).

Предположим, что от пользователя, который получил указатель Investment* от createInvestment, ожидается, что в конце работы он передаст его функции getRidOfInvestment, вместо того чтобы применить к нему delete. Подобный интерфейс – прямая дорога к другой ошибке, заключающейся в использовании не того механизма удаления ресурсов (пользователь может все-таки вызвать delete вместо getRidOfInvestment). Реализация createInvestment может снять эту проблему за счет того, что вернет tr1::shared_ptr с привязанной к нему в качестве чистильщика функцией getRidOfInvestment.

Конструктор tr1::shared_ptr принимает два аргумента: указатель, которым нужно управлять, и функцию-чистильщик, которая должна быть вызвана, когда счетчик ссылок достигнет нуля. Это наводит на мысль попытаться следующим образом создать нулевой указатель tr1::shared_ptr с getRidOfInvestment в качестве чистильщика:

std::tr1_shared_ptr<Investment> // попытка создать нулевой shared_ptr

pInv(0, getRidOfInvestment); // с чистильщиком

// это не скомпилируется

К сожалению, C++ это не приемлет. Конструктор tr1::shared_ptr требует, чтобы его первый параметр был указателем, а 0 – это не указатель, это целое. Да, оно преобразуется в указатель, но для данного случая этого недостаточно: tr1::shared_ptr настаивает на настоящем указателе. Приведение типа решает эту проблему:

std::tr1_shared_ptr<Investment> // создает null shared_ptr

pInv(static_cast<Investment*>(0), // с getRidOfInvestment в качестве

getRidOfInvestment); // чистильщика. о static_cast см.

// в правиле 27

Это значит, что код, реализующий createInvestment, который должен возвратить tr1::shared_ptr с getRidOfInvestment в качества чистильщика, будет выглядеть примерно так:

std::tr1::shared_ptr<Investment> createInvestment()

{

std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),

getRidOfInvestment);

retVal = ...; // retVal должен указывать

// на корректный объект

return retVal;

}

Конечно, если указатель, которым должен управлять pInv, можно было бы определить до создания pInv, то лучше было бы передать его конструктору pInv вместо инициализации pInv нулем с последующим присваиванием значения (см. правило 26).

Особенно симпатичное свойство tr1::shared_ptr заключается в том, что он автоматически использует определенного пользователем чистильщика, чтобы избежать другой потенциальной ошибки пользователя – «проблемы нескольких DLL». Она возникает, если объект создается оператором new в одной динамически скомпонованной библиотеке (DLL), а удаляется оператором delete в другой. На многих платформах в такой ситуации возникает ошибка во время исполнения. tr1::shared_ptr решает эту проблемы, поскольку его чистильщик по умолчанию использует delete из той же самой DLL, где был создан tr1::shared_ptr. Это значит, например, что если класс Stock является производным от Investment и функция createInvestment реализована следующим образом:

std::tr1::shared_ptr<Investment> createInvestment()

{

return std::tr1::shared_ptr<Investment>(new Stock);

}

то возвращенный ей объект tr1::shared_ptr можно передавать между разными DLL без риска столкнуться с описанной выше проблемой. Объект tr1::shared_ptr, указывающий на Stock, «помнит», из какой DLL должен быть вызван delete, когда счетчик ссылок на Stock достигнет нуля.

Впрочем, этот правило не о tr1::shared_ptr, а о том, как делать интерфейсы легкими для правильного использования и трудными – для неправильного. Но класс tr1::shared_ptr дает настолько простой способ избежать некоторых клиентских ошибок, что на нем стоило остановиться. Наиболее распространенная реализация tr1::shared_ptr находится в библиотеке Boost (см. правило 55). Размер объекта shared_ptr из Boost вдвое больше размера обычного указателя, в нем динамически выделяется память для служебных целей и данных, относящихся к чистильщику, используется вызов виртуальной функции для обращения к чистильщику, производится синхронизация потоков при изменении значения счетчика ссылок в многопоточной среде. (Вы можете отключить поддержку многопоточности, определив символ препроцессора.) Короче говоря, этот интеллектуальный указатель по размеру больше обычного, работает медленнее и использует дополнительную динамически выделяемую память. Но во многих приложениях эти дополнительные затраты времени исполнения будут незаметны, зато уменьшение числа ошибок пользователей заметят все.

Что следует помнить

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

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

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

• Класс tr1::shared_ptr поддерживает пользовательские функции-чистильщики. Это снимает «проблему нескольких DLL» и может быть, в частности, использовано для автоматического освобождения мьютекса (см. правило 14).

Данный текст является ознакомительным фрагментом.



Поделитесь на страничке

Следующая глава >

Похожие главы из других книг:

Ошибка 5. Неправильно выбирать фразы для продвижения сайта

Из книги автора

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


1.6.4. Правило разделения: следует отделять политику от механизма и интерфейсы от основных модулей

Из книги автора

1.6.4. Правило разделения: следует отделять политику от механизма и интерфейсы от основных модулей В разделе "Что в Unix делается неверно" отмечалось, что разработчики системы X Window приняли основное решение о реализации "механизма, а не политики". Такой подход был направлен на


1.6.10. Правило наименьшей неожиданности: при проектировании интерфейсов всегда следует использовать наименее неожиданные элементы

Из книги автора

1.6.10. Правило наименьшей неожиданности: при проектировании интерфейсов всегда следует использовать наименее неожиданные элементы Данное правило также широко известно под названием "Принцип наименьшего удивления" (Principle of Least Astonishment).Простейшими в использовании


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

Из книги автора

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


Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы

Из книги автора

Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы Отношение C++ к инициализации значений объектов может показаться странным. Например, если вы пишете:int x;то в некоторых контекстах переменная x будет гарантированно инициализирована нулем, а в


Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции

Из книги автора

Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции В мире объектно-ориентированного программирования преобладают явные интерфейсы и полиморфизм на этапе исполнения. Например, рассмотрим следующий (бессмысленный) класс:class Widget


1.6.4. Правило разделения: следует отделять политику от механизма и интерфейсы от основных модулей

Из книги автора

1.6.4. Правило разделения: следует отделять политику от механизма и интерфейсы от основных модулей В разделе "Что в Unix делается неверно" отмечалось, что разработчики системы X Window приняли основное решение о реализации "механизма, а не политики". Такой подход был направлен на


1.6.10. Правило наименьшей неожиданности: при проектировании интерфейсов всегда следует использовать наименее неожиданные элементы

Из книги автора

1.6.10. Правило наименьшей неожиданности: при проектировании интерфейсов всегда следует использовать наименее неожиданные элементы Данное правило также широко известно под названием "Принцип наименьшего удивления" (Principle of Least Astonishment).Простейшими в использовании являются


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

Из книги автора

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


огород козловского: Трудно быть богом, или Дворник и Шарик

Из книги автора

огород козловского: Трудно быть богом, или Дворник и Шарик Трудно быть богом, или Дворник и ШарикАвтор: Козловский ЕвгенийОпубликовано в журнале "Компьютерра" N27-28 от 22 июля 2008 годаВ "Огороде" "Поляризация" (www.computerra.ru/think/ogorod/358863) я довольно подробно описал HTC Touch Cruise (P3650),


Трудно быть героем: почему Ассанжу не понравилась «Пятая власть» Евгений Золотов

Из книги автора

Трудно быть героем: почему Ассанжу не понравилась «Пятая власть» Евгений Золотов Опубликовано 18 октября 2013 Нынче трудно быть героем. Обыватель всегда смотрел на мир через призму своих, обывательских, представлений, но сегодня, вскормленный


Глава 5 Чтобы компьютер долго служил и не терял информацию, его нужно правильно использовать

Из книги автора

Глава 5 Чтобы компьютер долго служил и не терял информацию, его нужно правильно использовать 5.1. Правила эксплуатации стационарного компьютера 5.1.1. Общие правила Ваш компьютер будет «жить долго и счастливо», если вы станете придерживаться следующих правил


Как правильно использовать дополнительный свет

Из книги автора

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