Эти цифры наводят на размышления о сложности языка. Согласно приведен-
ным данным, Modula-2 немного сложнее, чем ее предок, Pascal; оба и стоят на одном уровне с языком С. C++ задумывался как расширение С и сопровождался руководством в стиле С. C++ v3.0 добавил девятнадцать ключевых слов к имевшимся в традиционном С, что увеличило его на две трети. Справочное руководство по C++ v3.0 содержало 155 страниц, в четыре раза больше, чем руководство по С. Эти две оценки наводят на мысль, что C++ v3.0 значительно сложнее, чем С. Проект стандарта ANSI C++ 1995 составлял примерно 650 страниц. Это опять в четыре раза больше по сравнению с 1990 годом. И хотя этот документ включает большое количество дополнительного материала по улучшенной стандартной библиотеке, его объем все же отражает большую сложность языка. Более того, многие конструкции C++ независимы, так что их взаимодействие значительно влияет на сложность.1
Подтверждающий пример можно найти на стр. 306 книги Эллис (Ellis) и Страус-трупа (Stroustrup) “С+ + Annotated Reference Manual”1, где для показа различных вариантов и свойств разных типов функций используется таблица размером 13 на 5. Пятью характеристиками функции являются: наследуемость, виртуальность, возвращаемый тип, дружественная ли это функция или функция-член и генерируемость по умолчанию. Например, конструкторы, деструкторы и функции преобразования не могут объявлять свой возвращаемый тип, a new() и delete () должны объявлять, причем void* и void соответственно. С, в действительности, имеет лишь одну форму семантики функции. Такое увеличение в шестьдесят пять раз внушает благоговейный трепет. И хотя в этой таблице присутствует стройность и многие характеристики могут быть получены из концептуального понимания модели языка, авторы, тем не менее, считают целесообразным перечислить различия и свойства разных типов функций.
В C++ ключевые понятия перегружаются несколькими значениями (смыслами). Это приводит к путанице в понятиях. Кандидатом в самые злостные нарушители представляется ключевое слово static. Может существовать локальная статическая переменная, то есть переменная, сохраняющая значение при выходе из блока. Допустим статический идентификатор с областью видимости файла, то есть имя, видимость которого ограничивается этим файлом. Может существовать статический член данных класса, который является общим для всех объектов данного класса. Разрешается задать статическую функцию-член, то есть функцию-член, которая не получает аргумент-указатель this. Между всеми этими значениями-смыслами существует связь, но в то же время они настолько различны, что довольно трудно правильно понять тот факт, что они родились из общей концепции.
Успех ООП на C++
Объектно-ориентированное программирование на C++ завоевало ослепительный успех, несмотря на известные недостатки. Причина в том, что C++ привнес в промышленность приемлемую технологию ООП. Он основан на существующем, удачном и широко используемом языке. Он позволяет писать переносимый, компактный и эффективный код. Сохранена безопасность типов, а расширяемость типов является всеобъемлющей. C++ сосуществует с другими распространенными языками и не предъявляет особых требований к системе.
С изначально создавался как язык для разработки операционных систем, и как таковой позволяет писать код, который легко транслируется для эффективного использования машинных ресурсов. Благодаря этой эффективности программные продукты приобретают существенные преимущества. Поэтому, несмотря на жалобы на то, что традиционный С не является безопасным и надежным языком, С развивался в своей сфере применения. Сообщество программистов на С использовало структурное программирование и АТД-расширения на основе специальных соглашений и правил. ООП вторглось вэто профессиональное сообщество лишь когда оно (ООП) «поженилось» с языком С в рамках концепции, которая поддерживала и традиционную точку зрения С, и преимущества ООП. Ключом к популярности C++ было понимание того, что наследование и полиморфизм дают важные дополнительные преимущества по сравнению с традиционной практикой программирования. Полиморфизм в C++ позволяет клиенту использовать АТД как черный ящик. Успех ООП характеризуется той степенью, в которой пользовательский тип может быть сделан неотличимым от собственного типа языка. Полиморфизм позволяет задать приведения типов, которые интегрируют АТД с собственными типами. Он разрешает объектам из иерархии подтипов динамически реагировать на вызов функции; в этом заключается принцип сообщений в ООП. Полиморфизм также упрощает клиентский протокол, а «размножение» имен управляется перегрузкой функций и операторов. Наличие всех четырех форм полиморфизма вдохновляет программиста на проектирование с учетом инкапсуляции и сокрытия данных.
ООП много значит для многих людей. Все попытки дать ему определение напоминают старания слепых мудрецов описать слона. Напомним нашу формулу, определяющую объектно-ориентированность:
ООП = расширяемость типов + полиморфизм.
Во многих языках и системах ценой сокрытия деталей была неэффективность на этапе выполнения и чрезмерная жесткость (негибкость) интерфейса. C++ предлагает ряд вариантов, обеспечивающих и гибкость, и эффективность. Как следствие, этот язык будет иметь все больший и больший успех в промышленности.
Платонизм: проектирование «tabula rasa»
C++ дает программисту инструмент для реализации объектно-ориентированного проекта. Но как вы разрабатываете такой проект? Не бывает простой методики tabula rasa1, то есть проекта «на ровном месте». Любой проект всегда должен быть тесно привязан к предметной области и отражать ее абстракции. Раскрыть эти абстракции позволяет философия проектирования, которую мы называем платонизмом (platonism).
Всоответствии с платонической парадигмой существует идеальный объект. Например, представьте идеальный стул и попытайтесь описать его характеристики. Это должны быть свойства, разделяемые всеми стульями
Вселенной. Такой стул может быть подкатегорией другого идеала — мебели. Стул может иметь подкатегории, такие как вращающийся стул, шезлонг, стул с подлокотниками, кресло-качалка и тому подобное. Для толкового описания стула, возможно, потребуется устроить экспертизу по стульям, с привлечением производителей и пользователей стульев с целью прийти к соглашению о сути и природе «стульности». Платонический стул должен легко модифицироваться для описания наиболее часто встречающихся стульев. Платонический стул следует описывать в терминах, согласовывающихся с существующей «стульной» терминологией. На C++ оказал влияние Simula 67 — язык, специально разработанный для моделирования. Платоническая парадигма является моделированием или симуляцией конкретного мира. Она несет в себе дополнительные возможности по формированию объектно-ориентированного проекта программного продукта. Объектно-ориентированное проектирование обычно предоставляет открытый интерфейс, который является удобным, обобщенным и эффективным. Эти важные моменты могут вступать в противоречия. Еще раз отметим, что нет простых правил, позволяющих разрешить подобные конфликты.
Дополнительные возможности могут оказаться очень выгодными и существенно перекроют увеличение стоимости начального проекта. Первое и главное: они налагают дополнительный уровень дисциплины на процесс программирования. Усиление такой дисциплины всегда приносит дивиденды. Второе: идеологически связанные Друг с другом, наполненные смыслом куски кода инкапсулируются в классы. Инкапсуляция и декомпозиция (разбиение на составляющие) тоже всегда приносили свои плоды. Третье: совершенствуется повторное использование кода с помощью наследования и АТД. Повторное использование кода всегда выгодно. Четвертое: улучшается прото-типирование посредством откладывания выбора реализации и предоставления доступа к большим и удобным общим библиотекам. Дешевые прототипы всегда окупаются.
Платоническая парадигма применения техники ООП проводит бархатную революцию во всем процессе программирования. Она не вытесняет более раннюю технику, например приемы структурного программирования, используя их для программирования «в малом», чтобы потом эффективнее управлять композицией большие программных проектов.
Принципы проектирования
Как правило, при программировании следует использовать уже имеющиеся наработки. Например, в математическом и научном сообществе существуют стандартные определения для комплексных и действительных чисел, матриц и многочленов. Каждое из них можно легко закодировать, представив в виде АТД. Ожидаемое открытое поведение этих типов принимается всеми математиками.
Сообщество разработчиков обладает обширным опытом по использованию стандартных контейнерных классов. Приемлемые соглашения существуют в отношении поведения стека, ассоциативного массива, двоичного дерева, очереди. Кроме того, у сообщества программистов есть много примеров специализированных языков программирования, ориентированных на конкретную проблемную область. Например, язык SNOBOL и выросший из него язык ICON имеют очень богатые возможности для обработки строк. Подобные возможности могут быть собраны в виде АТД в C++.
Полезным принципом проектирования служит принцип «лезвия Оккамы1» (Occam's razor). Этот принцип утверждает, что не следует вводить новые сущности сверх необходимости — или сверх законченности, обратимости, ортогональности, согласованности, простоты, эффективности и выразительности. Подобные идеалы могут конфликтовать, что часто приводит к неизбежной «торговле» между ними, когда дело доходит до реализации проекта.
Обратимость (invertibility) означает, что программа должна иметь функции-члены, обратные по отношению друг к другу. Для математических типов сложение и вычитание являются обратными операциями. В текстовом редакторе ими служат добавление и удаление элементов текста. Некоторые операции сами себя обращают, например, операция отрицания. В нематематическом контексте важность обратимости можно понять на примере блестящего успеха команды «Отменить» (Undo) в текстовых редакторах и команд восстановления в менеджерах файлов.
Законченность (completeness) лучше всего рассматривать на примере булевой алгебры, где единственной операции «и-не» достаточно для конструирования всех возможных булевских выражений. Но при изучении булевой алгебры в качестве основных операций обычно рассматриваются отрицание, логическое умножение (конъюнкция) и логическое сложение (дизъюнкция). Самой по себе законченности недостаточно для того, чтобы судить о проекте. Больший набор операторов часто и более выразителен.
Ортогональность (orthogonality) —это принцип, который гласит, что любой элемент проекта должен интегрироваться и работать со всеми остальными элементами без перекрытий и избыточности. Например, система, которая оперирует геометрическими фигурами, должна включать операции горизонтального и вертикального перемещения и операцию поворота. Их достаточно, чтобы расположить фигуру в любом месте экрана.
Иерархичность (hierarchy) достигается с помощью наследования. Проекты должны быть иерархичны, что является отражением двух принципов — декомпозиции и локализации. Оба они служат для сокрытия деталей — ключевой идеи в борьбе со сложностью. Однако при подобном проектировании существует серьезная проблема. Какой детализации достаточно для того, чтобы сделать сущность пригодной в качестве класса? Важно избежать разрастания излишне детализированных сущностей. Избыточные детали слишком усложняют управление проектом классов.
Схемы, диаграммы и инструменты
Процесс проектирования можно облегчить с помощью диаграмм. Существует несколько систем обозначений (нотаций) для объектно-ориентированного проектирования. Некоторые из них используются автоматизированными системами разработки программ (computer assisted software engineering — CASE-средства). Опишем две схемы, которые мы находим полезными. Первая из них — это CRC-карточки (Budd, 1991), а вторая — диаграммы Вассермана-Пирчера, (Wasserman et. al., 1990; в качестве альтернативы — Booch, 1995).
CRC означает класс, ответственность, сотрудничество (class, responsibility, collaboration). Ответственность — это обязательства, которые должен выполнять класс. Например, объекты комплексных чисел должны предоставлять реализацию комплексной арифметики. Сотрудник (collaborator) — это другой объект, который взаимодействует с данным для обеспечения некоего общего набора поведений. Например, целые и действительные числа сотрудничают с комплексными для предоставления исчерпывающего набора математических поведений.
CRC-карточки используются для проектирования заданного класса. Сначала описываются ответственность класса и его сотрудники. Оборотная сторона карточки применяется для описания деталей реализации. Лицевая сторона карточки соответствует открытому поведению.
CRC-карточка
