Шрифт:
При разработке интерфейсов, простых для правильного применения и трудных – для неправильного, вы должны предвидеть, какие ошибки может допустить пользователь. Например, предположим, что вы разрабатываете конструктор класса, представляющего дату:
На первый взгляд, этот интерфейс может показаться разумным (во всяком случае, в США), но есть, по крайней мере, две ошибки, которые легко может допустить пользователь. Во-первых, он может передать параметры в неправильном порядке:
Во-вторых, номер месяца или дня может быть указан неверно:
(Последний пример может показаться надуманным, но вспомните, что на клавиатуре «2» находится рядом с «3». Такие опечатки случаются сплошь и рядом.)
Многих ошибок можно избежать за счет введения новых типов. Система контроля типов – ваш первый союзник в деле предотвращения компилируемости нежелательного кода. В данном случае мы можем ввести простые типы-обертки, чтобы различать дни, месяцы и годы, затем использовать их в конструкторе Date:
Еще лучше сделать Day, Month и Year полноценными классами, инкапсулирующими свои данные (см. правило 22). Но даже применение простых структур наглядно демонстрирует, что разумное использование новых типов способно эффективно предотвратить ошибки при использовании интерфейсов.
После того как определены правильные типы, иногда имеет смысл ограничить множество принимаемых ими значений. Например, есть только 12 допустимых значений месяцев, что и должен отразить тип Month. Один из способов сделать это – применить перечисление (enum) для представления месяца. Но перечисления не так безопасны по отношению к типам, как хотелось бы. Например, перечисления могут быть использованы как значения типа int (см. правило 2). Более безопасное решение – определить набор допустимых месяцев:
Идея применения функций вместо объектов для представления месяцев может показаться вам необычной. Но вспомните о ненадежности инициализации нелокальных статических объектов. Правило 4 поможет освежить вашу память.
Другой способ предотвратить вероятные ошибки клиентов – ограничить множество разрешенных для типа операций. Общий способ установить ограничения – добавить const. Например, в правиле 3 объясняется, как добавление модификатора const к типу значения, возвращаемого функцией operator*, может предотвратить следующую ошибку клиента:
Фактически это пример другого общего правила облегчения правильного использования типов и усложнения неправильного их использования: поведение ваших типов должно быть согласовано с поведением встроенных типов (кроме некоторых исключительных случаев). Клиенты уже знают, как должны себя вести типы вроде int, поэтому вы должны стараться, чтобы ваши типы по возможности вели себя аналогично. Например, присваивание выражению a*b недопустимо, если a и b – целые, поэтому если нет веской причины отклониться от этого поведения, оно должно быть недопустимо и для ваших типов. Когда сомневаетесь, делайте так, как ведет себя int.
Избегать неоправданных расхождений с поведением встроенных типов необходимо для того, чтобы обеспечить согласованность интерфейсов. Из всех характеристик простых для применения интерфейсов согласованность – наверное, самая важная. И наоборот, несогласованность – прямая дорога к ухудшению качества интерфейса. Интерфейсы STL-контейнеров в большинстве случаев согласованы (хотя и не идеально), и это немало способствует простоте их использования. Например, каждый STL-контейнер имеет функцию-член size, которая сообщает, сколько объектов содержится в контейнере. Напротив, в языке Java для массивов используется свойство length, для класса String – метод length, а для класса List – метод size. Также и в. NET: класс Array имеет свойство Length, а класс ArrayList – свойство Count. Некоторые разработчики считают, что интегрированные среды разработки (IDE) делают эти несоответствия несущественными, но они ошибаются. Несоответствия мешают программисту продуктивно работать, и ни одна IDE это не компенсирует.