Реализация объектной модели

Последний раз обновлено 27.06.01


Содержание

Понятно, что объектная модель зависит от того, как ее будут реализовывать. Если язык программирования не позволяет автоматизировано использовать основные механизмы модели, или при составлении модели не учитывали возможность наследования или полиморфизма, то создание модели будет затруднительно. При образовании видов на объект надо учитывать, что эти виды могут быть потом описаны.

Предложенное здесь изложение С++ не претендует на его описание. Если у вас есть проблемы с синтаксисом или вы хотите узнать тонкости описания на С++, то вам надо использовать книгу по С++. Я думаю, что мне нет особого смысла переписывать оттуда сюда. Здесь даны частные случаи, которые хотя и не всегда работают, но которые нельзя обойти при программировании, мне так кажется.


Механизмы образования объектов: часть 1

Реализация объектной модели

Данное нами определения объекта - "набор свойств с названием" годится для работы с абстрактной объектной моделью, т.к. в ней допускается любое описание объекта. Но чтобы использовать модель для программы, объекты должны быть созданы. Объекты могут быть описаны на основе уже имеющихся. Наследование, это новый механизм образования объекта из уже имеющихся. Рассмотрим известные механизмы в сравнении и с точки зрения логической, ассоциативной связи:

  1. Прямое повторное описание
    Ассоциативной связи нет. В ряде случаев, объекты могут иметь схожие интерфейсы, но имеет место случайное совпадение, оно не потребует синхронного сопровождения, одинакового кода, и в дальнейшем совпадение может пропасть.

  2. Вложение. Отношения объектов "part of", "часть"
    Ассоциативная связь есть. Вложение - стандартный механизм образования типа для АТД или простой структуры, когда на основе ранее определенных типов получается новый, при этом ранее определенные типы рассматриваются как некие поля. Возможно несколько независимых полей одинаковых типов. Объект, содержащий вложенный объект, назовем объектом-носителем или, по просьбам трудящихся, контейнером, как это общепринято.

  3. Наследование. Отношения объектов "is-a", "является"
    Ассоциативная связь есть. Механизм наследования образует связь более сильную, чем вложение. При вложенности, полученный объект не будет объектом, который в него вложили, а при наследовании, полученный класс будет заодно и базовым классом. Сделать два объекта одинаковыми для шаблона удобно и обязательно механизмом наследования.

Если вид Х на объект А позволяет считать объект А объектом Х, то создание объекта отличается от простого конструирования А из Х ( если вы вложили и запекли в пироге яблоко, то пирог удастся выдать за яблоко только слепому или голодному, или если расковырять пирог ).


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

Реализация объектной модели

Этому механизму реализации связи между объектами следует уделить особенное внимание. Во-первых, он не встречается в предыдущих моделях, во-вторых, по уверениям специалистов, именно он и приводит ко всем замечательным преимуществам ООП перед другими способами программирования.

Описание класса

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

Для описания класса в С++ применяют ключевое слово class. Возможны и другие объявления, с помощью struct и union, но я думаю, что нет необходимости их использовать, т.к. это добавляет похожесть класса на структуру или объединение С, что плохо, а экономит лишь одну или две строки в описании класса.

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

Переменная, указатель и ссылка

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

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

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

Синтаксис доступа как к переменной не влияет на механизм связывания сообщения с методом, а лишь на возможность или нет сменить реальный объект, на который указывает имя переменной. Т.е. если написано a.work(), а не a->work(), то это не означает, что в первом случае будет использовано раннее связывание, т.к. класс объекта известен на этапе компиляции.

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

Описание наследования

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

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

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

Шаблон кода, который получает параметр как ссылку на объект базового класса, может успешно работать и с классом потомком. Доступ к объекту потомку при этом происходит через указатель или ссылку на базовый класс.

Ограничение доступа к свойствам

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

Было бы неплохо, чтобы доступ к свойствам был различный, что позволит иметь свойства, ошибочное использование которых запрещено автоматически. В С++ существует три уровня доступа к свойству, которые можно применить к любому виду на объект, т.е. в одноименном уровне доступа С++ могут находится свойства разных наших видов. Это уровни:

В контексте классов, различают доступ к объекту через:

  1. экземпляр класса или ссылку на экземпляр базового класса
    Если в описании одного класса объявлен ( вложен ) объект другого класса ( свойство ), то методам протокола класса доступны сообщения и свойства только из public секции вложенного объекта. К объекту другого класса объявленному вне описания класса, в программе, такой же доступ.

  2. протокол класса
    Все методы, описанные в протоколе класса, могут использовать все другие методы и свойства, описанные в протоколе класса. Говорят, что метод класса имеет доступ к классу через протокол класса. К любому объекту этого же класса (не только к тому, для которого вызван метод) метод этого класса имеет такой же самый доступ.

  3. протокол базового класса
    Методам из протокола класса доступны сообщения и свойства из public и protected полей базового класса. Т.е. protected расширяет обычный доступ специально для механизма наследования. Для любого объекта этого же (но не базового) класса (не только к тому, для которого вызван метод) такой же доступ. Интересно, что для методов из протокола класса на доступ не влияет то, как наследуется базовый класс: открыто или закрыто.

    Несмотря на то, что класс является потомком, для методов из протокола класса доступ к ссылке на объект базового класса (не к собственным свойствам унаследованным от базового класса, которые доступны через this или другой объект своего класса) происходит именно через ссылку на экземпляр базового класса.

    При доступе к свойствам базового класса через ссылку на класс потомка, эти свойства открыты в зависимости от того, как потомок наследует свой базовый класс. Если потомок объявляет private базовый класс, то не доступны сообщения и свойства базового класса даже из секции public.

    В наследнике можно изменить доступ к сообщениям базового класса, произвольным образом переопределяя их в новых уровнях. Если базовый класс наследуется как private, то доступ к его public сообщениям и свойствам в объекте наследнике возможен через указатель на базовый класс (Пример 1).

    Пример 1
    Области видимости при наследовании
    Описание классов
    //базовый класс
    class Tbase
    {
    public:
        //доступ через указатель на базовый класс
        void priv()
         { printf("%s:priv()\n","Tbase"); }
    
    protected:
        //смена доступа в потомке
        void to_public()
         { printf("%s:to_public()\n","Tbase"); }
    };
    
    //наследует закрытый базовый класс
    class Td_private: private Tbase{};
    
    //наследует открытый базовый класс
    class Td_public:public Tbase
    {
    public:
        void to_public()
         {
          Tbase::to_public();
          printf("%s:to_public()\n","Td_public");
         }
    };
    применение
    Td_public  pub;
    Td_private priv;
    Tbase      *bp=(Tbase*)&priv;
    
        //доступ к измененному в потомке
        pub.to_public();
        //доступ к закрытому в потомке
        bp->priv();

    Дружественные классы и функции ( friend )

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

    Есть способ игнорировать разграничения доступа, при доступе через экземпляр класса или протокол базового класса, описанные выше. Это использовать объявление в протоколе класса, к которому происходит доступ, тех классов или функций ( именно функций, т.к. они определены прямо в программе ) для которых этот доступ будет разрешен. Это нужно, если класс или функция работает сразу с двумя классами, которые не связаны между собой, но требуется доступ к защищенным свойствам. Я знаю несколько типовых применений этих свойств:

    1. Итераторы - аналог индекса для списка. Обычно, в списке определяют ряд переменных, которые отражают выбранный элемент и другое состояние списка. Эти переменные отражают только один вариант состояния. Поэтому алгоритмы, которые используют несколько вариантов за время своего выполнения, должны сами сохранять и переключать эти состояния, что не относится к сути решаемых задач. Итераторы позволяют определять столько состояний, сколько нужно, и при доступе через итератор не заботиться о том, что другая часть алгоритма могла изменить ожидаемое старое состояние.

    2. Перегрузка операций с поддержкой автоматического приведения типа и независимости порядка операндов. Для примера следует обратиться к руководству по С++.

    Контекст имен свойств при наследовании

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

    Как уже говорилось, при наследовании возможна ситуация, когда одному сообщению соответствует несколько методов. Компилятор должен как-то отличать метод с одинаковым именем одного класса от другого. По аналогии с А-моделью, можно ввести искажение имени, вставив в имя метода название класса, в котором он определен. То же можно сделать и с любым свойством класса. Это делается с помощью оператора указания контекста имен свойства ::

    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
    Не виртуальные базовые классы при множественном наследовании
    Описание классов
    class Tbase
    {
    public:
        int x;
        virtual void equ()
         { printf("Tbase:equ():x=%u\n",x); }
    
        virtual void equ2()
         { printf("Tbase:equ2():x=%u\n",x); }
    
        void not_equ()
         { printf("Tbase:not_equ():x=%u\n",x); }
    
        Tbase(){ x=0; }
    };
    
    class Td_1: public Tbase
    {
    public:
        int x;
    
        void equ()
         { printf("Td_%c:equ():x=%u\n",'1',x); }
    
        void equ2()
         { printf("Td_%c:equ2():x=%u\n",'1',x); }
    
        void not_equ()
        { printf("Td_%c:not_equ():x=%u\n",'1',x); }
    
        Td_1(){x=1;}
    };
    
    class Td_2: public Tbase
    {
    public:
        int x;
    
        void equ()
         { printf("Td_%c:equ():x=%u\n",'2',x); }
    
        void equ2()
         { printf("Td_%c:equ2():x=%u\n",'2',x); }
    
        void not_equ()
         { printf("Td_%c:not_equ():x=%u\n",'2',x); }
    
        Td_2(){x=2;}
    };
    
    class Td:public Td_1,public Td_2
    {
    public:
        int x;
    
        void equ()
         { printf("Td:equ():x=%u\n",x); }
    
        void not_equ()
         { printf("Td:not_equ():x=%u\n",x); }
    
        Td(){x=3;}
    };
    Использование
    Tbase b,*bp;
    Td_1  d1,*dp1;
    Td_2  d2,*dp2;
    Td    d, *dp;
    
        printf("\t\t*** Tbase ***\n");
        //прямое обращение недопустимо
        //насильно для примера присвоим через (void*)
        bp=(Tbase*)((void*)&d);
        bp->equ();
        bp->not_equ();
        printf("%s->x=%u\n","Tbase_p",bp->x);
    
        printf("\t\t*** Td_1 ***\n");
        dp1=(Td_1*)&d;
        dp1->equ();
        dp1->equ2();
        dp1->not_equ();
        printf("%s->x=%u\n","Td_1_p",dp1->x);
    
        printf("\t\t*** Td_2 ***\n");
        dp2=(Td_2*)&d;
        dp2->equ();
        dp2->equ2();
        dp2->not_equ();
        printf("%s->x=%u\n","Td_2_p",dp2->x);
    
        printf("\t\t*** Td ***\n");
        dp=&d;
        dp->equ();
        dp->not_equ();
        printf("%s->x=%u\n","Td_p",dp->x);
    Результаты выполнения
            *** Tbase ***
    Td::equ():x=3 
    //ниже вызван бессмысленный для Tbase
    //метод класса Td_1::equ2()
    //не переопределенный в Td
    Td_1::equ2():x=1
    Tbase::not_equ():x=0
    Tbase_p->x=0
            *** Td_1 ***
    Td::equ():x=3
    Td_1::equ2():x=1
    Td_1::not_equ():x=1
    Td_1_p->x=1
            *** Td_2 ***
    Td::equ():x=3
    Td_2::equ2():x=2
    Td_2::not_equ():x=2
    Td_2_p->x=2
            *** Td ***
    Td::equ():x=3
    Td::not_equ():x=3
    Td_p->x=3

    Пример 3
    Виртуальные базовые классы при множественном наследовании
    Описание классов
    class Tbase
    {
    public:
        int x;
    
        virtual void equ()
         { printf("Tbase:equ():x=%u\n",x); }
    
        virtual void equ2()
         { printf("Tbase:equ2():x=%u\n",x); }
    
        void not_equ()
         { printf("Tbase:not_equ():x=%u\n",x); }
    
        Tbase(){x=0;}
    };
    
    class Td_1: virtual public Tbase
    {
    public:
        int x;
    
        void equ()
         { printf("Td_%c:equ():x=%u\n",'1',x); }
    
        void equ2()
         { printf("Td_%c:equ2():x=%u\n",'1',x); }
    
        void not_equ()
         { printf("Td_%c:not_equ():x=%u\n",'1',x); }
    
        Td_1(){x=1;}
    };
    
    class Td_2: virtual public Tbase
    {
    public:
        int x;
    
        void equ()
         { printf("Td_%c:equ():x=%u\n",'2',x); }
    
        void equ2()
         { printf("Td_%c:equ2():x=%u\n",'2',x); }
    
        void not_equ()
         { printf("Td_%c:not_equ():x=%u\n",'2',x); }
    
        Td_2(){x=2;}
    };
    
    class Td:public Td_1,public Td_2
    {
    public:
        int x;
    
        void equ()
         { printf("Td:equ():x=%u\n",x); }
    
        //пришлось переопределить эту виртуальную функцию
        void equ2()
         { printf("Td:equ2():x=%u\n",x); }
    
        void not_equ()
         { printf("Td:not_equ():x=%u\n",x); }
    
        Td(){x=3;}
    };
    Использование
    Tbase b,*bp;
    Td_1  d1,*dp1;
    Td_2  d2,*dp2;
    Td    d, *dp;
    
        printf("\t\t*** Tbase ***\n");
        bp=(Tbase*)&d;
        bp->equ();
        bp->Tbase::equ2();
        bp->not_equ();
        printf("%s->x=%u\n","Tbase_p",bp->x);
    
        printf("\t\t*** Td_1 ***\n");
        dp1=(Td_1*)&d;
        dp1->equ();
        dp1->Td_1::equ2();
        dp1->not_equ();
        printf("%s->x=%u\n","Td_1_p",dp1->x);
    
        printf("\t\t*** Td_2 ***\n");
        dp2=(Td_2*)&d;
        dp2->equ();
        dp2->Td_2::equ2();
        dp2->not_equ();
        printf("%s->x=%u\n","Td_2_p",dp2->x);
    
        printf("\t\t*** Td ***\n");
        dp=&d;
        dp->equ();
        dp->not_equ();
        printf("%s->x=%u\n","Td_p",dp->x);
    Результаты выполнения
            *** Tbase ***
    Td:equ():x=3
    Tbase:equ2():x=0
    Tbase:not_equ():x=0
    Tbase_p->x=0
        *** Td_1 ***
    Td:equ():x=3
    Td_1:equ2():x=1
    Td_1:not_equ():x=1
    Td_1_p->x=1
            *** Td_2 ***
    Td:equ():x=3
    Td_2:equ2():x=2
    Td_2:not_equ():x=2
    Td_2_p->x=2
            *** Td ***
    Td:equ():x=3
    Td:not_equ():x=3
    Td_p->x=3


    Механизмы образования объектов: часть 2

    Реализация объектной модели

    Вернемся к нашей объектной модели. Как преобразовать сообщение из пространства сообщений, в сообщение класса С++? Можно ли по виду на объект пространства сообщений прямо получить механизм? Какой механизм лучше с точки зрения сопровождения и эффективности программы?

    Избежать этих вопросов при написании программы нельзя. Мы не можем во время программирования вдаваться и в сложности понятий отношения, абстракции и т.д. Нам, как пользователям метода ООП, лучше использовать готовые простые критерии по выбору способа образования. Если это задается произвольно, зачем тогда вообще это разделять? ООП это разделяет.

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

    Вложенность против базового класса

    Механизмы образования объектов: часть 2

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

    Что означает понятное определение? Это означает, что его можно перефразировать, уточнить любое слово, сохранив смысл. Вот замечательные по своей сути определения, которые каждый может найти:

    1. "Собака" является "псовым", но содержит "хвост"
    2. Лакмусовой бумажкой проверки отношения является "is-a" против содержит "part of" является обратная проверка: если В не есть А, то В не стоит производить от А.

    Первое просто утверждает, что если объект не базовый класс, то он вложен. Но вопрос не в этом, это утверждение очевидно, если исключить повторное описание. Именно не базовый ли класс мы и хотим узнать. Неявно используется то, что мы все видели собак и хвосты и считаем, что в базовых объектах "псовые" заложено то, что хвост не является собакой, а собака это "псовое". Это кажется очевидным, что хвост не собака, а собака не хвост, но, в общем случае, это далеко не очевидно и как раз это то и представляет интерес.

    Если с хвостами и другими органами животных все может и понятно, то как быть с дверями? Куда деть двери в комнате: в "комната" является "строением" и содержит "двери" или в "комната" является "строением с дверями" и содержит "ничего"? Или вот. Файл Х типа является файлом, но может еще являться базой данных, а может включить ее как одно из своих полей-свойств, что выбрать при создании типа?

    За приведенным примером с собаками, пытаются скрыть то, что скрыто за словом "является", а второе утверждение просто ставит в недоумение, не приводя примеры, как раскрыть слово "есть".

    Проиллюстрируем еще. У того же Буча ( пример 2 ) есть пример с кошкой, который я уже приводил, в котором кошка взглядом патологоанатома и бабушки выглядит по разному, и для бабушки шерсть "есть" кошка, а не кошка "содержит" часть кошки - шерсть, как для патологоанатома.

    Так что же получается, можно использовать как вложение, так и наследование? Это так. Когда у вас нет уже заранее обоснованной модели, (обоснованной условиями задачи, имеющимися базовыми классами или еще чем-нибудь, т.е. когда вы уже, фактически, определились с тем, что использовать для создания) тесты с "хвостами" и "перестановками А и В" не годятся, потому как они не дают однозначного и легко проверяемого критерия.

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

    "Является" и "есть" (как мне подсказали) это означает примерно следующее: "не обладает или обладает незначительными отличиями". Но для выбора механизма, а наследование это механизм, этих условий мало: можно создать объект и с помощью вложения, который будет отличаться незначительно, тоже "являться". Нам лучше из логической сферы перейти в сферу техническую и выяснить: в каких случаях наследование нельзя заменить на вложенность никак и в каких случаях трудоемкость создания класса механизмом наследования меньше.

    Я могу предложить следующие критерии:

    1. Использовать вложение
      • (отличительный от наследования критерий) Надо получить несколько независимых экземпляров вложенного объекта, количество которых, может быть, изменяется во время выполнения.
      • (менее актуально с появлением namespace) Формирование пространства имен, выделяя логическую группу сообщений, полученных от модели, от пространства сообщений, из области имен объекта по умолчанию.

    2. Использовать наследование
      • (отличительный от вложения критерий) Объект должен "быть типом" базового класса. "Быть типом" означает, что и базовый, и производный классы могут использоваться в одном шаблоне, т.е. наследование это способ задать равнозначность разных классов для одного шаблона. Для задания базового класса для шаблона хорошо подходит абстрактный базовый класс (задание интерфейса).
      • (дополнительный критерий) Позволяет использовать методы и свойства предка в потомке, вызывая их по произвольному алгоритму (общий код). Потомок может конвейерно обрабатывать сообщение несколькими методами из линии наследования, полностью модернизировать метод реализации сообщения предка, заменяя неверный метод, который может быть доступен только как откомпилированный код.

    Таким образом, тот факт, что мы вынесли наследование и вложение в механизмы, говорит о том, что:

    Получение из пространства имен отношений независим/содежит/является

    Механизмы образования объектов: часть 2

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

    Было бы хорошо, чтобы механизм имел однозначное соответствие с логической структурой, чтобы можно было описывать программу в логических действиях. Но три известные нам механизма перекрываются в некоторых случаях, т.е. можно использовать любой и нельзя просто указать обоснованную различимость. Сейчас важно пока отметить те случаи, когда эти механизмы не перекрываются, заметить области, в которых выбор механизма определяется однозначно. Эти условия перечислены в предыдущем разделе в пунктах 1 и 2 для наследования и вложения и помечены как отличительные. Если ассоциативной связи нет и вы не пытаетесь построить один объект на основе описания другого, то используется простое прямое описание.

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

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

    Вложенный объект имеет не полиморфичные, относительно контейнера сообщения, даже если собственные методы вложенного объекта объявлены как virtual, но он может транслировать их в полиморфичные, относительно контейнера, сообщения самого контейнера с произвольными именами, которые можно назвать сообщениями реализации модели. При этом при создании вложенного объекта используется ссылка на контейнер.


    Связь с А-моделью

    Реализация объектной модели

    Мы уже вплотную приблизились к способам задания модели на С++. Я надеюсь, что теперь только надо узнать как мы хотим описать то, что хотим получить, для чего надо открыть книгу по С++. Часть особенностей, которые показались мне важными, перечислены в следующем разделе "Объектная модель и С++".

    Может вызвать неясность, как объектная модель связана с А-моделью, которая в свою очередь связана с С-моделью. Объектная модель задана аксиоматически, но легко заметить, что объект можно, в некотором приближении, считать АТД с добавлением операции наследования. Приближение используется потому, что в предложенном задании О-модели, наследование не является абсолютно необходимой чертой, а относится к разряду механизмов и стоит на равне с вложением и прямым описанием. Функция и переменная тоже не полностью соответствуют сообщению, методу и свойству объекта, однозначной связи между ними нет.

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

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

    Но это модель для обучения. Она, вероятно, не самая непротиворечивая, но может использоваться и для перехода от простого структурно-алгоритмического программирования к программированию с использованием средств ООП ( для чего она и создавалась ), и для практического решения ряда задач. Полезность и недостатки ее, скорее всего, можно определить только при реальном использовании, а не теоретически.

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


    Отличия модульной С-модели от О-модели

    Реализация объектной модели

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

    Объект можно считать АТД, который в свою очередь является модулем. Но объект, как модуль, имеет более сложную структуру и развитые механизмы реализации, чем модули в структурной модели. Вся работа с О-моделью сосредоточена на выборе структуры модуля и механизмов ее реализации. Поэтому я и говорил об О-модели как о новом уровне типизации для структурной модели. Хотя разделение на модули контекста задачи для обоих моделей может быть похоже.

    Объектная модель Модульная структурная модель
    Позволяет задать сложную иерархическую структуру модуля, описывая один модуль в терминах других модулей. Например, вызвать для модуля А функцию х() модуля В, который вложен в модуль А: А.В.х() Из-за нетипизированности модуля нельзя сосредоточить описание модуля в одном типе, в одном месте и использовать его в остальных модулях, как их часть, ссылаясь на имя типа. В языке С можно ухищрениями с #include и #define добиться чего-то подобного, но в серьез мы это рассматривать не будем.
    Оперирует модулями как элементарными единицами программы. Основными операциями над модулями являются: выбор структуры модулей и механизмов ее реализации. Оперирует структурами данных и имеет модули простой структуры. Основными операциями являются: выбор структур данных и задание последовательности действий для работы с ними.


    Реализация объектной модели

    Сайт создан в системе uCoz