Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Замечание об инкапсуляции важнее, чем может показаться с первого взгляда. Если вы скрываете данные-члены от пользователей (то есть инкапсулируете их), то можете обеспечить неизменность инвариантов класса, поскольку повлиять на них могут только функции-члены. Более того, вы сохраняете за собой право позже изменить реализацию. Если же вы не скрываете своих решений, то очень скоро обнаружите, что даже если у вас есть исходный код класса, ваша способность изменить его открытые члены чрезвычайно ограничена, потому что при этом перестанет работать слишком много клиентских программ. Открытость означает отсутствие инкапсуляции, и на практике «неинкапсулированный» означает «неизменяемый», особенно если речь идет о классах, которые нашли широкое применение. Но как раз широко используемые классы наиболее нуждаются в инкапсуляции, поскольку они
Аргументы против защищенных (protected) данных-членов аналогичны. Фактически тут нет вообще никаких отличий, хотя поначалу может показаться, что это не так. Рассуждения о синтаксической непротиворечивости и тонко настраиваемом доступе в той же мере касаются защищенных членов, что и открытых, но как насчет инкапсуляции? Являются ли защищенные данные более инкапсулированными, чем открытые? Как это ни странно, но на практике – нет.
В правиле 23 объясняется, что инкапсуляция некоей сущности обратно пропорциональна объему кода, который может перестать работать, если эта сущность изменяется. Таким образом, степень инкапсуляции членов данных обратно пропорциональна объему кода, который перестанет работать, если этот член изменится, например будет изъят из класса (возможно, став вычисляемым, как в примере averageSoFar выше).
Предположим, у нас есть открытый член данных, и мы исключаем его из класса. Как много кода это затронет? Весь клиентский код, который использует его, объем которого, как правило, неизвестен. Открытые данные-члены, таким образом, абсолютно не инкапсулированы. Но предположим, что исключается защищенный член данных. Сколько кода будет затронуто теперь? Все производные классы, количество которых опять же неизвестно. Таким образом, защищенные члены-данные не инкапсулированы в той же степени, что и открытые, поскольку в обоих случаях изменения затрагивают клиентский код неизвестного объема. Это не очевидно, но, как вам скажут опытные разработчики библиотек, это все-таки правда. Как только вы объявили член данных открытым или защищенным и пользователи начали обращаться к нему, изменить что-либо становится очень трудно. Слишком много кода нужно переписывать, повторно тестировать, документировать или перекомпилировать. С точки зрения инкапсуляции, должно быть только два уровня доступа: закрытый (обеспечивающий инкапсуляцию) и все остальные (не обеспечивающие).
• Объявляйте данные-члены закрытыми (private). Это дает клиентам синтаксически однородный доступ к данным, обеспечивает возможность тонкого управления доступом, позволяет гарантировать инвариантность и предоставляет авторам реализации классов гибкость.
• Защищенные члены не более инкапсулированы, чем открытые.
Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса
Возьмем класс для представления Web-браузера. В числе прочих такой класс может предлагать функции, который очищают кэш загруженных элементов, очищают историю посещенных URL и удаляют из системы все «куки» (cookies):
Найдутся пользователи, которые захотят выполнить все эти действия вместе, поэтому WebBrowser может также предоставить функцию и для этой цели:
Конечно, такая функциональность может быть обеспечена также функцией, не являющейся членом класса, которая вызовет соответствующие функции-члены:
Что лучше – функция-член clearEverything или свободная функция clear-Browser?
Принципы объектно-ориентированного проектирования диктуют, что данные и функции, которые оперируют ими, должны быть связаны вместе, и это предполагает, что функция-член – лучший выбор. К сожалению, это предположение неверно. Оно основано на непонимании того, что такое «объектно-ориентированный». Да, в объектно-ориентированных программах данные должны быть инкапсулированы, насколько возможно. В противоположность интуитивному восприятию функция-член clearEverything в действительности менее инкапсулирована, чем свободная функция clearBrowser. Более того, предоставление свободной функции позволяет обеспечить большую гибкость при «упаковке» функциональности класса WebBrowser, а это приводит к меньшему числу зависимостей на этапе компиляции и расширяет возможности для расширения класса. Поэтому свободная функция лучше по многим причинам. Важно их отчетливо понимать.
Начнем с инкапсуляции. Если некая сущность инкапсулируется, она скрывается из виду. Чем больше эта сущность инкапсулирована, тем меньше частей программы могут ее видеть. Чем меньше частей программы могут видеть некую сущность, тем больше гибкости мы имеем для внесения изменений, поскольку изменения напрямую касаются лишь тех частей, которым эти изменения видны. Таким образом, чем больше степень инкапсуляции сущности, тем шире наши возможности вносить в нее изменения. Вот причина того, почему мы ставим инкапсуляцию на первое место: она обеспечивает нам гибкость в изменении кода таким образом, что это затрагивает минимальное количество пользователей.
Рассмотрим данные, ассоциированные с объектом. Чем меньше существует кода, который видит эти данные (то есть имеет к ним доступ), тем в большей степени они инкапсулированы и тем свободнее мы можем менять их характеристики, например количество членов-данных, их типы и т. п. Грубой оценкой объем кода, который может видеть некоторый член данных, можно считать число функций, имеющих к нему доступ: чем больше таких функций, тем менее инкапсулированы данные.
В правиле 22 объясняется, что данные-члены должны быть закрытыми, потому что в противном случае к ним имеет доступ неограниченное число функций. Они вообще не инкапсулированы. Для закрытых же данных-членов количество функций, имеющих доступ к ним, определяется количеством функций-членов класса плюс количество функций-друзей, потому что доступ к закрытым членам разрешен только функциям-членам и друзьям класса. Если есть выбор между функцией-членом (которая имеет доступ не только к закрытым данным класса, но также к его закрытым функциям, перечислениям, определениям типов (typedef) и т. п.) и свободной функцией, не являющейся к тому же другом класса (такие функции не имеют доступа ни к чему из вышеперечисленного), но обеспечивающей ту же функциональность, то напрашивается очевидный вывод: большую инкапсуляцию обеспечивает функция, не являющаяся ни членом, ни другом, потому что она не увеличивает числа функций, которые могут иметь доступ к закрытой секции класса. Это объясняет, почему clearBrowser (свободная функция) предпочтительнее, чем clearEverything (функция-член).
Здесь стоит обратить внимание на два момента. Первое – все вышесказанное относится только к свободным функциям, не являющимся друзьями класса. Друзья имеют такой же доступ к закрытым членам класса, что и функции-члены, а потому точно так же влияют на инкапсуляцию. С точки зрения инкапсуляции, выбор следует делать не между функциями-членами и свободными функциями, а между функциями-членами, с одной стороны, и свободными функциями, не являющимися друзьями, – с другой. (Но оценивать проектное решение надо, конечно, не только с точки зрения инкапсуляции. В правиле 24 объясняется, что когда дело касается неявного приведения типов, то выбирать надо между функциями-членами и свободными функциями.)