[ <<< | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Источники | >>> ]
© 2001 А.И. Легалов
Возникает вполне резонный вопрос: чем отличаются друг от друга процедурное и объектно-ориентированное агрегирование? Очень многим, если следовать традиционному восприятию стереотипов. Основным отличием является то, что ОО агрегирование выстраивает в голове программиста более устойчивую ассоциацию объекта, поддержанную непосредственным размещением процедур внутри классов. Дополнительный эффект проявляется в том, что методы класса имеют на один параметр меньше, так как абстрактный характер переменных класса проявляется через их размещение в абстрактной оболочке. Практически все приверженцы ООП утверждают адекватность моделей реального мира, используемых для отображения в программные объекты, структурам, описываемым классами. Однако, этот тезис является весьма спорным. Существует множество реальных систем, более эффективные модели которых не определяются через отношения объектов, а возможность их отображения с помощью объектов связана просто с гибкостью средств программирования, позволяющих с легкостью переделывать одни понятия в другие, сохраняя при этом тем же самым результат работы: программу, функционирующую должным образом. Программирование - это философия наоборот. Философы различным образом объясняют единый мир - программисты различным образом приходят к одному и тому же результату.
Однако, наряду с отображаемыми моделями, разработчики, занимающиеся созданием сложных эволюционирующих программ, держат в своей голове множество других ассоциаций, связанных, например, с возможностями дальнейшего расширения кода без изменения уже написанных фрагментов. А обеспечить нормальную реализацию таких ассоциаций невозможно, если воспринимать моделируемые объекты только физиологически. Разделение классов на данные и процедуры как раз и позволяет вернуть гибкость в реализации. Да и вряд ли у более-менее опытного программиста возникнут проблемы с восприятием объекта реализованного без помощи класса.
Приведу простой пример. Предположим, нами разработан класс:
class simple { int v; public: simple(int val); void out(); }; |
Лично я не думаю, что его процедурная реализация менее понятна для восприятия:
struct simple { int v; }; simple *create_simple (int val); void destroy_simple(simple* s); void out (simple &s); |
Вместе с тем, отделение процедур от данных позволяет скрыть от клиента информацию о внутренней организации структуры данных и процедур ее обработки, не прибегая при этом к каким-либо специальным ухищрениям. А предоставляемое клиенту описание объекта взаимодействия определяет только интерфейс и не забивает его голову дополнительными деталями (о сокрытии информации я сейчас говорить не хочу, так как это, на мой взгляд, тема отдельного разговора):
struct simple; simple *create_simple (int val); void destroy_simple(simple* s); void out (simple &s); |
Как видите, Скотт Мейерс не зря говорит о гибкости внешних функций [Meyers] и об их способности улучшить инкапсуляцию. Следуя его же рекомендациям (весьма часто и без этого используемым в процедурном подходе, особенно тогда, когда не хочется к каждой единице компиляции подключать объемный заголовочный файл), мы можем создать несколько подмножеств аналогичных интерфейсов в различных модулях (единицах компиляции). Например:
struct simple; simple *create_simple (int val); void destroy_simple(simple* s); |
Или:
struct simple; void out (simple &s); |
И так далее. Исходные тексты этих примеров находятся в архиве simple.zip.
Естественно, что возникает ряд возражений. Одно из них связано с тем, что приходится вводить дополнительные функции создания и разрушения объектов вместо использования конструктора, обеспечивающего более наглядную запись. Однако, это, на мой взгляд, связано с издержками существующих процедурных языков. Никто не пытался перепроектировать их таким образом, чтобы создание экземпляра абстрактного типа данных ассоциировалось с операциями преобразования типа и его разрушения. Мне кажется, что не составит особого труда найти соответствующее техническое решение. При программировании на C++ можно попытаться использовать перегрузку оператора приведения типа. В конце концов, можно использовать в структуре конструкторы и деструкторы, а все остальные процедуры вынести за класс.
Другим слабым аргументом в пользу внешних процедур является то, что и в объектных моделях применяют аналогичные приемы для отделения создаваемого класса от метода его создания. В качестве примера следует привести образцы "Абстрактная фабрика" и "Фабричный метод" [Гамма]. Для того, чтобы возвращать экземпляры различных классов, в них используется дополнительная обертка над конструкторами, выполненная в виде процедур создания объектов, являющихся, правда, виртуальными методами классов.
Итак, процедурное и ОО агрегирование практически ничем не отличаются между собой, если не считать за отличие размещение процедур. Между тем, процедурное агрегирование обеспечивает более гибкую реализацию ассоциаций, связанных с конструированием эволюционирующих программ.
Обсуждая долго и нудно альтернативные методы агрегирования, нельзя не отметить, что мы говорим о двух различных способах записи, которые можно применять к одной и той же конечной реализации. То есть, наши разные внешние ассоциации зачастую несут одну и ту же конечную смысловую интерпретацию. Посмотрите на то, как, в конце концов, реализуется после трансляции невиртуальный метод класса [Голуб]. Это обычная внешняя процедура, использующая структуру данных, размещенных в классе, в качестве аргумента this! А раз все в мире так относительно, то мы можем немного еще поиграть с внешними ассоциациями, чтобы процедурное стало выглядеть как объектное.
Я стараюсь поддерживать переписку с рядом своих бывших студентов, разбросанных по всему Шару и продолжающих серьезно заниматься программированием, чтобы таким образом получать дополнительную информацию (надеюсь, что когда они встанут на ноги, то возьмут меня к себе сторожем или уборщицей:). В свое время Алексей Гуртовой, весьма серьезно занимающийся ООП и использующий его на практике в MetaCommunications, сообщил мне о статье Скотта Мейерса (которую я и перевел), а также следующей идее расширения C++, промелькнувшей в news:comp.lang.c++.moderated. При вызове внешних функций, принимающих ссылку на некоторый класс в качестве своего первого аргумента, предлагается указывать экземпляр класса не в виде параметра, а как префикс, предшествующий вызову функции. Вот дословный текст цитаты, присланной им.
Post comp.lang.c++.moderated Andrei Alexandrescu
What I think Scott's article should bring into discussion, is a future alternate syntax for uniformizing member and nonmember function calls. If Scott's article is well understood and its consequences taken seriously by the C++ community, maybe the language could allow member call syntax for nonmembers.
For instance, a nice rule would be that a nonmember that takes a reference to an object of type T as the first parameter could be invoked using member syntax, like this:
class A { ... }; void Fun(A& obj, int x, double y); A a; Fun(a, 6, 7.8); // classic call syntax a.Fun(6, 7.8); // alternate (new) syntax |
Если обратиться к ранее написанному простому примеру, то этот механизм можно проиллюстрировать следующим образом:
struct simple { int v; }; void out (simple& s) { cout << "value = " << s.v << endl; } simple s; ... out(s); // обычный вызов функции вывода s.out(); // новый (альтернативный) вариант |
Итак, все в мире относительно, что подтверждается и развитием ряда других языков. Этот же прием был использован в 1996 году для расширения языка Оберон. Так в нем появились связанные процедуры, расширившие механизм наследования виртуализацией и обеспечившие использование ООП в ранее процедурном языке. При этом Вирт и Мессенбек полностью сохранили при описании языка программирования Оберон-2 процедурную терминологию [MoessenboeckWirth]. Но и в языке, похожем на C++, можно пойти дальше того, чтобы использовать образную ассоциацию внешней функции для ее явного ОО вызова. Можно, вместо образной ассоциации, ввести конкретную синтаксическую форму, похожую на представление методов класса. Для этого достаточно "вытащить" соответствующий формальный параметр из скобок и прописать вместо него префикс класса перед именем функции. Вот, как это может выглядеть на предыдущем простом примере:
struct simple { int v; }; extern void simple::out() { cout << "value = " << v << endl; } simple s; ... s.out(); // только новый вариант вызова |
Получается, что любая внешняя функция может прикинуться методом класса, не являясь, на самом деле, таковой. То, что это внешняя функция, а не метод, определяется ключевым словом extern. Увидев его, транслятор не станет "кричать" о том, что Вы забыли указать в классе этот метод. Можно даже добавить ключевое слово static, которое локализует использование данной функции текущей единицей компиляции:
struct simple { int v; }; // ограничение использования текущим модулем extern static void simple::out() { cout << "value = " << v << endl; } simple s; ... s.out(); // только в текущем модуле |
Хотелось бы еще раз напомнить, что я не занимаюсь ревизией C++. Приводимые примеры, скорее всего, являются намеками для тех, кто занимается разработкой собственных языков. Переделкой стандартов пусть занимаются соответствующие комитеты. Поэтому, вполне допустимо, что в предлагаемом синтаксисе существуют противоречия с C++. Основное, что хотелось бы показать в этой работе, это возможность использования других конструкций. Хотя, я думаю, что большинству C++ программистов все равно, как писать внешнюю функцию и с чем ее образно ассоциировать.
В заключении хочу еще раз отметить, что, по моему разумению, объектно-ориентированное агрегирование ничего не дает, по сравнению с обычным процедурным агрегированием. Более того, фиксация процедур в классах уменьшает гибкость при разработке эволюционирующей программы. Наличие же в языке различных методов создания программных объектов (с добавлением к уже существующим способам представления тех, которые были описаны выше) ведет к разбуханию языка программирования. И здесь я делаю первый намек на то, что от языков, отображающих одни и те же понятия разными способами, надо переходить к инструментам, позволяющим отображать множественные отношения и ассоциации между базовыми понятиями. Захотели, посмотрели на программные объекты как на процедуры и данные, захотели - посмотрели как на классы. А разработку таких понятий также можно вести с любых позиций и парадигм.
[ <<< | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Источники | >>> ]