public:
void addValue(int speed); // добавить новое значение
double averageSoFar() const; // вернуть среднюю скорость
...
};
Теперь рассмотрим реализацию функции-члена averageSoFar. Можно, например, завести в классе член, который представляет среднее арифметическое значений скоростей, накопленных на данный момент. Тогда функция averageSoFar будет просто возвращать значение этого члена класса. Другой подход заключается в том, чтобы вычислять среднее значение скорости в функции averageSoFar при каждом вызове, для чего ей придется просмотреть все данные в коллекции.
Первый подход (хранение текущей средней скорости) увеличивает размер каждого объекта SpeedDataCollection, потому что необходимо выделить место для члена данных, хранящего текущее среднее, накопленный итог и количество элементов данных. При этом averageSoFar может быть реализована очень эффективно: это будет просто встроенная функция (см. правило 30), которая возвращает значение текущего среднего. В противоположность этому вычисление по запросу сделает данную функцию медленнее, но каждый объект SpeedDataCollection станет меньше.
Кто скажет – как лучше? На машинах с маленькой памятью (например, встроенных устройствах, установленных на дороге), и в приложениях, где среднее значение требуется нечасто, его вычисление при каждом вызове, возможно, представляет лучшее решение. Но в приложениях, где среднее значение будет запрашиваться часто, скорость реакции существенна, а память – не проблема, хранение текущего среднего обычно предпочтительнее. Важно отметить, что, имея доступ к среднему через функцию-член (то есть инкапсулировав его), вы можете легко заменять реализацию, при этом программу-клиент придется всего лишь перекомпилировать. Можно избежать даже этого неудобства, если следовать технике, описанной в правиле 31.
Сокрытие данных-членов за интерфейсом функций может обеспечить гибкость реализации в разных отношениях. Например, это облегчает извещение других объектов о том, что к члену данных происходит обращение для чтения или записи, обеспечивает возможность проверять инварианты и выполнение пред– и постусловий, позволяет реализовать синхронизацию в многопоточной среде и т. д. Программисты, которые пришли в C++ из таких языков, как Delphi и C#, увидят в этой возможности аналогию со «свойствами» («properties»), существующими в этих языках, правда, к имени «свойства» приходится добавлять скобки.
Замечание об инкапсуляции важнее, чем может показаться с первого взгляда. Если вы скрываете данные-члены от пользователей (то есть инкапсулируете их), то можете обеспечить неизменность инвариантов класса, поскольку повлиять на них могут только функции-члены. Более того, вы сохраняете за собой право позже изменить реализацию. Если же вы не скрываете своих решений, то очень скоро обнаружите, что даже если у вас есть исходный код класса, ваша способность изменить его открытые члены чрезвычайно ограничена, потому что при этом перестанет работать слишком много клиентских программ. Открытость означает отсутствие инкапсуляции, и на практике «неинкапсулированный» означает «неизменяемый», особенно если речь идет о классах, которые нашли широкое применение. Но как раз широко используемые классы наиболее нуждаются в инкапсуляции, поскольку они более других могут выиграть от замены старой реализации на более совершенную.
Аргументы против защищенных (protected) данных-членов аналогичны. Фактически тут нет вообще никаких отличий, хотя поначалу может показаться, что это не так. Рассуждения о синтаксической непротиворечивости и тонко настраиваемом доступе в той же мере касаются защищенных членов, что и открытых, но как насчет инкапсуляции? Являются ли защищенные данные более инкапсулированными, чем открытые? Как это ни странно, но на практике – нет.
В правиле 23 объясняется, что инкапсуляция некоей сущности обратно пропорциональна объему кода, который может перестать работать, если эта сущность изменяется. Таким образом, степень инкапсуляции членов данных обратно пропорциональна объему кода, который перестанет работать, если этот член изменится, например будет изъят из класса (возможно, став вычисляемым, как в примере averageSoFar выше).
Предположим, у нас есть открытый член данных, и мы исключаем его из класса. Как много кода это затронет? Весь клиентский код, который использует его, объем которого, как правило,
• Объявляйте данные-члены закрытыми (private). Это дает клиентам синтаксически однородный доступ к данным, обеспечивает возможность тонкого управления доступом, позволяет гарантировать инвариантность и предоставляет авторам реализации классов гибкость.
• Защищенные члены не более инкапсулированы, чем открытые.
Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса
Возьмем класс для представления Web-браузера. В числе прочих такой класс может предлагать функции, который очищают кэш загруженных элементов, очищают историю посещенных URL и удаляют из системы все «куки» (cookies):
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
Найдутся пользователи, которые захотят выполнить все эти действия вместе, поэтому WebBrowser может также предоставить функцию и для этой цели:
class WebBrowser {
public:
...
void clearEveryThing(); // вызывает clearCache(), clearHistory()
// и removeCookies()
...
};
Конечно, такая функциональность может быть обеспечена также функцией, не являющейся членом класса, которая вызовет соответствующие функции-члены:
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCache();
}