Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 185
Текст из файла (страница 185)
В определенных проектах наследование действительно может и не давать существенного выигрыша. Однако для больших проектов политика «никакого наследования» приводит лишь к менее ясным и гибким системам, в которых якобы отсутствующее наследование симулируется при помощи иных языковых конструкций. В дальнейшем сами программисты вынуждены будут применять наследование для улучшения кода, несмотря на отсутствие его в проекте системы. В результате, проектная политика «никакого наследования» приведет лишь к тому, что в итоге наследование будет встречаться в программе несогласованным, лоскутным образом, а единого согласованного проекта всей системы не получится. 24.2З. Игнорирование возможностей статической проверки типов Приуменьшение или игнорирование роли статической проверки типов часто обосновывается рассуждениями вроде того, что «типы — это артефакты языка программирования», «естественнее думать об объектах„а не о типах» и «статическая проверка типов вынуждает нас слишком рано задумываться о деталях реализации».
На такие «мелочи», как проверка типов, действительно можно не обращать внимание на ранних стадиях проектирования и анализа. В то же время, классы и классовые иерархии также небесполезны на стадии проектирования: они помогают спе- 24.2 Проектирование и язык программирования цифицировать концепции, выяснить их взаимосвязи и уточнить их смысл. По мере продвижения в разработке проекта эта точность все более обретает форму все более точных спецификаций классов и их интерфейсов.
Важно понять, что точно определенные и строго типизированные интерфейсы являются фундаментальным инструментом проектирования. Язык С-ь+ разрабатывался с учетом этого обстоятельства. Строго типизированный интерфейс гарантирует, что согласованные куски кода могут быть откомпилированы и скомпонованы так, что это позволяет им делать обоснованные предположения друг о друге. Выполнение этих предположений гарантируется системой типов. В свою очередь, это уменьшает необходимость в интенсивном тестировании, упрощая тем самым фазу интеграции проекта, над которым работает множество людей. Интеграция не стала важнейшей частью настоящей главы лишь потому, что уже накоплен большой положительный практический опыт интеграции систем со строго типизированными интерфейсами. Рассмотрим следующую аналогию.
В физическом мире мы соединяем друг с другом различные приборы, и кажется, что число стандартов на разъемы соединений бесконечно. Очевидно, что разъемы специально спроектированы так, чтобы было невозможно соединить два устройства в том случае, если они не предназначены для этого, а соединения осуществлялись лишь надлежащим образом. Например, вам не удастся подсоединить электрическую бритву к высоковольтной сети. Если бы вам это удалось, результатом была бы обуглившаяся электробритва или труп ее владельца.
Много усилий потрачено на то, чтобы гарантировать невозможность соединения частей аппаратуры, не предназначенных для этой цели. Иногда вместо несовместимых разъемов в аппаратуру встраивают внутреннюю защиту, например предохранители от скачков напряжения. Более высокую степень защищенности обеспечивает сочетание несовместимости разъемов и дополнительной встроенной динамической защиты от непредвиденных отклонений в режимах использования устройств.
В мире программ аналогия почти полная. Статическая проверка типов равносильна совместимости на уровне разъемов, а динамические проверки сродни динамической защите электрических цепей. Как в физическом мире, так и в программном мире, если не сработали оба вида проверок — жди беды. В больших системах нужно использовать оба вида проверок. Согласно рассмотренной в 523.4.3 схеме этапов проектирования информация о типах возникает на этапе 2 (возможно, после поверхностного рассмотрения на этапе 1) и становится определяющей на этапе 4. Статически проверяемые интерфейсы являются главной гарантией совместной работы программного кода, разработанного разными группами людей.
Документация на интерфейсы (включая точную спецификацию всех вовлеченных типов) служит основным средством общения между разными группами программистов. Такие интерфейсы являются одним из важнейших результатов проектирования системы, и на них фокусируется общение между проектировщиками и программистами. Игнорирование статической проверки типов при рассмотрении интерфейсов приводит к проектам, затуманиваюшим структуру программы и откладывающим обнаружение ошибок до стадии выполнения программы.
Например, интерфейс можно определить в терминах самоидентифицируюших себя объектов; 856 Глава 24. Проектирование и программирование УУ Пример, предполагающий динамическую проверку типов, а не статическую: Яеаск зг УУ стек может. содержать указатели на объекты любых ьтров коЫ3'() ( з.ризя(пею ЯааЬ900) г з.риза(пею БааЬ37В); В.рор() ->гайеоз3г(); УУотлично: ВааЬ37В - это самолет з.рор() ->гакео77(); УУгипы!те еггою авто не может взлететь ) Недостаточная точность определения интерфейса (функции Вгасй::рпз)г()) привела к необходимости динамической проверки вместо статической. Стек з предназначен для хранения самолетов, но в коде это явным образом не отражено, так что теперь пользователь должен сам гарантировать соблюдения этого требования. Более точная спецификация — шаблон плюс виртуальные функции вместо негарантированной динамической проверки — позволяет выявить ошибку на стадии компиляции: Вгасй<ргапе*> з; УУ 3(аск может содержат~ только указатели на самолеты коЫ3'( ) з.ризЬ (пезг КааЬ900); УУ еюогч БааЬ900 - не самолет з.ризЬ (пеы ЯааЬ37В); В.рор () ->гааеоЯ'() 7 УУ чудесно: ЗааЬ37В - это самолет з .
рор ( ) - >!айео13" ( ); Этот вопрос рассматривался также в З)б.2.2. Разница в производительности для статической и динамической проверок может быть значительной (в 3-5 раз хуже для динамической проверки). Не следует, однако, бросаться в другую крайность. Исключительно статическими проверками невозможно выявить все ошибки. Например, очевидно, что никакими статическими проверками невозможно зашититься от аппаратных сбоев. Посмотрите также пример из В25.4, (, из которого видно, что полная статическая проверка не всегда достижима.
Все же нужно стремиться к тому, чтобы большинство интерфейсов для типов уровня приложения проверялись статически; см. з24.4.2. Иная проблема состоит в том, что идеальный с абстрактной точки зрения проект может породить проблемы из-за того, что не принимает во внимание ограниченные возможности базового средства — языка программирования, в данном случае С++. Например, функция 1(), которой требуется выполнять операцию пзгп г1яйг() над своим аргументом, может делать это только при условии обшности типов ее аргументов; с!аез Р1апе ( УУ ... коЫ гигп г10Ь(() 857 24.2.
Проектирование и язык программирования с(авв Саг ( // ... юЫ гигп г/дат(); )' гоЫ/" (Х* р) ( р->гиги гтддт(); /... ) // каков должен быть тип Х? Некоторые языки (вроде Бша11(а1)( или С(.ОБ) позволяют использовать два типа взаимозаменяемым образом (если у них операции одинаковые), связывая их через общий базовый класс и откладывая разрешение имени до выполнения программы. Язык С++ намеренно выполняет это с помощью шаблонов, разрешая имена во время компиляции.
Нешаблонные функции допускают разные типы аргументов, только если они неявным образом приводятся друг к другу. Таким образом, в предыдущем примере Х должен быть общим базовым классом для Р!але и для Саг (то есть общим базовым классом для управляемых транспортных средств). Как правило, примеры, инспирированные идеями из языков, чуждых С++, могут отражаться в нем путем явного отражения неявных допущений. Например, для классов Р!але и Саг в отсутствие у них общей базы можно создать такую классовую иерархию, которая позволит нам все-таки передавать функции Г'(Х" ) объекты, содержащие Саг или Р(але (825.4.1), что впрочем весьма непросто.
Шаблоны обычно помогают встраивать в код на С++ чуждые для этого языка идеи. Несоответствие проектных идей возможностям С++ обычно порождает неестественный и неэффективный код, который трудно понимать и поддерживать. Несоответствие техники проектирования языку реализации можно сравнить с дословным переводом с одного естественного языка на другой.
Например, английский язык с немецкой грамматикой так же неуклюж, как немецкий с английской, и оба могут оказаться непонятными для человека, владеющего лишь одним из них. ,Классы в программе являются конкретными представлениями концепций проекта. Таким образом, делая менее четкой разницу между классами, мы затуманиваем ключевые концепции проекта. 24.2.4. Отказ от традиционного программирования Программирование весьма дорого и непредсказуемо по сравнению со многими другими видами деятельности, а результирующий код не на 100% надежен. Трудоемкость ручного программирования высока — большая часть задержек серьезных проектов происходит как раз из-за неготовности кода.
Почему бы тогда совсем не отказаться от программирования как вида деятельности? Для многих руководителей избавление от наглых, недисциплинированных, ненадлежащим образом одетых, слишком высокооплачиваемых и слишком увлекающихся техническим жаргоном программистов кажется безусловным благом. Для программистов это может показаться абсурдным. Однако на самом деле имеется возможность избавиться от традиционного программирования в ряде важных прак- 858 Глава 24. Проектирование и программирование тических областей, автоматически сгенерировав конечный код исходя из одних лишь высокоуровневых спецификаций.
Или можно генерировать код, отталкиваясь от показанных на мониторе компьютера изображений геометрических фигур. Последнее типично для проектирования графического интерфейса пользователя, когда с помошью автоматизированных средств можно получить готовый код во много раз быстрее, чем при традиционном ручном программировании. Аналогично, схема базы данных и доступ к этим данным согласно имеюшейся схеме намного проще и быстрее запрограммировать автоматически на основе формальных спецификаций, чем писать для этого код на С++ или иных языках программирования обшего назначения.