Выбрать главу

Предположим теперь, что к объекту типа B, достижимому через сущность типа A, применяется статическое связывание. При этом из-за того, что соответствующая версия процедуры rA , как правило, не будет поддерживать необходимый инвариант (как, например, depositACCOUNT1 для объектов типа ACCOUNT2 или displayWINDOW для объектов типа BUTTON), будет получаться неверный объект (например, объект класса ACCOUNT2 с неправильным полем balance или объект класса BUTTON, неправильно показанный на экране).

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

Суммируем: статическое связывание является либо оптимизацией, либо ошибкой. Если его семантика совпадает с семантикой динамического связывания (как в случаях (1) и (2)), то оно является оптимизацией, которую может выполнить компилятор. Если у него другая семантика, то это ошибка.

Подход языка С++ к связыванию

Учитывая широкое распространение и влияние языка С++ на другие языки, нужно разъяснить, как в нем решаются некоторые из обсуждаемых здесь вопросов.

Соглашения, принятые в С++, кажутся странными. По умолчанию связывание является статическим. Чтобы процедура (в терминах С++ - функция или метод) связывалась динамически, она должна быть специально объявлена как виртуальная (virtual).

Это означает, что приняты два решения:

1 Сделать программиста ответственным за выбор статического или динамического связывания.

2 Использовать статическое связывание в качестве предопределенного.

Оба нарушают ОО-разработку ПО, но в различной степени: (1) можно попробовать объяснить, а (2) защищать трудно.

По сравнению с подходом этой книги (1) ведет к другому пониманию того, какие задачи должны выполняться людьми (разработчиками ПО), а какие - компьютерами (более точно, компиляторами). Это та же проблема, с которой мы столкнулись при обсуждении автоматического распределения памяти. Подход С++ продолжает традиции C и дает программисту полный контроль над тем, что случится во время выполнения, будь то размещение объекта или вызов процедуры. В отличие от этого, в духе ОО-технологии стремление переложить на плечи компилятора все утомительные задачи, выполнение которых вручную приводит к ошибкам, и для которых имеются подходящие алгоритмы. В крупном масштабе и на большом промежутке времени компиляторы всегда справятся с работой лучше.

Конечно, разработчики отвечают за эффективность их программ, но они должны сосредотачивать свои усилия на том, что может действительно существенно повлиять на результат: на выборе подходящих структур данных и алгоритмов. За все остальное несут ответственность разработчики языков и компиляторов.

Отсюда и несогласие с решением (1): С++ считает, что статическое связывание, как и подстановка кода, должно определяться разработчиками, а развиваемый в этой книге ОО-подход полагает, что за это отвечает компилятор, который будет сам оптимизировать вызовы. Статическое связывание - это оптимизация, а не выбор семантики.

Для ОО-метода имеется еще одно негативное последствие (1). Всегда при определении процедуры требуется указать политику связывания: является она виртуальной или нет, т.е. будет связываться динамически или статически. Такая политика противоречит принципу Открыт-Закрыт, так как заставляет разработчика с самого начала угадать, что будет переопределяться, а что - нет. Это не соответствует тому, как работает наследование: на практике может потребоваться переопределить некоторый компонент в далеком потомке класса, при проектировании которого нельзя было это предвидеть. При подходе С++, если разработчик исходного класса такого не предусмотрел, то придется снова вернуться к этому классу, чтобы изменить объявление компонента на virtual. При этом предполагается, что исходный текст доступен для модификации. А если его нет, или у разработчика нет права его менять, то вас ожидает горькая участь.

По этим причинам решение (1), требующее, чтобы программисты сами задавали политику связывания, мешает эффективному применению ОО-метода.

Решение (2) - использовать статическое связывание в качестве предопределенного - еще хуже. Очень трудно подобрать доводы в его пользу с точки зрения проектирования языка. Как мы видели, выбор статического связывания всегда приводит к ошибкам, если его семантика отличается от динамического. Поэтому не может быть никаких причин для его выбора в качестве предопределенного.

Одно дело - сделать программистов, а не компиляторы ответственными за оптимизацию в безопасных случаях (т.е. попросить их явно указывать статическое связывание, если они считают, что это корректно), но заставлять их писать нечто специальное, чтобы получить корректную семантику - это совсем другое. Если верно или неверно понятые соображения эффективности начинают брать верх над основополагающим требованием корректности ПО, то что-то не в порядке.

Даже в языке, заставляющем программиста отвечать за выбор политики связывания (такое решение принято в C), предопределенное значение должно быть противоположным. Вместо того, чтобы требовать объявлять динамически связываемые функции виртуальными (virtual), язык должен был бы использовать динамическое связывание по умолчанию и разрешить программистам выделять словом static (или каким-нибудь другим) компоненты, для которых они хотели бы запросить оптимизацию, доверив им самим (в традиции C и С++) удостоверяться в том, что она допустима.

Это различие особенно важно для начинающих, которые, естественно, имеют тенденцию доверять значениям по умолчанию. Даже для языка, менее страшного, чем С++, нельзя предполагать, что кто-либо сразу справится со всеми деталями наследования. Ответственный подход к этому должен гарантировать корректную семантику для новичков (и вообще, для разработчиков, начинающих новый проект, которые "хотят чтобы прежде всего он был правильным, а уж затем быстрым"), а затем предоставить возможности оптимизации для тех, кому это требуется и кто хорошо разбирается в предмете.

Имея в виду широко распространенный интерес к "совместимости снизу - вверх", создание комитета для изменения политики связывания в С++, особенно пункта (2), будет тяжелым делом, но стоит попытаться пролить свет на опасность нынешних соглашений.

Прискорбно, но подход С++ влияет и на другие языки, например, политика динамического связывания в языке Borland Delphi, продолжающем прежние расширения Паскаля, по сути, та же, что и в С++. Отметим все же, что вышедший из недр С++ язык Java в качестве базового использует динамическое связывание.

Эти наблюдения позволяют дать некоторый практический совет. Что разработчик может сделать при использовании С++ или иного языка с той же политикой связывания? Самым лучшим для разработчиков, не имеющих возможности переключиться на другие средства или ждать улучшений в этом языке, было бы объявлять все функции как виртуальные и тем самым разрешить их любые переопределения в духе ОО-разработки ПО. (К сожалению, некоторые компиляторы С++ ограничивают число виртуальных функций в системе, но можно надеяться, что эти ограничения будут сняты).