Композиционное наследование в объектно-ориентированном программировании.

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

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

Наследование позволяет нечто большее. Через механизм виртуальных методов базовый класс имеет возможность вызывать методы дочернего. Таким образом, используемый, ранее написанный класс может выполнять активную роль в порожденном классе. Принцип наследования в ООП оказался очень плодотворным. Он сильно расширяет области повторного использования кода. Ранее написанный код получает возможность управлять не только своими структурами данных, но и данными потомков. Наследование бывает одиночным и множественным. При одиночном наследовании новый класс может иметь только одного предка (базовый класс). Предок в свою очередь может иметь своего предка и т.д. В результате множество классов можно представить в виде дерева (иерархии классов). При множественном наследовании допустимо существование более одного предка в дочернем классе. Но, не смотря на широкие возможности, механизм наследования интуитивно уступает композиции в своих выразительных возможностях.

В широко распространенных языках программирования оба типа наследования реализованы в различных вариациях. Например, в СИ++ существуют одиночное и множественное наследование. В Java, для классов реализовано только одиночное, множественное разрешается для интерфейсов. В сравнительно молодом языке C# множественное наследование также возможно только для интерфейсов. Таким образом, мы видим, что применение множественного наследования далеко от активного.

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

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

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

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

Композиционное наследование (КН)

Рассмотрим идею на примере простой задачи. У нас есть робот, у робота две ноги. Необходимо запрограммировать его ходьбу. Робот умеет шагать. При этом, делая шаг, нога может встретить препятствие (упор). Робот, возможно, может убрать препятствие (но не нога!), тогда он убирает его, и нога совершает шаг. Для наглядности позволим себе вольность - разрешим употребление русских букв в идентификаторах. Решаем эту задачу с помощью применения метода КН :

В примере класс "Робот " имеет две переменные "леваяНога" и "праваяНога" одного класса "Нога". Синтаксис определения совпадает с определением члена класса СИ++, перед которым пишется новое ключевое слово "base", оно указывает, что мы имеем дело не с членом класса, а с родительским объектом. Следует обратить внимание, если разрешить всем членам класса (если они тоже классы) быть потенциальными предками порожденного класса, то в примере:

можно опустить слово "base" и писать как и ранее:

Во-вторых, в дочернем классе "Робот " введены два метода, " УбратьПрепятствиеЛевойНоге " и " УбратьПрепятствиеПравойНоге", которые переопределяют один и тот же метод класса "Нога", но в разных родительских объектах. Эти два момента и определяют суть принципа композиционного наследования, который позволяет создавать производный класс, синтезируя поведение множества родительских объектов.

Полиморфизм

В примере полиморфизм используется как возможность старого кода, класс Нога - виртуальный метод УбратьПрепятствие, вызывать новый код в классе Робот, метод УбратьПрепятствиеЛевойНоге и УбратьПрепятствиеПравойНоге. Очевидно, что неоднозначности нет, т.к. разные методы вызываются из разных родителей, несмотря на то, что родители и одного класса Нога

Инкапсуляция

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

Сравнительный анализ

На примерах сравним формы реализации задачи классического наследования и КН. Доступ к родительским методам и переменным.-

Опишем базовый класс:

Опишем производный класс, в классическом варианте:

И аналог производного класса в КН:

В этом примере использование класса A не различается:

В, обоих примерах все различия выделены жирным шрифтом. Сравним их:

1. Указание на дочерний класс

Во втором варианте появляется новый идентификатор "a", который дает имя дочернему объекту. В классическом варианте понятие дочернего объекта отсутствует, есть только дочерний класс.

2. Переопределение виртуального метода Fun2()

Как видите, в КН требуется дополнительная информация, которая указывает,какой метод и в каком дочернем объекте переопределяется.

3. Доступ к переменным и методам в методе B:: Fun2()

Снова понадобилось уточнение, в каком дочернем объекте, вызывается виртуальный метод Fun2()

4. Доступ к переменным и методам в методе B:: SumAll() 

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

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

Приведение ссылок

Повышающее приведение, приведение из производного класса в базовый.

Классический пример:

Аналог в КН.

Очевидно, что разница небольшая. Кроме того, в функции main КН операторы выглядят более естественно. Понижающее приведение, приведение из базового класса в производный.Данная операция не всегда законна и существуют другие более тонкие методы приведения типов. В том виде как она приведена в примере, предполагается, что программист знает, что делает.

Классический пример:

Аналог в КН:

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

Моделирование средствами классического ООП

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

Возможно, формальные вставки можно реализовать с помощью препроцессора СИ++. Кроме того, в приведенном примере не учтен вопрос инкапсуляции. Если в классе A будут protected переменные, мы не сможем в классе B получить к ним доступ. Можно, конечно, в описание класса А вставить "friend class B;" но это будет нарушением принципа полиморфизма - редактирование старого кода.

Введение понятия "Композиционного наследования" проверено на практике в рамках разработанного российскими программистами "СП-языка" и показало свою практическую ценность. "СП-язык" (интерпретируемый) является компонентом программной оболочки "СП-Z50", предназначенной для решения различных задач, ориентированных на работу с базами данных. Разработка программной оболочка "СП-Z50"(ныне Викта) была начата в 1992г. За эти годы "СП-язык" превратился в алгоритмически полный объектно-ориентированный язык программирования с элементами инкапсуляции и наследования. "Композиционное наследование" позволило увеличить скорость реализации проектов, устранило неудовлетворенность при применении механизма наследования, сделало прикладные программы более читаемыми и понятными.

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

Литература.

  1. Б. Страуструп. “Язык программирования С++”. Перевод с английского. 2002г
  2. Ян Ф. Дарвин “Java .Сборник рецептов для профессионалов”. Питер, 2002г
  3. Т.А. Павловская “С#.Программирование на языке высокого уровня”, Питер, 2007г
  4. Д. Райли “Абстракция и структуры данных”, Москва, “Мир”,1993г
  5. П. Ноутон, Г. Шилдт “Java 2”, БХВ – Петербург, 2001г
  6. Т.Бадд. "Объектно-ориентированное программирование в действии"
  7. Иан Грэхем. “ Объектно-ориентированные методы. Принципы и практика”