Инкапсуляция и С++
Предположим, что вам удалось преодолеть проблемы с транслятором и компоновщиком, описанные в предыдущем разделе. Очередное препятствие при построении двоичных компонентов на C++ появится, когда вы будете проводить инкапсуляцию (encapsulation), то есть формирование пакета. Посмотрим, что получится, если организация, использующаяFastString в приложении, возьмется выполнить невыполнимое: закончит разработку и тестирование за два месяца до срока рассылки продукта. Пусть также в течение этих двух месяцев некоторые из наиболее скептически настроенных разработчиков решили протестироватьO(1)-поисковый алгоритмFastString, запустив профайлер своего приложения. К их большому удивлению,FastString::Find стала бы на самом деле работать очень быстро, независимо от заданной длины строки. Однако с операторомLength дело обстоит не столь хорошо, так какFastString::Length использует подпрограммуstrlen из динамической библиотеки С. Эта подпрограмма – алгоритмO(n) – осуществляет линейный поиск по строкам с использованием символа конца строки (null terminator); скорость его работы пропорциональна длине строки. Столкнувшись с тем, что клиентское приложение может многократно вызывать операторLength , один из таких скептиков, скорее всего, свяжется с разработчиком библиотеки и попросит его убыстритьLength , чтобы его работа также не зависела от длины строки. Но здесь есть одно препятствие. Разработчик библиотеки уже закончил свою разработку и, скорее всего, не расположен менять одну строку исходного кода, чтобы воспользоваться преимуществами улучшенного методаLength . Кроме того, некоторые другие разработчики, возможно, уже выпустили свои продукты, основанные на текущей версииFastString , и теперь разработчик библиотеки не имеет морального права изменять эти приложення.
С этой точки зрения нужно просто вернуться к определению классаFastString и решить, что можно изменить и что необходимо сохранить, чтобы уже установленная база успешно функционировала. К счастью, классFastString был разработан с учетом возможности инкапсуляции, и все его элементы данных ( data members) являются закрытыми ( private). Это придает классу значительную гибкость, так как ни одна клиентская программа не может непосредственно получить доступ к элементам данныхFastString . В силу того, что по отношению к четырем открытым ( public) членам класса не было сделано никаких изменений, то и в любом клиентском приложении никаких изменений также не потребуется. Вооружившись этой верой, разработчик библиотеки переходит к реализацииFastString версии 2.0.
Очевидным улучшением является следующее решение: в тексте конструктора ( constructor) занести длину строки в кэш и возвращать кэшированную длину в новой версии методаLength. Так как строка не может быть изменена после создания, нет необходимости беспокоиться, что ее длина будет вычисляться многократно. В действительности длина уже однажды вычислена в конструкторе при назначении буфера, так что понадобится только горстка дополнительных машинных инструкций. Вот каким будет модифицированное определение класса:
// faststring.h version 2.0
class declspec(dllexport) FastString {
const int mcch;
// count of characters
// число символов
char mpsz;
public:
FastString(const char *psz);
~FastString(void);
int Length(void) const;
// returns # of characters
// возвращает число символов
int Find(const char *psz) const;
// returns offset – возвращает смещение
};
Отметим, что единственной модификацией является добавление закрытого элемента данных. Чтобы правильно инициализировать такой элемент, конструктор должен быть изменен следующим образом:
FastString::FastString(const char *psz) : mcch(strlen(psz)), mpsz(new char[mcch + 1])
{
strcpy(mpsz, psz);
}
С введением кэшированной длины метод Length становится тривиальным:
int FastString::Length(void) const
{
return mcch;
// return cached length
// возвращает скрытую длину
}
Сделав эти три модификации, разработчик библиотеки може
Чтобы правильно инициализировать такой элемент, конструктор должен быть изменен следующим образом:
FastString::FastString(const char *psz) : mcch(strlen(psz)), mpsz(new char[mcch + 1])
{
strcpy(mpsz, psz);
}
С введением кэшированной длины метод Length становится тривиальным:
int FastString::Length(void) const
{
return mcch;
// return cached length
// возвращает скрытую длину
}
Сделав эти три модификации, разработчик библиотеки может теперь перестроить DLLFastString и сопутствующий ей набор тестов, которые полностью проверяют каждый аспект классаFastString. Разработчик будет приятно удивлен, узнав, что принцип инкапсуляции обошелся ему дешево, и в исходных текстах тестов не понадобилось делать никаких изменений. После проверки того. что новая DLL работает правильно, разработчик библиотек отсылаетFastString версии 2.0 клиенту, будучи уверенным, что вся работа завершена.
Когда клиенты, заказавшие изменения, получают модернизированныйFastString, они включают новое определение класса и DLL в систему контроля своего исходного кода и запускают тестирование нового и улучшенногоFastString. Подобно разработчику библиотеки, они тоже приятно удивлены: для того, чтобы воспользоваться преимуществами новой версииLength, не требуется никаких модификаций исходного кода. Вдохновленная этим опытом, команда разработчиков убеждает начальство включить новую DLL в окончательный «золотой» CD, уже готовый для выпуска. Это тот редкий случай, когда руководство идет навстречу энтузиастам-разработчикам и включает в окончательный продукт новую DLL. Подобно большинству программ инсталляции, описание установки клиентской программы настроено на молчаливое (без предупреждения) замещение всех старых версийFastStringDLL, какие есть на машине конечного пользователя. Это выглядит вполне безобидно, поскольку эти изменения не затронули открытый интерфейс класса, так что тотальная молчаливая модернизация под версию 2.0FastString только улучшит любые имеющиеся клиентские приложения, которые были установлены раньше.
Представим себе следующий сценарий: конечные пользователи наконец-то получают свои экземпляры вожделенного продукта. Каждый из них тут же бросает все и устанавливает новое приложение на свою машину, дабы попробовать его. После того как высохли слезы восторга от того, что наконец-то можно делать быстрый текстовый поиск, пользователь возвращается к его или ее нормальному состоянию и запускает ранее установленное приложение, которое также имеет неосторожность использовать DLLFastString . Первые несколько минут всё идет хорошо. Затем внезапно появляется сообщение, что возникла исключительная ситуация и что вся работа конечного пользователя пропала. Он пытается запустить приложение снова, но на этот раз диалоговое окно об исключительной ситуации появляется почти сразу. Конечный пользователь, привычный к употреблению современного программного обеспечения, переустанавливает операционную систему и все приложения, но даже это не спасает от повторения исключительной ситуации. Что же произошло?
А произошло то, что разработчик библиотеки был убаюкан верой в то, что C++ поддерживает инкапсуляцию. Хотя C++ и поддерживаетсинтаксическую инкапсуляцию через свои закрытые и защищенные ключевые слова, в стандарте C++ ничего не сказано одвоичной инкапсуляции. Это происходит потому, что модель трансляции C++ требует, чтобы клиентский компилятор имел доступ ко всей информации относительно двоичного представления объектов, – с целью обработать экземпляр класса или делать невиртуальные вызовы метода. Это включает в себя информацию о размере и порядке закрытых и защищенных элементов данных объекта. Рассмотрим сценарий, показанный на рис. 1.3. Версия 1.0FastString требует четыре байта на экземпляр (принимаяsizeof(char *) == 4 ). Клиенты написанного под версию 1.0 определения класса выделяют четыре байта памяти под вызов конструктора класса. Конструктор, деструктор и методы версии 2.0 (а именно эти версии содержатся в DLL в машине конечного пользователя) ожидают, что клиент выделил восемь байт на экземпляр (принятоsizeof(int) == 8 ), и не предусматривают собственных резервов для записи во все восемь байт.