Текст книги "Программист-прагматик. Путь от подмастерья к мастеру"
Автор книги: Эндрю Хант
Соавторы: Дэвид Томас
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 14 (всего у книги 28 страниц)
Языки, поддерживающие исключения, могут сделать процедуру освобождения ресурса нетривиальной. Как удостовериться, что все ресурсы, назначенные до возбуждения исключения, освобождены надлежащим образом? В некоторой степени ответ зависит от языка программирования.
Балансировка ресурсов в исключениях языка С++
Язык С++ поддерживает механизм исключений типа try…catch. К сожалению, это означает, что всегда существует по крайней мере два возможных варианта выхода из подпрограммы, которая перехватывает, а затем повторно возбуждает исключение:
void doSomething(void) {
Node *n = new Node;
try {
// do something
}
catch (…) {
delete n;
thow;
}
delete n;
}
Заметим, что созданный нами узел освобождается дважды – один раз во время нормального выхода из подпрограммы, а второй раз в обработчике исключений. Это явное нарушение принципа DRY и проблема в сопровождении, которая может возникнуть в любой момент.
Однако в наших интересах воспользоваться семантикой языка С++. Локальные объекты автоматически разрушаются при выходе из блока, в котором они находятся. Это дает нам несколько вариантов. Если обстоятельства позволяют, можно поменять n: оно обозначает не указатель, а реальный объект Node в стеке:
void doSomething1(void) {
Node n;
try {
// делаем что-либо
}
catch (…) {
throw;
}
}
В этом случае мы используем С++ для автоматического разрушения объекта Node независимо от того, возбуждено исключение или нет.
В случае, если замена указателя на объект невозможна, тот же самый эффект достигается при инкапсулировании ресурса (речь идет об указателе Node) в пределах другого класса.
// Класс оболочки для ресурсов Node
class NodeResource {
Node *n;
public:
NodeResource() {n = new Node;}
~NodeResource() {delete n;}
Node *operator ->() {return n;}
};
void doSomething2(void) {
NodeResource n;
try {
// do something
}
catch (…) {
throw;
}
}
Теперь класс-оболочка NodeResource выступает гарантом того, что при разрушении его объектов происходит и разрушение соответствующих узлов. Для удобства класс оболочка предоставляет оператор разыменования – », с тем чтобы пользователи могли обращаться к полям в инкапсулированном объекте Node напрямую.
Поскольку эта методика столь полезна, в стандартной библиотеке С++ имеется шаблонный класс autOjDtr, обеспечивающий автоматические оболочки для динамически размещаемых объектов.
void doSomething3(void) {
auto_ptr
// Обращение к узлу Node как р-»…
// В конце узел автоматически удаляется
}
Балансировка ресурсов в языке Java
В отличие от C++ язык Java реализует «ленивую» форму автоматического разрушения объекта. Объекты, ссылки на которые отсутствуют, считаются кандидатами на попадание в «мусор», и их метод finalize будет вызываться в любой момент, когда процедура сборки мусора будет претендовать на эти объекты. Представляя собой удобство для разработчиков, которым больше не приходится жаловаться на утечки памяти, в то же время он усложняет реализацию процедуры очистки ресурсов по схеме С + +. К счастью, разработчики языка Java глубокомысленно ввели компенсирующую языковую функцию – предложение finally. Если блок try содержит предложение finally, то часть программы, относящаяся к этому предложению, гарантированно исполняется только в том случае, если исполняется любая инструкция в блоке try. Неважно, возбуждается при этом исключение или нет (даже при выполнении оператора return программой в блоке try) – программа, относящаяся к предложению finally, будет выполнена. Это означает, что использование ресурса может быть сбалансировано с помощью программы типа:
public void doSomething() throws IOException {
File tmpFile = new File(tmpFileName);
FileWriter tmp = new FileWriter(tmpFile);
try {
// do some work
}
finally {
tmpFile.delete();
}
}
Подпрограмма использует промежуточный файл, который мы хотим удалить, независимо от того, как подпрограмма заканчивает свою работу. Блок finally позволяет нам выразить это в сжатой форме.
Случаи, при которых балансировка ресурсов невозможнаВозникают моменты, когда основная схема распределения ресурсов просто не годится. Обычно это происходит в программах, которые используют динамические структуры данных. Одна подпрограмма выделяет область в памяти и связывает ее в структуру большего размера, где она и находится в течение некоторого времени.
Хитрость здесь состоит в установлении семантического инварианта для выделения памяти. Необходимо решить, кто несет ответственность за данные в составной структуре. Что произойдет при освобождении структуры верхнего уровня? Есть три основных варианта развития событий:
1. Структура верхнего уровня также несет ответственность за освобождение любых входящих в нее подструктур. Затем эти структуры рекурсивно удалят данные, содержащиеся в них, и т. д.
2. Структура верхнего уровня просто освобождается. Любые структуры, на которые она указывает (и на которых нет других ссылок), становятся "осиротевшими".
3. Структура верхнего уровня отказывается освобождать себя, если в нее входят какие-либо подструктуры.
В этом случае выбор зависит от условий, в которых находится каждая взятая в отдельности структура данных. Однако этот выбор должен быть явным для каждого случая, и ваше решение должно реализовываться последовательно. Реализация любого из представленных вариантов на процедурном языке программирования типа С может представлять проблему: структуры данных сами по себе не являются активными. В этих условиях для каждой из основных структур предпочтительнее написать модуль, обеспечивающий стандартные средства распределения и освобождения. (Этот модуль также обеспечивает распечатку результатов отладки, преобразование в последовательную и параллельную формы и средства обхода.)
И наконец, если отслеживание ресурсов становится слишком хитрой процедурой, можно создать собственную форму ограниченной автоматической сборки «мусора», реализуя схему подсчета ссылок для ваших динамически распределенных объектов. В книге "More Effective С++" ([Меу9б]) этой теме посвящен целый раздел.
Проверка балансаПоскольку прагматики не доверяют никому, включая авторов книги, то мы полагаем, что во всех случаях неплохо было бы написать такую программу, которая осуществляла бы реальную проверку того, освобождены ли ресурсы надлежащим образом. Для большинства приложений это обычно означает создание оболочек для каждого типа ресурса и их использование для отслеживания всех распределений и освобождений. В некоторых точках программы логика диктует, что ресурсы находятся в определенном состоянии; для проверки этого и необходимо использовать оболочки.
Например, в программе, выполняемой на протяжении длительного времени и обслуживающей запросы, наверняка есть одна-единственная точка в начале основного цикла обработки, в которой происходит ожидание прихода следующего запроса. Именно в этой точке можно получить подтверждение тому, что с момента последнего выполнения цикла использование ресурсов не увеличилось.
При работе на более низком (но не менее полезном) уровне можно потратиться на инструментальные средства, которые (помимо всего прочего) проверяют выполняемые программы на наличие утечек памяти (регулярного неосвобождения области памяти). Весьма популярными являются Purify (www.rational.com) и Insure++ (www.parasoft.com).
Другие разделы, относящиеся к данной теме:
• Проектирование по контракту
• Программирование утверждений
• Несвязанность и закон Деметера
Вопросы для обсуждения
• Несмотря на то, что не существует надежных способов удостовериться в том, что вы освободили ресурсы, в этом могут помочь некоторые технологии проектирования, если их применять последовательно. В данной главе обсуждалось, как установить семантический инвариант, с тем чтобы основные структуры данных могли управлять освобождением памяти. Подумайте, как с помощью принципа «Проектирование по контракту» можно было бы усовершенствовать эту идею.
Упражнения
22. Некоторые разработчики программ на С и С++ обращают особое внимание на необходимость установки указателя в NULL после освобождения области памяти, на которую он ссылается. Почему это можно считать удачной идеей? (Ответ см. в Приложении В.)
23. Некоторые разработчики программ на языке Java обращают особое внимание на необходимость установки объектной переменной в NULL после окончания использования объекта. Почему это можно считать удачной идеей? (Ответ см. в Приложении В.)
Глава 5
Гибкость против хрупкости
Жизнь не стоит не месте.
Не могут стоять на месте и программы, которые мы пишем. Чтобы не отставать от сегодняшнего, близкого к кошмару, темпа изменений, необходимо приложить все усилия для написания программ слабосвязанных и гибких, насколько это возможно. В противном случае мы придем к тому, что наша программа быстро устареет или станет слишком хрупкой, что не позволит устранять ошибки, и может в конечном итоге оказаться в хвосте сумасшедшей гонки в будущее.
В разделе «Обратимость» говорится об опасностях необратимых решений. Мы расскажем вам, как принимать обратимые решения так, чтобы ваша программа смогла остаться гибкой и адаптируемой перед лицом нашего неопределенного мира.
В начале необходимо рассмотреть связывание – взаимозависимость между модулями программы. В разделе "Несвязанность и закон Деметера" будет показано, как сохранить отдельные концепции и уменьшить связывание.
Хороший способ сохранить гибкость – это писать программы меньшего размера. Изменение кода открывает перед вами возможность внесения новых дефектов. В разделе «Метапрограммирование» объясняется, как полностью вывести из текста программы подробности в то место, где их можно изменить безопаснее и проще.
В разделе "Временное связывание" рассматриваются два временных аспекта применительно к связыванию. Зависите ли вы от того обстоятельства, что «тик» наступает раньше, чем «так»? Если вы хотите сохранить гибкость, то нет!
Ключевым принципом в создании гибкой программы является отделение модели данных от их визуального представления, или воспроизведения. Несвязанность модели и ее визуального представления описана в разделе "Всего лишь визуальное представление".
И наконец, существует методика несвязанности модулей в еще большей степени за счет предоставления "места встречи", где модули могут обмениваться данными анонимно и асинхронно. Эта тема освещена в разделе "Доски объявлений".
Взяв эти методики на вооружение, вы можете написать программу, которая будет энергично вращаться – как в рок-н-ролле.
26
Несвязанность и закон Деметера
Хорошая изгородь – добрые соседи.
Роберт Фрост, Подготовка к выборам
В разделах «Ортогональность» и «Проектирование по контракту» мы высказали предположение, что выгодно писать «скромные» программы. Но эта «скромность» работает в двух направлениях: не раскрывайте себя перед другими и не общайтесь со слишком многими людьми.
Шпионы, диссиденты, революционеры и им подобные часто организованы в небольшие группы, называемые ячейками. Хотя отдельные личности в каждой ячейке могут знать друг о друге, они не знают ничего об участниках других ячеек. Если одна ячейка раскрыта, то никакое количество "сыворотки правды" неспособно выбить из ее участников информацию об их сподвижниках вне пределов ячейки. Устранение взаимодействий между ячейками убережет всех.
Мы полагаем, что этот принцип хорошо бы применить и к написанию программ. Разбейте вашу программу на ячейки (модули) и ограничьте взаимодействие между ними. Если один модуль находится под угрозой и должен быть заменен, то другие модули должны быть способны продолжить работу.
Сведение связанности к минимумуЧто произойдет, если появятся модули, которые знают друг о друге. В принципе ничего – вы не должны впадать в паранойю, как шпионы или диссиденты. Однако, необходимо внимательно следить за тем, со сколькими другими модулями вы взаимодействуете. Это важнее, чем то, каким образом вы пришли к взаимодействию с ними.
Предположим, вы занимаетесь перепланировкой своего дома или строите дом с нуля. Обычная организация включает "генерального подрядчика". Вы нанимаете подрядчика для выполнения работ, но подрядчик выполняет или не выполняет эти работы сам; работа может быть предложена разнообразным субподрядчикам. Но, будучи клиентом, вы не имеете дело с субподрядчиками напрямую, генеральный подрядчик берет от вашего имени эту головную боль на себя.
Нам бы хотелось воспользоваться той же моделью в программном обеспечении. Когда мы запрашиваем у объекта определенную услугу, то мы хотим, что бы эта услуга оказывалась от нашего имени. Мы не хотим, чтобы данный объект предоставлял нам еще какой-то объект, подготовленный третьей стороной, с которым нам придется иметь дело для получения необходимой услуги.
Предположим, что вы пишете класс, генерирующий график по данным научного прибора. Научные приборы рассеяны по всему миру, каждый объект-прибор содержит объект-местоположение, который дает информацию о его расположении и часовом поясе. Вы хотите, чтобы ваши пользователи могли выбирать прибор и наносить его данные на график с отметкой часового пояса. Вы можете записать
public void plotDate(Date aDate Selection aSelection) {
TimeZone tz =
ASelection.getRecorder().getLocation().getTimeZone();
...
}
Но теперь подпрограмма построения графика без особой надобности связана с тремя классами – Selection, Recorder и Location. Этот стиль программирования резко увеличивает число классов, от которых зависит наш класс. Почему это плохо? Потому что при этом увеличивается риск того, что внесение несвязанного изменения в другой части системы затронет вашу программу. Например, если сотрудник по имени Фред вносит изменение в класс Location так, что он непосредственно более не содержит TimeZone, то вам придется внести изменения и в свою программу.
Вместо того чтобы продираться через иерархию самостоятельно, просто спросите напрямую о том, что вам нужно:
public void plotDate(Date aDate, TimeZone aTz) {
...
}
plotDate(someDate, someSelection.getTimeZone());
Мы добавили метод к классу Selection, чтобы получить часовой пояс от своего имени; подпрограмме построения графика неважно, передается ли часовой пояс непосредственно из класса Recorder, от некоего объекта, содержащегося в Recorder, или же класс Selection сам составляет другой часовой пояс. В свою очередь, подпрограмма выбора должна запросить прибор о его часовом поясе, оставив прибору право получить его значение из содержащегося в нем объекта Location.
Непосредственное пересечение отношений между объектами может быстро привести к комбинаторному взрыву [28] отношений зависимости. Признаки этого явления можно наблюдать в ряде случаев:
1. В крупномасштабных проектах на языках С или С++, где команда компоновки процедуры тестирования длиннее, чем сама программа тестирования.
2. «Простые» изменения в одном модуле, распространяющиеся в системе через модули, не имеющие связей.
3. Разработчики, которые боятся изменить программу, поскольку они не уверены, как и на чем скажется это изменение.
Системы, в которых имеется большое число ненужных зависимостей, отличаются большой сложностью (и высокими затратами) при сопровождении и в большинстве случае весьма нестабильны. Для того чтобы поддерживать число зависимостей на минимальном уровне, мы воспользуемся законом Деметера при проектировании методов и функций.
Закон Деметера для функцийЗакон Деметера для функций [LH89] пытается свести к минимуму связывание между модулями в любой программе. Он пытается удержать вас от проникновения в объект для получения доступа к методам третьего объекта. Краткое содержание данного закона представлено на рисунке 5.1.
Создавая «скромную» программу, в которой закон Деметера соблюдается в максимально возможной степени, мы можем добиться цели, выраженной в следующей подсказке:
Подсказка 36: Минимизируйте связывание между модулями
А не все ли равно?
Оказывает ли следование закону Деметера (каким бы хорошим он не был с точки зрения теории) реальную помощь в создании программ, более простых в сопровождении?
Исследования [ВВМ96] показали, что классы в языке С++ с большими совокупностями откликов менее ошибкоустойчивы, чем классы с небольшими совокупностями (совокупность откликов представляет собой число функций, непосредственно вызываемых методами конкретного класса).
Рис. 5.1. Закон Деметера для функций
Поскольку следование закону Деметера уменьшает размер совокупности отклика в вызывающем отклике, то классы, спроектированные данным образом, также будут менее склонны к наличию ошибок (см. [URL 56], где приводится более подробная информация о статьях и других источниках по проекту Деметера).
Использование закона Деметера сделает вашу программу более адаптируемой и устойчивой, но не бесплатно: будучи "генеральным подрядчиком", ваша программа должна непосредственно делегировать полномочия и управлять всеми существующими субподрядчиками, не привлекая к этому клиентов вашего модуля. На практике это означает, что вы будете создавать большое количество методов-оболочек, которые просто направляют запрос далее к делегату. Эти методы-оболочки влекут за собой расходы во время исполнения и накладные расходы дискового пространства, которые могут оказаться весьма значительными, а для некоторых приложений даже запредельными.
Как и при использовании любой методики, вы должны взвесить все «за» и «против» для конкретного приложения. В проекте схемы базы данных обычной практикой является «денормализация» схемы для улучшения производительности: нарушение правил нормализации в обмен на скорость выполнения. Подобного же компромисса можно достичь и в этом случае. На самом деле, обращая закон Деметера и плотно связывая несколько модулей, вы можете получить существенный выигрыш в производительности. Ваша конструкция работает прекрасно, пока она известна и приемлема для этих связываемых модулей.
Физическая несвязанность
В данном разделе мы много говорим о сохранении логической несвязанности между элементами проектируемой системы. Однако существует взаимозависимость другого рода, которая становится весьма существенной с увеличением масштаба систем. В своей книге «Large-Scale С++ Software Design» [Lak96] Джон Лакос обращается к вопросам, касающимся отношений между файлами, каталогами и библиотеками, составляющими систему. Игнорирование этих проблем физического проектирования в крупномасштабных проектах приводит, помимо прочих проблем, к тому, что цикл сборки может растягиваться на несколько дней, а процедуры модульного тестирования могут сорвать сроки готовности всей системы. Г-н Лакос приводит убедительные доказательства того, что логическое и физическое проектирование должно осуществляться в тандеме и что устранение повреждений в большом фрагменте программы, нанесенных ему циклическими зависимостями, представляется чрезвычайно трудным делом. Мы рекомендуем вам прочесть эту книгу, если вы участвуете в разработке крупномасштабных проектов, даже если вы осуществляете реализацию на языке, отличном от С++.
В противном случае вы можете оказаться на пути к хрупкому, негибкому будущему. Или вообще оказаться без будущего.
Другие разделы, относящиеся к данной теме:
• Ортогональность
• Обратимость
• Проектирование по контракту
• Балансировка ресурсов
• Всего лишь визуальное представление
• Команды прагматиков
• Безжалостное тестирование
Вопросы для обсуждения
• Мы обсудили, как делегирование полномочий облегчает соблюдение закона Деметера и, следовательно, уменьшает связывание. Однако написание всех методов, необходимых для пересылки вызовов к делегированным классам, является утомительной процедурой, чреватой ошибками. Каковы преимущества и недостатки написания препроцессора, который автоматически генерирует эти вызовы? Должен ли этот препроцессор запускаться только единожды, или же он должен применяться как составная часть процесса сборки?
Упражнения
24. Мы обсудили концепцию физической несвязанности в последней врезке. Какой из указанных ниже файлов заголовка в языке С++ характеризуется более сильным связыванием с остальной системой? (Ответ см. в Приложении В.)
person1.h
#include «date.b»
class Person 1 {
private:
Date myBirthdate;
public:
Person1(Date &birthDate);
//...
person2.h
class Date;
class Person2 {
private:
Date *myBirthdate;
public:
25. В данном примере и примерах из упражнений 26 и 27 определите, являются ли показанные вызовы метода допустимыми с точки зрения закона Деметера. Первый пример написан на языке Java. (Ответом, в Приложении В.)
public void showBalance(BankAccount acct) {
Money amt = acct.getBalance();
printToScreen(amt.printFormat());
}
26. Этот пример также написан на языке Java. (Ответ см. в Приложении В.)
public class Colada {
private Blender myBlender;
private Vector myStuff;
public Colada() {
myBlender = new Blender();
myStuff = new Vector));
}
private void doSomething() {
myBlender.addlngredients(myStuff.elements());
}
}
27. Этот пример написан на языке С + +. (Ответ см. в Приложении В.)
void processTransaction(BankAccount acct, int) {
Person *who;
Money amt;
amt.setValue(123.45);
acct.setBalance(amt);
who = acct.getOwnerQ;
markWorkflow(who->name(), SET BALANCE);
}