разработка информационных систем
Banner  
О нас О нас
Наши продукты Наши продукты
Clip CLIP
R2D2 R2D2
Статьи Статьи
Контакты Контакты
Карта
English English

Мысли вслух

Статьи о программировании


Статьи об управлении, бухгалтерском и управленческом учете, принципах и подходах в создании информационных систем управления (ИСУ).

Сравнение ОО-моделей некоторых популярных языков


Этот кусок "выдран" из большого анализа, так что если где попадутся "несуразности" - не обращайте на них внимание.

Рассмотрим в негативном смысле (т.е. чем плохи существующие языки). Мы сравниваем с языками C++, Java, CLU. Мы не рассматриваем более традиционные объектно-ориентированные языки, такие как Smalltalk или Objective C, так как они ориентированны на модель передачи сообщений, а не на процедурный вызов, поэтому, на наш взгляд, не могут иметь сравнимую эффективность при работе в едином адресном пространстве (конечно, без специальной аппаратной поддержки).

1.1. Объектно-ориентированные свойства

Основными такими свойствами являются, на наш взгляд, инкапсуляция структуры объекта и отделение интерфейса от реализации.

1.1.1. Язык C++.

Язык C++, при всех своих достоинствах, никак не стимулирует эти свойства - в нем предполагается, что при почти любом использовании объекта компилятор должен иметь полную информацию о структуре объекта и всех его подобъектах. В результате, когда иерархия классов достигает некоторого критического размера (зависящего, конечно, от мощности используемой техники), компиляция любого модуля C++ программы приводит к включению потрясающего количества header-файлов и мучительно медленной работе. Печальным примером такого рода может служить библиотека TurboVision из поставки Borland C++.

Можно сказать, что это прямое следствие гносеологической модели C++ и в рамках классического C++ данное противоречие почти невозможно разрешить. Прекомпиляция header-файлов нескольно смягчает ситуацию, но радикально не решает проблему.

Совершенно неприемлемым является то, что любое изменение private-членов и особенно private-методов класса приводит к необходимости перекомпиляции всех модулей, использующих этот класс. Зто прямое нарушение инкапсуляции.

В общем, на наш взгляд, язык C++ спроектирован с явным креном в сторону зффективности объектного кода в ущерб объектным свойствам и общей производительности системы программирования.

1.1.2. Язык Java

С точки зрения объектно-ориентированности гораздо лучше, чем C++, но сохранение доступности части информации о структуре объекта в виде public-членов класса говорит само за себя.

1.1.3. Язык CLU

Является наиболее объектно-ориентированным из рассматриваемых, осуществляет полное скрытие структуры объекта и отделение интерфейса от реализации. Доступ к членам объекта возможен только через операции интерфейса.

1.2. Наследование

1.2.1. Язык C++

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

1.2.2. Язык Java

На наш взгляд, Java имеет самую удобную модель наследования - одиночное для классов и множественное для интерфейсов. При этом красиво разрешается большинство противоречий, свойственных множественному наследованию. Кроме того, понятие "интерфейс" интуитивно соответствует понятию "свойство" так же, как "класс" соответствует "объекту". А поскольку многие сущности реального мира гораздо легче представить как "свойства", а не как "объекты", то такое представление является и более точным и удобным.

1.2.3. Язык CLU

CLU в силу своего возраста не имеет модели наследования - вместо нее используется параметризация типов.

1.3. Полиморфизм

C++ использует неявный (неописуемый средствами языка) полиморфизм для базовых типов, а также позволяет переопределять большинство операций.

Java использует только неявный полиморфизм для базовых типов, мотивируя это тем, что переопределение операций при бессистемном использовании ухудшает читаемость программы. Это, безусловно, так, однако полное отсутствие возможности переопределения операций сильно затрудняет написание полноценных типов и увеличивает разрыв между типами "первого" и "второго" сорта.

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

1.4. Параметризация типов

C++ (как и C) использует неявную (встроенную в язык и неописуемую средствами языка) параметризацию для типа "массив", а также для указателей. Кроме того, новые версии используют развитую систему параметризации с помощью шаблонов (template), которая неявно компилирует параметризованный класс с каждым новым значением типа-параметра.

Java использует только неявную параметризацию для типа "массив".

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

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

1.5. Исключительные ситуации

C++(в современном варианте), Java и CLU имеют вполне удобные средства управления исключительными ситуациями.

1.6. Эффективность

1.6.1. Язык C++

C++ здесь вне конкуренции - он явно заточен на получение максимальной эффективности объектного кода, и превзойти его практически невозможно.

1.6.2. Язык Java

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

Интерпретирующий характер Java-машины, естественно, снижает скорость операций с объектами. Компиляция "на лету" выравнивает ситуацию, но порождает проблемы в других областях - например, невозможно разделение кода программы между несколькими одновременно работающими модулями ( shared-библиотеки )

1.6.3. Язык CLU

На операциях с объектами имеет скорость на уровне "C++ с виртуальными функциями", т.е. с использованием дополнительного уровня косвенной адресации.

1.7. Модель использования памяти

1.7.1. Язык C++

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

Единственная автоматизация, предлагаемая C++ - автоматический вызов деструкторов объектов, что позволяет достаточно надежно управлять выделением/удалением свободной памяти.

1.7.2. Языки Java и CLU

Java и CLU предлагают решение проблемы памяти - сборщик мусора. Конечно, это кардинальное решение. Но, с другой стороны, это лобовое решение, и поэтому имеет в общем случае большие накладные расходы. Конечно, если у вас в единоличном распоряжении современный компьютер ( с простаивающим, как правило, процессором ), и в отдельном витке запущен сборщик мусора, то скорее всего вы и не заметите никакого сбора мусора. Если же задача работает в системе с разделением времени, то приходится это время экономить. Интерактивная программа, периодически зависающая для сбора мусора, не очень приятная вещь. Кроме того, непредсказуемость момента и длительности сборки мусора не позволяет использовать его в системах, требующих гарантированного времени отклика (реального времени). Опять же, с другой стороны, сборщики мусора как правило повышают степень локализованности ссылок, что очень желательно для систем с виртуальной памятью.

Системы со сборщиками мусора используются очень давно, и, однако же, не завоевали окончательно сердца программистов. Поэтому, на наш взгляд, полностью полагаться на сборку мусора по крайней мере нескромно.

В общем, хотелось бы иметь возможность выбирать модель управления памятью из наработанных (стандартных).

1.8. Использование классов в виде динамических библиотек

C++ никак не стимулирует динамическое связывание, и даже, можно сказать, активно этому противится. В результате приходится для создания динамической библиотеки делать "обертку" из C-функций, что начисто лишает привлекательности использование C++ с/в динамических библиотеках.

Java грузит байт-код динамически, а также позволяет подключать C-level dll, но для этого надо явно описывать вызов загрузчика библиотеки.

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

1.9. Реализация в рамках C-машины

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

Кроме того, иногда необходима возможность "опуститься на уровень C" для достижения максимальной эффективности. Известно, что в большинстве программ (мы не имеем в виду, например, ядро операционной системы) действует правило 90/10 т.е. 90% кода (а в программах с развитым пользовательским интерфейсом, наверное и 99%) не являются "узким местом" в смысле эффективности. Однако, такие узкие места все-таки существуют, и для их кодирования желательно использовать максимально эффективный язык, т.е. C.

Для связи с системными и другими библиотеками также необходим легкий доступ к C коду.

Кроме того, компиляторы C доступны на любой платформе и сильно оптимизированны.

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

C++ является "расширением" C, т.е. построен не в виде нового уровня абстракции, а в качестве расширения существующего. Это, по сути, то же самое что, например, строить пользовательскую программу в виде "расширения" операционной системы (как это принято в MS-DOS). Всем известно, что из этого получается. Кто-то сказал, что классы C++ больше похожи не на "черный ящик", а на "открытый электрощит" - если повезет и достаточно знаний, можно подключиться очень эффективно, но вполне может и током шибануть. Следствием увеличения сложности системы (а не уменьшения, как должно быть при переходе к новому уровню абстракции) всегда является повышенная трудоемкость и пониженная надежность, не говоря уже о непомерном увеличении сложности компилятора. Может быть, в этом причина того, что C++ так медленно "завоевывает" мир - ему уже около 15 (!!) лет, а некоторые вполне очевидные вещи вроде обработки исключений только-только начинают в нем широко использоваться. Для сравнения, язык Java, как более логичный и построенный в виде отдельного уровня абстракции, распостранился не в пример быстрее.

1.10 Реализация в виде транслятора в байт-код.

В принципе, такая реализация возможна, как показывает Java, однако, на наш взгляд, является слишком тяжеловесной, дублирующей и неизбежно ограничивающей возможности языка.

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

Такими ограничениями являются: отказ от средств определения классов, отказ от статического контроля типов, поддержка во время выполнения контроля типов и классов памяти объектов, изменение структуры программы c "классово-ориентированной" на "модульно-ориентированную".

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

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

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


© Павел Лазарев, paul at itk dot ru, 2001
© ООО "Инженерно-Техническая Компания" (ИТК) 2006
426072, Удмуртская республика, Ижевск а/я 1247, uri at itk dot ru