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