Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса

Правило 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();

}

Что лучше – функция-член clearEverything или свободная функция clear-Browser?

Принципы объектно-ориентированного проектирования диктуют, что данные и функции, которые оперируют ими, должны быть связаны вместе, и это предполагает, что функция-член – лучший выбор. К сожалению, это предположение неверно. Оно основано на непонимании того, что такое «объектно-ориентированный». Да, в объектно-ориентированных программах данные должны быть инкапсулированы, насколько возможно. В противоположность интуитивному восприятию функция-член clearEverything в действительности менее инкапсулирована, чем свободная функция clearBrowser. Более того, предоставление свободной функции позволяет обеспечить большую гибкость при «упаковке» функциональности класса WebBrowser, а это приводит к меньшему числу зависимостей на этапе компиляции и расширяет возможности для расширения класса. Поэтому свободная функция лучше по многим причинам. Важно их отчетливо понимать.

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

Рассмотрим данные, ассоциированные с объектом. Чем меньше существует кода, который видит эти данные (то есть имеет к ним доступ), тем в большей степени они инкапсулированы и тем свободнее мы можем менять их характеристики, например количество членов-данных, их типы и т. п. Грубой оценкой объем кода, который может видеть некоторый член данных, можно считать число функций, имеющих к нему доступ: чем больше таких функций, тем менее инкапсулированы данные.

В правиле 22 объясняется, что данные-члены должны быть закрытыми, потому что в противном случае к ним имеет доступ неограниченное число функций. Они вообще не инкапсулированы. Для закрытых же данных-членов количество функций, имеющих доступ к ним, определяется количеством функций-членов класса плюс количество функций-друзей, потому что доступ к закрытым членам разрешен только функциям-членам и друзьям класса. Если есть выбор между функцией-членом (которая имеет доступ не только к закрытым данным класса, но также к его закрытым функциям, перечислениям, определениям типов (typedef) и т. п.) и свободной функцией, не являющейся к тому же другом класса (такие функции не имеют доступа ни к чему из вышеперечисленного), но обеспечивающей ту же функциональность, то напрашивается очевидный вывод: большую инкапсуляцию обеспечивает функция, не являющаяся ни членом, ни другом, потому что она не увеличивает числа функций, которые могут иметь доступ к закрытой секции класса. Это объясняет, почему clearBrowser (свободная функция) предпочтительнее, чем clearEverything (функция-член).

Здесь стоит обратить внимание на два момента. Первое – все вышесказанное относится только к свободным функциям, не являющимся друзьями класса. Друзья имеют такой же доступ к закрытым членам класса, что и функции-члены, а потому точно так же влияют на инкапсуляцию. С точки зрения инкапсуляции, выбор следует делать не между функциями-членами и свободными функциями, а между функциями-членами, с одной стороны, и свободными функциями, не являющимися друзьями, – с другой. (Но оценивать проектное решение надо, конечно, не только с точки зрения инкапсуляции. В правиле 24 объясняется, что когда дело касается неявного приведения типов, то выбирать надо между функциями-членами и свободными функциями.)

Во-вторых, из того, что забота об инкапсуляции требует, чтобы функция не была членом класса, вовсе не следует, что эта функция не может быть членом какого-то другого класса. Это может облегчить жизнь программистам, привыкшим к языкам, в которых все функции должны быть членами классов (например, Eiffel, Java, C# и т. п.). Например, мы можем сделать clearBrowser статической функцией-членом некоторого служебного класса. До тех пор пока она не является частью (или другом) класса WebBrowser, она никак не скажется на инкапсуляции его закрытых членов.

В C++ более естественно объявить clearBrowser свободной функцией в том же пространстве имен, что и класс WebBrowser:

namespace WebBrowserStuff {

class WebBrowser {...};

void clearBrowser(WebBrowser& wb);

...

}

Но дело тут не только в естественности, ведь пространства имен, в отличие от классов, могут быть находиться в нескольких исходных файлах. И это важно, потому что функции вроде clearBrowser являются вспомогательными. Не будучи ни членами, ни друзьями класса, они не имеют специального доступа к WebBrowser и никак не могут расширить те возможности, которые у пользователей класса WebBrowser и так уже были. Не будь функции clearBrowser, пользователь мог бы самостоятельно вызвать clearCache, clearHistory и removeCookies.

Для класса, подобного WebBrowser, можно было бы определить много таких вспомогательных функций: для работы с закладками, вывода на печать, управления «куками» и т. п. Вообще говоря, большинству пользователей будут интересны только некоторые из этих функций. Но с какой стати компиляция пользовательской программы, в которой используются только функции, относящиеся к закладкам, должна зависеть, например, от наличия функций управления «куками»? Самый простой способ разделить их – это объявить функции, относящиеся к закладкам, в одном заголовочном файле, функции управления «куками» – в другом, функции поддержки печати – в третьем и так далее:

// заголовок “webbrowser.h” – заголовок для самого класса WebBrowser,

// а также базовой функциональности, имеющей к нему отношение

namespace WebBrowserStuff {

class WebBrowser{...};

... // базовая функциональность, то есть

// функции-нечлены, нужные почти всем

// клиентам

}

// заголовок “webbrowserbookmarks.h”

namespace WebBrowserStuff {

... // вспомогательные функции, касающиеся

} // закладок

// заголовок “webbrowsercookies.h”

namespace WebBrowserStuff {

... // вспомогательные функции, касающиеся

} // “куков”

...

Отметим, что именно так организована стандартная библиотека C++. Вместо единственного монолитного заголовка <С++ StandardLibrary>, содержащего все, что есть в пространстве имен std, существуют десятки более мелких заголовочных файлов (например, <vector>, <algorithm>, <memory> и т. п.). В каждом из них объявлена некоторая функциональность из std. Пользователь, которому нужно только то, что имеет отношение к векторам, может не включать в свою программу директиву #include <memory>, а пользователь, не нуждающийся в списках, не обязан включать #include <list>. Поэтому на этапе компиляции пользовательские программы зависят только от тех частей системы, которые они действительно используют (см. в правиле 31 обсуждение других способов уменьшения зависимостей компиляции). Подобное разделение функциональности невозможно, если она обеспечивается функциями-членами класса, потому что класс должен быть определен полностью, его нельзя разбить на части.

Размещение вспомогательных функций в разных заголовочных файлах, но в одном пространстве имен – означает также, что пользователи могут легко расширять набор вспомогательных функций. Для этого нужно лишь поместить новые функции (нечлены и недрузья) в то же пространство имен. Например, если пользователь класса WebBrowser решит дописать вспомогательные функции, имеющие отношение к загрузке изображений, он должен будет создать заголовочный файл, включающий объявления этих функций в пространство имен Web-BrowserStuff. Новые функции становятся после этого так же доступны, как и все прочие вспомогательные функции. Это еще одно свойство, которое не могут представить классы, потому что определения классов закрыты для расширения клиентами. Конечно, клиенты могут создавать производные классы, но они не будут иметь доступа к инкапсулированным (то есть закрытым) членам базового класса, поэтому таким образом «расширенную» функциональность уже не назовешь первоклассной. Кроме того, в правиле 7 объясняется, что не все классы предназначены для того, чтобы быть базовыми.

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

• Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса. Это повышает степень инкапсуляции и расширяемости, а также гибкость «упаковки» функциональности.

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