Глава     9

Повторное использование кода


Разделы

Содержание

     Объектно-ориентированное программирование было объявлено как технология, которая позволит наконец конструировать программы из многократно используемых компонент общего назначения. Такие авторы, как Брэд Кокс, зашли так далеко, что уже говорили об объектно-ориентированном подходе как о предвестнике «промышленной революции» в разработке программного обеспечения [Cox 1986]. Пока действительность не вполне соответствует ожиданиям пионеров ООП (тема, к которой мы еще обратимся в конце этой главы). Что действительно справедливо — так это то, что ООП позволяет встраивать многократно используемые программные компоненты гораздо интенсивнее, чем раньше. В этой главе мы рассмотрим два наиболее общих механизма многократного использования программного обеспечения, которые известны как наследование и композиция.

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

9.1. Наследование и принцип подстановки

Разделы

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

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

     В этом разделе мы обсудим не настоящие классы, а абстрактные концепции, программной реализацией которых выступают классы. При каких условиях одно абстрактное понятие можно подставить вместо другого? То есть при каких условиях экземпляр некоторого абстрактного понятия перестановочен с экземпляром другого абстрактного понятия? Одно из классических правил применяемых здесь, ставшее основным для объектно-ориентированного проектирования, известно как «быть экземпляром» (правило «is-a»).

9.1.1. «Быть экземпляром» и «включать как часть»

Наследование и принцип подстановки

     Знание двух различных форм отношений — основа понимания того, как и когда применять приемы многократного использования кода. Имеются два типа отношений, известных как быть экземпляром и включать как часть (is-a и has-a).

     Отношение быть экземпляром имеет место между двумя понятиями, если первое является уточнением второго. То есть для всех практических целей поведение и данные, связанные с более конкретным понятием, составляют подмножество поведения и данных, связанных с более абстрактным понятием. Например, все примеры наследования, описанные нами в предыдущих главах, удовлетворяют отношению быть экземпляром (хозяйка цветочного магазина Florist является экземпляром класса владельцев магазина Shopkeeper, собака Dog является экземпляром класса млекопитающих Mammal, бильярдный шар Ball является экземпляром класса графических объектов GraphicalObject, и т. д.).

     Название этого отношения происходит из простого правила проверки. Чтобы определить, является ли понятие X уточненным вариантом Y, просто составьте предложение «X является экземпляром Y». Если утверждение звучит корректно, то есть оно соответствует вашему жизненному опыту, то вы можете заключить, что X и Y связаны отношением быть экземпляром.

     Напротив, отношение включать как часть имеет место, когда второе понятие является компонентой первого, но оба эти понятия не совпадают ни в каком смысле независимо от уровня общности абстракции. Например, автомобиль Car имеет двигатель Engine, хотя ясно, что это не тот случай, когда Car является экземпляром Engine или Engine является экземпляром Car. Car тем не менее является экземпляром класса автомобилей Vehicle, который в свою очередь является экземпляром класса средств передвижения MeansOtTransportation 1.


  1. Условие «X является экземпляром Y» не должно трактоваться ни как утверждение «X является подмножеством Y», рассматриваемое в теоретико-множественном смысле, ни как утверждение «X является наращиванием Y» в смысле расширения структуры данных. Условие «X является экземпляром Y» справедливо, если сущность X — это уточнение, детализация, конкретизация, специализированная форма, и т. д. сущности Y, и ни в каком другом случае. В частности, приводимый далее пример с автомобилем и двигателем выглядит следующим образом: двигатель входит в автомобиль в смысле теории множеств (двигатель — более мелкая «подробность» и фрагмент устройства автомобиля), автомобиль усложняет двигатель (автомобиль — это двигатель целиком плюс еще много другого), но ни в том, ни в другом случае не справедливо условие «X является экземпляром Y». — Примеч. перев.

     Еще раз, чтобы проверить отношение включать как часть, просто составьте предложение «X включает Y как часть» и предоставьте решать здравому смыслу.

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

9.2. Композиция и наследование: описание

Разделы

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

class List
{
public:

     // конструктор
     List();

     // методы
     void addToFront(int);
     int  firstElement();
     int  length();
     int  includes(int);
     int  remove(int);
     ...
};

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

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

9.2.1. Использование композиции

Композиция и наследование: описание

     Сначала мы исследуем, может ли абстракция множества быть создана с помощью композиции. Напомним, что объект — это просто инкапсуляция данных и поведения. Когда для многократного использования существующей абстракции данных при создании нового типа используется композиция, то часть новой структуры данных является просто экземпляром существующей структуры. Это показано ниже, где тип данных Set содержит поле, названное theData, которое объявлено с типом List.

Class Set
{
public:

   // конструктор
   Set(); 

   // операции
   void  add(int);
   int   size();
   int   includes(int);

private:

   // область данных для значений
   List  theData;
};

     Поскольку абстракция List хранится как часть области данных нашего множества, она должна быть инициализирована в конструкторе. Будучи аналогичными командам инициализации полей данных для классов (глава 4), команды инициализатора в начале конструктора задают аргументы для инициализации полей данных. В данном случае конструктор, который мы вызываем для класса List, — безаргументный:

// список инициализации
Set::Set() : theData()
{
  // никакой дальнейшей инициализации
}

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

int Set::size ()
{
    return theData.length();
}

int Set::includes(int newValue)
{
     return theData.includes(newValue);
}

     Только одна операция оказывается чуть более сложной. Это — добавление нового элемента, так как нужно сначала убедиться, что данная величина не содержится в множестве (величины не могут появляться в множестве более одного раза):

void Set::add(int newValue)
{
   // если не в множестве
   if (! Includes (newValue))
    {
     // тогда добавить
     theData.addToFront(newValue);
    }
   // иначе ничего не делать
}

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

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

Композиция в других языках

     Композиция может быть применена в любом объектно-ориентированном языке программирования, рассматриваемом в этой книге. Но она встречается и в языках, не являющихся объектно-ориентированными. Единственная существенная разница — в способе инициализации инкапсулированных данных. В языке Smalltalk в общем случае это выполняется через класс-методы, в языке Objective-C — с помощью методов-фабрик, в языках Java и Object Pascal — с использованием конструкторов.

9.2.2. Применение наследования

Композиция и наследование: описание

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

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

Class Set : public List
{
public:

   // конструктор
   Set();

   // операции
   void add(int);

   int  size();
};

     Заметьте, что новый класс не определяет никаких новых полей данных. Вместо этого поля данных класса List будут использоваться для хранения элементов множества. Эти поля должны быть по-прежнему проинициализированы. Данная операция выполняется вызовом конструктора надкласса в конструкторе нового класса:

Set::Set() : List()
{     
  // никакой дальнейшей инициализации
}

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

void Set::add   (int newValue)
{   
    // добавить, если нет в множестве
    if (! Includes(newValue))
        addToFront (newValue);
}

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

Наследование в других языках

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

9.2.3. Закрытое наследование в языке C++

Композиция и наследование: описание

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

Class Set : private List
{
public:

    // конструктор
    Set() : List() { }

    // операторы
    void add(int);

    int includes(int x);
     {
      return List::includes(x);
     }

    int size()
     {
      return List::length();
     }
};

     Применяя термины, которые будут определены более строго в главе 10, можно сказать, что закрытое наследование создает подкласс, который не является подтипом. Тем самым закрытое наследование использует механизм наследования, но в явном виде нарушает принцип подстановки. Операции и области данных, наследуемые из родительского класса, задействуются в методах новой абстракции, но они не «просматриваются насквозь» и недоступны ее пользователям. По этой причине любой метод, который программист хочет экспортировать (такой, как includes в абстракции множества), должен быть переопределен заново для нового класса, даже если все, что он делает, — это вызов метода класса-предка. (Как было уже проиллюстрировано, чтобы избежать накладных расходов при вызовах процедур в подобных простых случаях, часто используются встраиваемые методы.)

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

9.3. Противопоставление композиции и наследования

Разделы

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

     Имея два различных механизма реализации, можем ли мы сказать, который из них лучше в нашем конкретном случае? Обратимся к принципу подстановки. Спросите себя, корректно ли в приложении, которое предполагает использование абстракции данных List, подставлять вместо нее множество Set? Хотя чисто техническим ответом может быть «да» (абстракция Set действительно реализует все операции List), здравый смысл говорит, скорее, «нет». Поэтому в данном случае композиция подходит лучше.

     Последний штрих: обе техники очень полезны, и объектно-ориентированный программист должен быть знаком с обеими.

9.4. Повторное использование кода: реальность?

Разделы

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

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

     Вот список недавно изданных книг, посвященных разработке многократно используемых компонент [Carroll 1995, McGregor 1992, Meyer 1994, Goldberg 1995].

Упражнения

Разделы


Повторное использование кода

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