Последний раз обновлено 27.06.01
Понятно, что объектная модель зависит от того, как ее будут реализовывать. Если язык программирования не позволяет автоматизировано использовать основные механизмы модели, или при составлении модели не учитывали возможность наследования или полиморфизма, то создание модели будет затруднительно. При образовании видов на объект надо учитывать, что эти виды могут быть потом описаны.
Предложенное здесь изложение С++ не претендует на его описание. Если у вас есть проблемы с синтаксисом или вы хотите узнать тонкости описания на С++, то вам надо использовать книгу по С++. Я думаю, что мне нет особого смысла переписывать оттуда сюда. Здесь даны частные случаи, которые хотя и не всегда работают, но которые нельзя обойти при программировании, мне так кажется.
Данное нами определения объекта - "набор свойств с названием" годится для работы с абстрактной объектной моделью, т.к. в ней допускается любое описание объекта. Но чтобы использовать модель для программы, объекты должны быть созданы. Объекты могут быть описаны на основе уже имеющихся. Наследование, это новый механизм образования объекта из уже имеющихся. Рассмотрим известные механизмы в сравнении и с точки зрения логической, ассоциативной связи:
Если вид Х на объект А позволяет считать объект А объектом Х, то создание объекта отличается от простого конструирования А из Х ( если вы вложили и запекли в пироге яблоко, то пирог удастся выдать за яблоко только слепому или голодному, или если расковырять пирог ).
Этому механизму реализации связи между объектами следует уделить особенное внимание. Во-первых, он не встречается в предыдущих моделях, во-вторых, по уверениям специалистов, именно он и приводит ко всем замечательным преимуществам ООП перед другими способами программирования.
Для описания класса в С++ применяют ключевое слово class
. Возможны и другие объявления, с помощью struct
и union
, но я думаю, что нет необходимости их использовать, т.к. это добавляет похожесть класса на структуру или объединение С, что плохо, а экономит лишь одну или две строки в описании класса.
Как уже говорилось, класс С++ описывает не сообщения, а методы их реализации, поэтому он часто явно определяет и механизм реализации ассоциативной связи.
За разным синтаксисом обращения к переменной и указателю скрыт тот простой факт, что переменная размещается в памяти компилятором автоматически, а указатели ссылаются на каким-то образом уже размещенные, в т.ч. самим компилятором, переменные и возможно изменение адреса памяти, использование другой переменной во время работы программы. Указатель позволяет создать шаблон алгоритма для работы с множеством объектов: списком или массивом.
Ссылка позволяет в некотором блоке считать переменную, может быть адресуемую указателем, одной и той–же в течение всего блока, и значит можно перейти к синтаксису обычной переменной, без подробностей размещения в памяти.
Синтаксис доступа как к переменной не влияет на механизм связывания сообщения с методом, а лишь на возможность или нет сменить реальный объект, на который указывает имя переменной. Т.е. если написано a.work()
, а не a->work()
, то это не означает, что в первом случае будет использовано раннее связывание, т.к. класс объекта известен на этапе компиляции.
Можно вообще допустить, что вместо трех видов доступа ( как к переменной, как к ссылке и как к указателю) для объекта идет доступ либо по ссылке (синтаксис как с обычной переменной), либо по указателю (подробности размещения в памяти). Но это допущение касается только доступа к свойствам объекта. Объект и ссылка на него это в целом совсем не одно и тоже.
Для того чтобы объявить новый класс заодно объектом некоторого другого класса, т.е. потомком базового класса, нужно указать базовый класс в описании потомка. В потомке можно как определить свои методы реализации сообщений базового класса, так оставить методы предка.
Ошибочно думать, что переопределенное свойство просто уничтожает, встает на место базового. Объект содержит в себе все свойства и базового класса, и явно определенные. При этом может возникнуть ситуация, когда имени одного сообщения соответствует более одного метода его реализации, т.е. имеет место несколько форм реализации сообщения, полиморфизм. Свойства с одинаковым именем и типом могут быть также определены как в базовом, так и в производном классе. Правила подбора свойства или метода описаны далее.
Шаблон кода, который получает параметр как ссылку на объект базового класса, может успешно работать и с классом потомком. Доступ к объекту потомку при этом происходит через указатель или ссылку на базовый класс.
Было бы неплохо, чтобы доступ к свойствам был различный, что позволит иметь свойства, ошибочное использование которых запрещено автоматически. В С++ существует три уровня доступа к свойству, которые можно применить к любому виду на объект, т.е. в одноименном уровне доступа С++ могут находится свойства разных наших видов. Это уровни:
public
- открытый, свойства доступны для всех
protected
- защищенный, свойства доступны для классов-потомков
private
- только для собственных свойств объекта
В контексте классов, различают доступ к объекту через:
public
секции вложенного объекта. К объекту другого класса объявленному вне описания класса, в программе, такой же доступ.
public
и protected
полей базового класса. Т.е. protected
расширяет обычный доступ специально для механизма наследования. Для любого объекта этого же (но не базового) класса (не только к тому, для которого вызван метод) такой же доступ. Интересно, что для методов из протокола класса на доступ не влияет то, как наследуется базовый класс: открыто или закрыто.
Несмотря на то, что класс является потомком, для методов из протокола класса доступ к ссылке на объект базового класса (не к собственным свойствам унаследованным от базового класса, которые доступны через this
или другой объект своего класса) происходит именно через ссылку на экземпляр базового класса.
При доступе к свойствам базового класса через ссылку на класс потомка, эти свойства открыты в зависимости от того, как потомок наследует свой базовый класс. Если потомок объявляет private
базовый класс, то не доступны сообщения и свойства базового класса даже из секции public
.
В наследнике можно изменить доступ к сообщениям базового класса, произвольным образом переопределяя их в новых уровнях. Если базовый класс наследуется как private
, то доступ к его public
сообщениям и свойствам в объекте наследнике возможен через указатель на базовый класс (Пример 1).
Пример 1
Области видимости при наследовании
Описание классов |
|
применение |
|
Есть способ игнорировать разграничения доступа, при доступе через экземпляр класса или протокол базового класса, описанные выше. Это использовать объявление в протоколе класса, к которому происходит доступ, тех классов или функций ( именно функций, т.к. они определены прямо в программе ) для которых этот доступ будет разрешен. Это нужно, если класс или функция работает сразу с двумя классами, которые не связаны между собой, но требуется доступ к защищенным свойствам. Я знаю несколько типовых применений этих свойств:
Как уже говорилось, при наследовании возможна ситуация, когда одному сообщению соответствует несколько методов. Компилятор должен как-то отличать метод с одинаковым именем одного класса от другого. По аналогии с А-моделью, можно ввести искажение имени, вставив в имя метода название класса, в котором он определен. То же можно сделать и с любым свойством класса. Это делается с помощью оператора указания контекста имен свойства ::
Tclass_name::property_name
Для свойств без явного задания контекста используется контекст имен по умолчанию. Все неоднозначности в вызове метода реализации сообщения можно снять, всегда явно указывая контекст имен используемого свойства.
Очевидно, что шаблон, работающий со ссылкой на объект некоторого класса, не может прибегнуть к такому способу, иначе, когда будет использоваться ссылка на потомка этого класса, невозможно будет вызвать переопределенные в потомке методы. Шаблон использует пространство имен по умолчанию, в надежде на то, что компилятор подставит нужный метод сам. Подбор свойства, таким образом, сводится к управлению пространством имен по умолчанию.
Как уже говорилось, класс С++ включает в себя описание части механизмов реализации связей объектов. В частности, для механизма наследования, надо задать для свойства правила подбора пространства имен по умолчанию.
Свойства объекта используются либо внешним, относительно объекта, кодом, либо собственными методами:
Приход сообщения в объект класса приводит к исполнению метода. Можно изменить подбор метода принятый по умолчанию (по типу ссылки на объект) на такой: по типу реального объекта. Это достигается путем объявления метода как virtual
. Начиная с этого класса, все классы, которые переопределяют метод этого сообщения, автоматически делают свой метод virtual
. Все методы во всех потомках для этого сообщения образуют виртуальный кластер.
Сообщение, отправленное как от внешнего кода, так и от внутреннего метода вызовет метод соответствующий реальному объекту во время выполнения. Здесь имеет место позднее связывание. Методы этого сообщения всех других классов из линии наследования доступны при явном указании контекста имен.
Свойства, те которые не сообщения, нельзя объявить как virtual
. Это означает, что внутренние методы, имеющие контекст имен по умолчанию как свой тип, уже на этапе компиляции свяжутся с таким свойством, и если переопределить его в потомке, то методы потомка будут работать с другой копией свойства, которая для них по умолчанию. Внешний код будет использовать свойство соответственно типу ссылки.
В некоторых случаях так и надо, но такое свойство не может содержать сообщения или свойства, которые надо вызывать соответственно типу объекта, а не указателя, т.е. нельзя переопределить virtual
сообщение в потомке, если оно расположено внутри некоторого подобъекта.
Может оказаться так, что группа сообщений по логике модели окажется в некой подгруппе, но хочется сделать так, чтобы эти вложенные сообщения были бы на правах сообщений непосредственно описанных в классе. Это можно сделать, если эти логически созданные сообщения, имея не виртуальные относительно контейнера методы, будут вызывать виртуальные методы с другими именами и описанными непосредственно в классе. Здесь будет просто преобразование имен.
Часть свойств можно сделать общими для всех объектов некоторого класса. Это могут быть также и методы. Для метода потребуется дополнительный параметр - ссылка на объект класса, чтобы иметь доступ к не статическим свойствам конкретного объекта. В протоколе класса статическое свойство описывается как static
.
Можно сделать для свойства некоторое подобие virtual
метода, используя для доступа к свойству указатель на него, определенный только в одном классе. В этом случае, каждый класс в линии наследования может иметь свою копию свойства, и просто устанавливает указатель на свою копию. Те методы, как внешние, так и внутренние, которые используют доступ к свойству через такой указатель, получают копию свойства реального объекта.
Конструктор - создает объект из ничего (конструктор по умолчанию) или из других объектов этого (конструктор копирования) или других (конструктор преобразования) классов. Объект создается, значит вызывается конструктор, при объявлении объекта, с помощью new
, при использовании временной переменной или при преобразовании одного типа в другой.
Конструктор вызывает перед своим выполнением конструкторы всех базовых классов от самого первого. Конструктор не может быть виртуальным, не имеет возвращаемого значения и конструкторов может быть много. Конструктор может находиться в защищенной части. Внешние методы не смогут его вызвать, и создать объект таким конструктором. Если базовый класс не имеет конструктора по умолчанию, но имеет другие виды конструкторов, то в конструкторе потомка надо указать явно, какой конструктор базового класса использовать.
Конструктор копирования используют, когда хотят, чтобы копировались, например, не указатели на свойства, а реальные свойства.
Конструкторы с параметрами объектов других типов служат для преобразования этих типов в данный тип. Альтернативой конструктору некоторого класса, для которого просто не может быть известно всех будущих классов, используется оператор преобразования типа, который определяется в этом неизвестном будущем классе.
Конструктор это не оператор для выделения памяти, в свете чего его многие пытаются рассматривать. Он нужен для создание объекта из чего-нибудь, в том числе и из ничего. Его можно рассматривать как аналог оператора преобразования одного типа в другой, а не оператора new
.
В противовес ему, деструктор удаляет объект, выполняя все необходимые операции по освобождению занятых ресурсов: освобождает память, закрывает файлы и т.п. Деструктор может быть виртуальным, если надо, чтобы был вызван деструктор соответствующий типу реального объекта, что чаще всего необходимо, а не типу ссылки.
Деструктор класса последовательно вызывает деструкторы базовых классов от себя в направлении самого первого базового класса автоматически. Поэтому в потомке, как правило, нужно освободить только те ресурсы, которые занимает конструктор именно этого класса, а не всех предков.
Деструктор вызывается автоматически при удалении объекта с помощью delete
или когда авто объект выходит из области определения. В последнем случае, вызов деструктора может произойти когда угодно, даже при выходе из программы и в произвольном порядке, в соответствии со стратегией сборки "мусора" ( удаленных автоматических объектов ) в вашем компиляторе и операционной системе. Может это и не совсем точно, но лучше не делать никаких предположений о порядке вызова деструктора для авто объектов компилятором, и, соответственно, не включать зависимый от этого код в деструктор. Другими словами, компилятор гарантирует лишь что такой ресурс как "память" будет освобожден правильно, но если вывыделяете еще какие либо ресурсы, которые зависят от очередности создания и удаления объектов, то компилятор не может ими правильно управлять.
Перечисленные выше правила подбора метода реализации сообщения осуществляются автоматически. Может возникнуть вопрос, возможен ли подбор метода нужного базового класса, "автоматический switch" по какому-то другому параметру, не только по типу ссылки на объект или типу объекта. Ответ нет. Правда, я слышал, что существует какое-то расширение Borland C++, которое позволяет в качестве параметра использовать целое, но я это никогда не использовал.
Проблема подбора метода базового класса возникает также при конвейерной обработке сообщения методами по линии наследования. Если потомок полностью переопределяет метод базового класса, то такой проблемы нет. Подробнее об этом в разделе "Полиморфизм".
Описание базового класса может иметь сообщения, для которых не существует метода их реализации. Методы таких сообщений описывают с помощью чисто виртуальных функций, а сообщения, если хотите, можно называть абстрактными сообщениями, а класс называют абстрактным. Нельзя создать объект такого класса. Этот класс может иметь методы для других сообщений, тех которые не абстрактные.
Такой абстрактный базовый класс используется для того, чтобы либо сосредоточить описание интерфейса, т.е. для объявления сообщений, либо описать сообщения для нескольких принципиально разных классов, общие методы реализации для которых выделить затруднительно. Чтобы не определять бессмысленных пустых методов, т.к. класс С++ описывает методы, а не сообщения и по-другому задать наличие сообщения, кроме как описав метод, нельзя.
Абстрактный базовый класс, состоящий только из чисто виртуальных функций, не является аналогом описания объекта из пространства сообщений, потому что определяет неявно механизм использования такого класса - наследование и виртуальные методы, что не оговаривается для пространства сообщений.
Как и другие два способа создания объекта: вложение и прямое описание, наследование - это механизм создания объекта. Как и вложение, наследование позволяет создавать объект на основе уже имеющихся объектов, но обладает рядом отличий.
Можно сказать о нескольких видах наследования:
Возможны три варианта наследования: два перечисленные выше и оба вместе. В С++ наследование абстрактного базового класса, где все сообщения чисто виртуальные, означает наследование интерфейса, наследование закрытого базового класса означает наследование реализации (объект производного класса не будет объектом базового класса), а наследование открытого базового класса означает одновременно наследование и интерфейса, и реализации.
Самое главное, на что сейчас следует обратить внимание, что наследованием можно задавать общность для принципиально разных объектов, позволяя считать их объектом одного, именно базового, класса, и, соответственно, использовать их в одном шаблоне, который работает с базовым классом (не учитываем разницу в реализациях, важна возможность использования в одном шаблоне). Это наследование интерфейса.
Интересно такое утверждение: наследование позволяет описать производный класс в терминах отличий от базового. Например, при наследовании реализации и интерфейса вместе можно сохранить интерфейс и описать отличия реализации, указывая методы заменяющие методы реализации старых сообщений.
Итого: для наследования интерфейса безразличны различия в реализациях объектов базового и производного классов, так как оно не несет в себе никакой реализации, а задает общность, ну а наследование реализации выгодно использовать тем больше, чем меньше отличия в реализации производного и базового класса.
Наследование реализации более сильная связь, чем наследование интерейса, и любое наследование более сильная связь, чем вложение, при котором вложенный объект, как правило, ничего не знает о том, кто его контейнер.
Дополнительные отличия добавляет следующая особая форма наследования - множественное.
Такое наследование, при котором у одного класса более одного базового класса перечислены в описании класса, т.е. объединяются несколько линий наследования. Наличие множественного наследования не привносит никакой путаницы при имеющейся модели, но сильно увеличивает повторное использование кода. Создается возможность образовать такой вид на объект, при котором результат будет отражать свойства обоих предков, т.е. можно создать что-то совершенно невообразимое в контексте каждого из свойств. Можно соединить корабль и цветочный горшок, причем его можно будет использовать и как горшок в комнате, и как корабль в океане, если кто-то может себе такое представить.
Такое объединение нескольких классов может использоваться как для локализации в классе потомке связи между двумя базовыми абстракциями и ее обработки, так и для добавления потомку "фоновых" свойств, которые позволят, например, откликаться на какие-то сообщения, обработка которых в объекте не производится.
Альтернативой множественного наследования может быть явное задание интерфейса, который будет транслировать вызовы во вложенный объект. Недостатком такого подхода является невозможность отдельного задания абстракций, невозможность использования шаблона кода, работающего с базовым классом и потомком и прочее.
Множественное наследование позволяет независимо разрабатывать разные методы или группы методов одного базового класса, всегда имея работоспособный объект и, затем, объединять их, эти методы, в одном классе.
Часть побочных эффектов множественного наследования рассмотрены в разделе "Виртуальный базовый класс". Описание класса при множественном наследовании сложнее, чем при простом, но это плата за дополнительное повторное использование кода.
При множественном наследовании возможна ситуация, когда имеется несколько копий состояний одного из базовых классов, полученных независимым наследованием. Именно из-за этой ситуации множественное наследование и не любят.
Что получится при такой ситуации? Те не статические свойства, которые определены в этом дублированном базовом классе будут иметь свои копии для каждого из потомков. Во многих случаях, это правильно. Методы разных объектов одного класса есть одно и тоже, и если они работают с private
свойствами своего класса и методами базовых классов, то множественное наследование на них не влияет.
Виртуальные методы такого дублированного базового класса потребуют явного переопределения в потомке, так же как и те не виртуальные методы, которые вызываются через ссылку на потомка.
Наибольшую сложность могут вызвать те свойства, которые должны быть, по логике объекта, общими для обоих копий дублированного базового класса, но не общими для всех объектов этого класса, т.е. некий аналог статических свойств, но глобальных только для всех базовых классов этого потомка. Эта глобальность может быть логической и ее отсутствие не даст ошибки во время компиляции, но приведет к неправильным результатам или невозможности использования такого класса в шаблоне.
Можно сказать, что если создавать класс, который потом можно будет использовать для множественного наследования, то надо придерживаться определенных правил в описании такого класса, в описании public
и protected
свойств. Видимо отсюда пошла неприязнь к свойствам не сообщениям в открытой части протокола класса. Если в открытой части протокола только методы, то они глобальные для всех объектов в силу своей природы, поэтому открытые свойства пытаются одеть в методы доступа.
Чтобы сделать свойства не методы общими при множественном наследовании, базовый класс с этими свойствами можно наследовать как virtual
во всех повторяющихся классах. Проблема в том, что обычный потомок не знает, что кто-то когда-то возьмет и потребует, чтобы он наследовал свой базовый класс как виртуальный, а для самого обычного потомка это не требуется. В своих программах, чтобы не корректировать ранее определенный код, что не всегда и возможно, и не наследовать все классы виртуально, лучше в том классе, который поддерживает множественное наследование себя в дальнейшем, все открытые(public
, protected
) свойства не методы, которые должны быть общими для всех возможных в рамках объекта-потомка копий базовых классов, вынести в отдельный виртуальный базовый класс.
При множественном наследовании, в отличие от обычного, надо указать явно конструкторы всех тех виртуальных базовых классов, которые принадлежат к дублированным базовым классам и не имеют конструктора по умолчанию. Хотя конструкторы виртуальных базовых классов могут быть указаны в конструкторах других базовых классов, будет вызван только вариант конструктора, указанный в реально создаваемом объекте. Здесь имеет место аналогия с виртуальным методом, когда для одного виртуального базового класса имеется несколько вариантов вызова конструктора.
На самом деле, я затрудняюсь указать все правила, которые гарантируют безопасное использование методов базовых классов в общем случае. Но в ряде частных случаев, множественное наследование позволяет облегчить себе жизнь.
Одним из них является случай, когда определяется структура сообщений с помощью абстрактного класса, поддерживающего множественное наследование, создаются шаблоны работающие со ссылками на такой объект. Этот абстрактный класс имеет несколько видов, методы которых используют друг друга, например: доступ к потоку, перемещения по просмотру, и методы последнего вида вызывают первый. Каждый вид совершенствуется независимо. Реальный объект наследует по одному варианту каждого из видов. Можно создать шаблон класса такого реального объекта, в качестве параметров которого выступают классы каждого из видов.
Альтернативой такому способу не является наследование реальным объектом только абстрактного базового класса, вложение каждого вида и определение методов абстрактного класса как вызывающих метод одного из видов, т.к. имеет место взаимодействие видов, и необходимо, чтобы они находились в одном контексте. При вложении неудобно делать конвейерную обработку сообщений методами других классов. Вид может иметь абстрактные методы и его нельзя будет создать как объект.
Конечно, можно создать такой вариант, когда подобъект будет вызывать методы другого подобъекта через контейнер, но это громоздко, хотя и не требует множественного наследования. Иногда, если реальное создание методов вида базового класса происходит во время выполнения по параметру конструктора, применение множественного наследования также затруднительно.
При достаточно большой иерархии классов, на практике, реальное нормальное использование методов базовых классов возможно только при наличии удобного браузера методов, и правил создания метода, которые гарантируют их применимость во всех потомках.
Чтобы разбавить слова реальным кодом, есть ряд примеров с множественным наследованием: (Пример 2,Пример 3). Более осмысленные примеры применения множественного наследования можно найти в реализации "просмотра файла с памятью".
Пример 2
Не виртуальные базовые классы при множественном наследовании
Описание классов |
|
Использование |
|
Результаты выполнения |
|
Пример 3
Виртуальные базовые классы при множественном наследовании
Описание классов |
|
Использование |
|
Результаты выполнения |
|
Вернемся к нашей объектной модели. Как преобразовать сообщение из пространства сообщений, в сообщение класса С++? Можно ли по виду на объект пространства сообщений прямо получить механизм? Какой механизм лучше с точки зрения сопровождения и эффективности программы?
Избежать этих вопросов при написании программы нельзя. Мы не можем во время программирования вдаваться и в сложности понятий отношения, абстракции и т.д. Нам, как пользователям метода ООП, лучше использовать готовые простые критерии по выбору способа образования. Если это задается произвольно, зачем тогда вообще это разделять? ООП это разделяет.
Можно сказать, что этот выбор зависит от имеющихся базовых объектов, а базовые объекты заданы декомпозицией задачи. Но можно задать вид на базовые объекты как угодно произвольно. Объекты контекста задачи вполне могут иметь любые общие базовые классы, что, как кажется, противоречит логике контекста задачи.
Механизмы образования объектов: часть 2
Рассмотрим ряд известных утверждений, призванных ответить, является ли часть объекта, группа свойств, базовым классом или вложенным объектом. Разница есть, иначе бы эти механизмы были бы одним и тем же. Надо перечислить эти отличия, которые и будут критерием выбора.
Что означает понятное определение? Это означает, что его можно перефразировать, уточнить любое слово, сохранив смысл. Вот замечательные по своей сути определения, которые каждый может найти:
Первое просто утверждает, что если объект не базовый класс, то он вложен. Но вопрос не в этом, это утверждение очевидно, если исключить повторное описание. Именно не базовый ли класс мы и хотим узнать. Неявно используется то, что мы все видели собак и хвосты и считаем, что в базовых объектах "псовые" заложено то, что хвост не является собакой, а собака это "псовое". Это кажется очевидным, что хвост не собака, а собака не хвост, но, в общем случае, это далеко не очевидно и как раз это то и представляет интерес.
Если с хвостами и другими органами животных все может и понятно, то как быть с дверями? Куда деть двери в комнате: в "комната" является "строением" и содержит "двери" или в "комната" является "строением с дверями" и содержит "ничего"? Или вот. Файл Х типа является файлом, но может еще являться базой данных, а может включить ее как одно из своих полей-свойств, что выбрать при создании типа?
За приведенным примером с собаками, пытаются скрыть то, что скрыто за словом "является", а второе утверждение просто ставит в недоумение, не приводя примеры, как раскрыть слово "есть".
Проиллюстрируем еще. У того же Буча ( пример 2 ) есть пример с кошкой, который я уже приводил, в котором кошка взглядом патологоанатома и бабушки выглядит по разному, и для бабушки шерсть "есть" кошка, а не кошка "содержит" часть кошки - шерсть, как для патологоанатома.
Так что же получается, можно использовать как вложение, так и наследование? Это так. Когда у вас нет уже заранее обоснованной модели, (обоснованной условиями задачи, имеющимися базовыми классами или еще чем-нибудь, т.е. когда вы уже, фактически, определились с тем, что использовать для создания) тесты с "хвостами" и "перестановками А и В" не годятся, потому как они не дают однозначного и легко проверяемого критерия.
Как другими словами заменить или уточнить "является" и "есть". Этого достаточно для выбора, если это не "является" или "есть", тогда вместо наследования используем вложенность.
"Является" и "есть" (как мне подсказали) это означает примерно следующее: "не обладает или обладает незначительными отличиями". Но для выбора механизма, а наследование это механизм, этих условий мало: можно создать объект и с помощью вложения, который будет отличаться незначительно, тоже "являться". Нам лучше из логической сферы перейти в сферу техническую и выяснить: в каких случаях наследование нельзя заменить на вложенность никак и в каких случаях трудоемкость создания класса механизмом наследования меньше.
Я могу предложить следующие критерии:
namespace
) Формирование пространства имен, выделяя логическую группу сообщений, полученных от модели, от пространства сообщений, из области имен объекта по умолчанию.
Таким образом, тот факт, что мы вынесли наследование и вложение в механизмы, говорит о том, что:
В частности, если нет использования объекта цветок и роза в одном шаблоне не обязательно их наследовать, даже если для вас это очевидно "является" и они используют часть общих методов. Использование наследования дает более сильную связь, чем вложение, а разделение общего кода можно реализовать иначе. Необоснованное использование наследования даже для задания интерфейсов может привести к сложной и запутанной системе классов.
Механизмы образования объектов: часть 2
Мы разделили создание модели на две части: логическую структуру и механизмы, которые ее реализуют. Пространство сообщений задает логические группы сообщений и логические связи между абстракциями. Чтобы получить программу, надо выделить некоторые абстракции как класс С++ и использовать определенные механизмы для его описания. Скорей всего, теоретически, существует самый оптимальный вариант описания классов и создания программы для данной задачи. Но поиск бесконечно оптимального варианта может занять бесконечно много времени и сил, лучше найти такой инженерный подход, который по заданной оптимальности даст минимальные затраты.
Было бы хорошо, чтобы механизм имел однозначное соответствие с логической структурой, чтобы можно было описывать программу в логических действиях. Но три известные нам механизма перекрываются в некоторых случаях, т.е. можно использовать любой и нельзя просто указать обоснованную различимость. Сейчас важно пока отметить те случаи, когда эти механизмы не перекрываются, заметить области, в которых выбор механизма определяется однозначно. Эти условия перечислены в предыдущем разделе в пунктах 1 и 2 для наследования и вложения и помечены как отличительные. Если ассоциативной связи нет и вы не пытаетесь построить один объект на основе описания другого, то используется простое прямое описание.
В тех случаях, где выбор механизма неозднозначен, его надо выбирать из условий заданной эффективности: наименьшее время создания класса, быстрота работы методов объекта и прочее. На данный момент мы не можем этим заниматься и вынуждены в этих случаях выбирать механизм произвольно. Такое сравнение механизмов требует большего, чем предполагается у читателя, опыта создания и реализации объектных программ и, я полагаю, выходит за рамки данной работы. Можно сказать только общие слова: применение наследования в этих случаях тем более обосновано, чем менее производный класс отличается от базового, т.е. наследование позволяет описывать производный класс в терминах отличий от базового.
Хочу еще обратить внимание на то, что если в пространстве сообщений группа сообщений выглядит как вложенный объект, то это не означает, что надо обязательно применить механизм вложения, т.е. нахождение или не нахождение сообщения в пространстве имен объекта по умолчанию слабо влияет на выбор механизма. Глядя на логический формат сообщения для объекта нельзя сказать, являются ли реальные методы группы сообщений наследуемыми или вложенными. В большинстве случаев возможно преобразование пространства сообщений как в наследование, так и во вложенность.
Вложенный объект имеет не полиморфичные, относительно контейнера сообщения, даже если собственные методы вложенного объекта объявлены как virtual
, но он может транслировать их в полиморфичные, относительно контейнера, сообщения самого контейнера с произвольными именами, которые можно назвать сообщениями реализации модели. При этом при создании вложенного объекта используется ссылка на контейнер.
Мы уже вплотную приблизились к способам задания модели на С++. Я надеюсь, что теперь только надо узнать как мы хотим описать то, что хотим получить, для чего надо открыть книгу по С++. Часть особенностей, которые показались мне важными, перечислены в следующем разделе "Объектная модель и С++".
Может вызвать неясность, как объектная модель связана с А-моделью, которая в свою очередь связана с С-моделью. Объектная модель задана аксиоматически, но легко заметить, что объект можно, в некотором приближении, считать АТД с добавлением операции наследования. Приближение используется потому, что в предложенном задании О-модели, наследование не является абсолютно необходимой чертой, а относится к разряду механизмов и стоит на равне с вложением и прямым описанием. Функция и переменная тоже не полностью соответствуют сообщению, методу и свойству объекта, однозначной связи между ними нет.
На самом деле, существует некоторый разрыв между АТД и объектом, разрыв этот создан, может быть, искусственно, специально используя другие термины и прочее. Но он действительно отражает тот факт, что объект против АТД, как абстрактная вещь против надстройки над структурой С, против технического приема программирования, решают разные задачи и формируются разными способами. Объект самодостаточен, элементарен и не требует разложения на какие-то другие элементарные составляющие, как надо для АТД. Объект сам есть элементарная составляющая, а не сборка из более мелких. Объект может быть выражен как угодно в программе, через структуры, функции, переменные - это не имеет значения.
Такое разделение О-модели на информационное взаимодействие ( пространство сообщений ) и реализацию взаимодействия ( механизмы ), на целевые и вспомогательные объекты весьма спорно и может забуксовать для многих применений. К тому же, не разрешился вопрос о формализации видов на объект.
Но это модель для обучения. Она, вероятно, не самая непротиворечивая, но может использоваться и для перехода от простого структурно-алгоритмического программирования к программированию с использованием средств ООП ( для чего она и создавалась ), и для практического решения ряда задач. Полезность и недостатки ее, скорее всего, можно определить только при реальном использовании, а не теоретически.
При переходе от структурного к объектному составлению программ надо ясно представлять себе отличия структурной модели от объектной и знать способы получения как той, так и другой. Понимание этих различий является самым трудным шагом при переходе к объектному методу создания программ. Все что было до этого написано, было написано именно для того, чтобы подчеркнуть эти отличия. Так как все равно остается последовательность действий, которая и есть программа, то структурные навыки тоже могут применяться, но, возможно, что это просто плохой стиль работы с объектами.
От того, что эта тема вынесена отдельно из "Отличие структурной модели от объектной" и поставлена после механизмов объектной модели можно догадаться о том, что эти механизмы являются частью отличий.
Объект можно считать АТД, который в свою очередь является модулем. Но объект, как модуль, имеет более сложную структуру и развитые механизмы реализации, чем модули в структурной модели. Вся работа с О-моделью сосредоточена на выборе структуры модуля и механизмов ее реализации. Поэтому я и говорил об О-модели как о новом уровне типизации для структурной модели. Хотя разделение на модули контекста задачи для обоих моделей может быть похоже.
Объектная модель | Модульная структурная модель |
Позволяет задать сложную иерархическую структуру модуля, описывая один модуль в терминах других модулей. Например, вызвать для модуля А функцию х() модуля В, который вложен в модуль А: А.В.х() | Из-за нетипизированности модуля нельзя сосредоточить описание модуля в одном типе, в одном месте и использовать его в остальных модулях, как их часть, ссылаясь на имя типа. В языке С можно ухищрениями с #include и #define добиться чего-то подобного, но в серьез мы это рассматривать не будем.
|
Оперирует модулями как элементарными единицами программы. Основными операциями над модулями являются: выбор структуры модулей и механизмов ее реализации. | Оперирует структурами данных и имеет модули простой структуры. Основными операциями являются: выбор структур данных и задание последовательности действий для работы с ними. |