Возможна ли ситуация при которой прямой наследник абстрактного класса не переопределит

Обновлено: 30.06.2024

Аннотация: В данной лекции рассматривается простое и множественное наследование классов. Виртуальные методы. Абстрактные классы. Создание и использование шаблонов классов.

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

Презентацию к лекции Вы можете скачать здесь.

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

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

Виды наследования

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

Если базовых классов несколько, они перечисляются через запятую. Перед каждым может стоять свой модификатор наследования . По умолчанию для классов он private , а для структур - public .

Если задан модификатор наследования public , оно называется открытым. Использование модификатора protected делает наследование защищенным, а модификатора private - закрытым. Это не просто названия: в зависимости от вида наследования классы ведут себя по-разному. Класс может наследовать от структуры, и наоборот.

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

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

Элементы protected при наследовании с ключом private становятся в производном классе private , в остальных случаях права доступа к ним не изменяются.

Доступ к элементам public при наследовании становится соответствующим ключу доступа.

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

Простое наследование

Простым называется наследование , при котором производный класс имеет одного родителя. Для различных элементов класса существуют разные правила наследования . Рассмотрим наследование классов на примере.

Создадим производный от класса monster класс daemon , добавив полезную в некоторых случаях способность думать:

В классе daemon введено поле brain и метод think , определены собственные конструкторы и операция присваивания , а также переопределен метод отрисовки draw . Все поля класса monster , операции (кроме присваивания ) и методы get_health , get_ammo и set_health наследуются в классе daemon , а деструктор формируется по умолчанию.

Рассмотрим правила наследования различных методов.

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

  • Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса по умолчанию (то есть тот, который можно вызвать без параметров). Это использовано в первом из конструкторов класса daemon .
  • Для иерархии , состоящей из нескольких уровней, конструкторы базовых классов вызываются начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса .
  • В случае нескольких базовых классов их конструкторы вызываются в порядке объявления.

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

Не наследуется и операция присваивания, поэтому ее также требуется явно определить в классе daemon . Обратите внимание на запись функции-операции: в ее теле применен явный вызов функции- операции присваивания из базового класса . Чтобы лучше представить себе синтаксис вызова, ключевое слово operator вместе со знаком операции можно интерпретировать как имя функции -операции.

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

Правила для деструкторов при наследовании :

  • Деструкторы не наследуются, и если программист не описал в производном классе деструктор , он формируется по умолчанию и вызывает деструкторы всех базовых классов .
  • В отличие от конструкторов, при написании деструктора производного класса в нем не требуется явно вызывать деструкторы базовых классов, поскольку это будет сделано автоматически.
  • Для иерархии классов , состоящей из нескольких уровней, деструкторы вызываются в порядке, строго обратном вызову конструкторов: сначала вызывается деструктор класса , затем - деструкторы элементов класса, а потом деструктор базового класса .

Поля, унаследованные из класса monster , недоступны функциям производного класса , поскольку они определены в базовом классе как private . Если функциям, определенным в daemon , требуется работать с этими полями, можно либо описать их в базовом классе как protected , либо обращаться к ним с помощью функций из monster , либо явно переопределить их в daemon так, как было показано в предыдущем разделе.

Добавляемые поля в наследнике могут совпадать и по имени, и по типу с полями базового класса . При этом поле предка будет скрыто.

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

Рассматривая наследование методов, обратите внимание на то, что в классе daemon описан метод draw , переопределяющий метод с тем же именем в классе monster (поскольку отрисовка различных персонажей, естественно, выполняется по-разному). Таким образом, производный класс может не только дополнять, но и корректировать поведение базового класса . Доступ к переопределенному методу базового класса для производного класса выполняется через уточненное с помощью операции доступа к области видимости имя.

Класс-потомок наследует все методы базового класса , кроме конструкторов, деструктора и операции присваивания . Не наследуются ни дружественные функции, ни дружественные отношения классов.

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

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

Смотрите, мы здесь определили одно поле model и три абстрактных метода: go, stop и draw. Абстракция в данном случае означает, что мы знаем что хотим от автомобиля, но пока незнаем как это будем делать. Своего рода, это некий набросок – абстракция, причем, абстракция на уровне класса и методов.

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

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

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

Здесь используется сеттер для задания поля model.

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

Итак, давайте тоже наполним конкретикой наш абстрактный класс Car. Для этого определим дочерний класс и назовем его, например, ToyotaCorolla. Если написать вот такие строчки:

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

Теперь никаких ошибок не будет и мы можем создать экземпляр этого класса:

Или, используя обобщенный тип ссылок на абстрактный класс:

И, далее, мы можем вызывать методы go, stop и draw, определенные в дочернем классе:

Давайте для примера добавим еще один дочерний класс ToyotaCamry:

Определим массив обобщенных ссылок в функции main:

Присвоим им экземпляры дочерних классов:

И вызовем общие методы базового класса Car, реализованные в дочерних классах:

Видите, благодаря тому, что в базовом классе прописаны виртуальные методы go, stop и draw, мы имеем возможность вызывать их, используя единый интерфейс – ссылки на экземпляры базового класса Car. Это еще один пример полиморфизма в ООП.

Но здесь у вас может возникнуть вопрос: зачем городить огород, придумывать абстрактный класс Car, когда все то же самое можно было сделать и с обычным классом Car. Да, все верно, например, можно прописать базовый класс в виде:

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

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

Вот эти моменты обусловили появление абстрактных классов и методов в языке Java.

Путь кодера

Подвиг 1. Объявите абстрактный класс Geom для представления геометрических фигур с полями: width, color для определения толщины и цвета линии, а также с абстрактным методом draw() для рисования конкретного графического примитива. Затем, запишите дочерние классы Line, Rect, Ellipse для представления линий, прямоугольников и эллипсов. Определите в них поля для хранения координат этих фигур и метод draw() для их рисования. Создайте обобщенные ссылки Geom на объекты дочерних классов и вызовите у них метод draw().

Подвиг 2. Объявите абстрактный класс Recipes (рецепты) с полями: название, тип (вегетарианский/обычный). И абстрактными методами: showIngredients (показать ингредиенты), showRecipe (показать рецепт). Описать несколько дочерних классов: Salad (для салатов), Pizza (для пицц), Porridge (для каш). В каждом дочернем классе определить поле для списка ингредиентов (в виде строки) и описания самого рецепта (в виде строки). А также реализовать абстрактные методы базового класса Recipes. Создать несколько экземпляров дочерних классов и через общий интерфейс (в виде ссылок типа Recipes) вызвать методы showRecipe и showIngredients.

В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.

Что такое наследование?

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

Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.

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

В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.

Важное примечание: приватные переменные и методы не могут быть унаследованы.

Типы наследования

В C ++ есть несколько типов наследования:

  • публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
  • защищенный ( protected ) — все унаследованные данные становятся защищенными;
  • приватный ( private ) — все унаследованные данные становятся приватными.

Для базового класса Device , уровень доступа к данным не изменяется, но поскольку производный класс Computer наследует данные как приватные, данные становятся приватными для класса Computer .

Класс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .

Конструкторы и деструкторы

В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.

Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.

Конструкторы: Device -> Computer -> Laptop .
Деструкторы: Laptop -> Computer -> Device .

Множественное наследование

Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.

Проблематика множественного наследования

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

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

Проблема ромба

Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .

К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.

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

  • вызвать метод конкретного суперкласса;
  • обратиться к объекту подкласса как к объекту определенного суперкласса;
  • переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).

Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .

Проблема ромба: Конструкторы и деструкторы

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

Виртуальное наследование

Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.

Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).

Абстрактный класс

В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.

Интерфейс

С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).

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

Наследование от реализованного или частично реализованного класса

Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.

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

Интерфейс

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

Интерфейс: Пример использования

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

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

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

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

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

Изучить объектно-ориентированные концепции наследования и полиморфизма и принципы определения качества программ.

Задачи

Оглавление

Методы класса являются функциями (Function) или процедурами, подпрограммами (Sub). Функции возвращают значения, подпрограммы – нет; это соответствует вашим имеющимся знаниям по организации модульности программного кода. Область видимости методов регулируется ключевыми словами: Public (доступны всем), Private (используются только внутри самого класса), Friend (доступны внутри класса, но при условии, что создан экземпляр класса), Protected (доступны только внутри самого класса и наследуемых от него классов).

Передача аргументов и параметров в методы выполняется с помощью директивы ByVal / ByRef. Директива ByVal приводит к созданию физической копии данных из клиентского кода (аргумент) в методе (параметр). Передача данных ByRef приводит только к копированию ссылки. При этом клиентский код и метод имеют различные ссылки, которые указывают на один и тот же объект или данные, что может привести к побочным эффектам. В качестве входных параметров могут передаваться объекты и структуры: массивы, перечисления, другие классы.

Перегрузка методов

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

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

Метод CalculateCost (), который принимает два аргумента, объект Order и Double

Private Sub CalculateCost(ByVal objOrder As Order, ByVal OrderCost As Double)
End Sub

Имеет следующую сигнатуру:

В то же время функция CalculateCost()

Public Function CalculateCost(Byval OrderCost As Double, ByVal DeliveryFree As Double) As Double
Return OrderCost+DeliveryFree+HandilingCharge
End Function

Имеет следующую сигнатуру:

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

Если вы хотите перегрузить метод, необходимо соблюдать несколько правил:

  • Каждый из перегружаемых методов должен отличаться хотя бы одной из следующих характеристик: числом параметров, типом данных параметров, порядком следования параметров.
  • Каждый метод должен иметь одинаковое имя метода, иначе вы просто создадите абсолютно новый метод.
  • Function может перегружать Sub, если они имеют разные списки параметров.
  • Вы не можете перегрузить метод свойством с таким же именем, и наоборот.

Разделяемые (Shared) методы

Во фрагменте кода, приведенном ниже, приведен пример разделяемого метода SaveDrivers () для сохранения объектов типа Driver в коллекции, передаваемой в базу данных.

Imports System.Collections
Module SharedMethods
Public Class Driver
Private m_DriverName As String
Public Shared Function SaveDrivers(ByRef DriverCollection As ArrayList) As Integer
Dim objDriver As New Driver
For Each objDriver In DriverCollection
‘Код для сохранения объектов в базе данных
Next
End Function
End Class
Sub Main ()
‘Это коллекция, в которой мы храним данные
Dim DriverCol As New ArrayList()
‘ Это объект класса Driver
Dim objDriver1 As New Driver()
‘Добавим объект в коллекцию
DriversCol. Add (objDriver 1)
‘Вызов разделяемого метода из объекта класса Driver. Технически это возможно, но приводит к путанице в коде. Смысл разделяемого метода в том, что он не принадлежит ни одному из объектов класса, должен вызываться из самого класса.
objDriver 1. SaveDrivers (DriversCol)
‘Поскольку метод SaveDrivers () не принадлежит какому-либо конкретному экземпляру класса Driver. Этот метод должен быть вызван из самого класса:
Driver.SaveDrivers (DriverCol)
End Sub
End Module

Разработка иерархий наследования

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

Классы, которые наследуют от суперклассов, называются производными классами, дочерними классами или подклассами.

Рис. 8.1. Иерархия наследования

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

Рис. 8.2. Наследование и полиморфизм

Суперкласс определяет общие данные и операции, требуемые всеми подклассами. Подклассы наследуют все члены суперкласса и могут, если это необходимо, определять дополнительные члены. Диаграмма 8.2. показывает некоторые данные, и операции для простой иерархии BankAccount. Обратите внимание на следующие моменты в этой иерархии наследования:

Класс BankAccount определяет два члена-данных с именами Balance и Number, потому что все виды банковских счетов нуждаются в этой информации. Класс BankAccount также определяет операции с именами debit, Credit, PrintStatement и Funds, так как все виды банковских счетов требуют проведения этих операций.

Класс SavingAccount наследует все данные и операции класса BankAccount и определяет дополнительный член-данных (InterestRate) и дополнительную операцию (ApplyInterest), потому что накопительные счета предлагают проценты.

Класс CheckingAccount наследует все данные и операции класса BankAccoiuny и определяет дополнительный член-данных (CheksWritten) для хранения информации обо всех чеках, которые были выписаны для этого счета. Класс CheckingAccount также определяет дополнительную операцию (DebitByCheck), чтобы позволить пользователю снимать деньги по выписанному чеку.

Класс StudentsAccount наследует все данные и операции класса BankAccount и определяет дополнительный член-данных (OverdraftLimit), чтобы предоставить студенту возможность превысить кредит. Класс StudentAccount также определяет дополнительную операцию (IncreaseLimit), чтобы студенты могли повысить свой предел превышения кредита, если они попадают в сложную финансовую ситуацию.

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

Переопределение и полиморфизм

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

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

Если подкласс переопределяет переопределяемые операции, то он должен определить операцию с тем же именем и той же сигнатурой, что и операция суперкласса.

UML не предоставляет никакой специальной нотации для указания того, что операция переопределяемая; мы полагаем, что операция переопределяемая, если она переопределяется в одном или нескольких классах.

На следующей (рис. 8.3) диаграмме UML CheckingAccount и StudentAccount переопределяют операцию PrintStatement, предназначенную для распечатки дополнительной информации о счете. Класс StudentAccount также переопределяет операцию Founds, потому что доступные средства для студенческого счета зависят от возможности превышения кредита.

Рис. 8.3. Переопределение методов базового класса

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

Переопределенные методы у классов наследником помечаются ключевым словом (модификатором) Overrides, а у класса родителя – модификатором Overridable. Например,

Public Overridable Function PrintStatement() As String

Так будет выглядеть описание метода, подлежащего переопределению, у родительского класса BankAccount,

Public Overrides Function PrintStatement() As String

а так будет выглядеть сигнатура переопределенного метода у класса наследника StudentAccount.

Абстрактные классы и абстрактные методы

Абстрактный класс – это класс, для которого нельзя создавать экземпляры. Другими словами, клиентский код не может создать объект типа абстрактного класса. Суперклассы часто объявляются абстрактными, потому, что они не содержат достаточно информации, чтобы предоставлять реальные объекты нашей системы; суперклассы играют исключительно роль хранилища общих данных и операций, требуемых их подклассам. Для того чтобы создать такой абстрактный класс необходимо добавить ключевое слово MustInherit к оператору описания класса: Public MustInherit Class BankAccount.

Противоположностью абстрактного класса является конкретный класс. Клиентский код может создавать объекты типа конкретного класса. При описании дочернего класса следует указывать ключевое слово Inherit – наследник:

Public Class StudentAccount
Inherit BankAccount

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

Абстрактные методы в описании абстрактных классов должны быть помечены ключевым словом MustOverride. Более того, подобные методы могут иметь только заголовок (сигнатуру), но не иметь ключевых слов End Function или End Sub. Но таким образом описанные методы абстрактного класса должны обязательно быть переопределены в классах наследниках.

Критерии качества программ

Основными стандартами в области качества стали международные стандарты серии ИСО 9000, разработанные Международной организацией по стандартизации. Увеличивающаяся в настоящее время конкуренция между производителями программных продуктов приводит к установлению жестких требований к качеству этой продукции. Для того чтобы быть конкурентоспособными, организации должны применять эффективные системы, ведущие к повышению качества программ и более совершенному удовлетворению требований заказчика. Под системой качества понимается, согласно ИСО 8402, совокупность организационной структуры, методик, процессов и ресурсов, необходимых для осуществления общего руководства качеством продукции, производимой организацией.

Целью руководящих положений и требований международных стандартов ИСО 9000 является удовлетворение требований с позиции четырех аспектов, являющихся ключевыми для качества продукции.

  • Качество благодаря определению потребностей заказчиков.
  • Качество благодаря конструкции, то есть качество благодаря встраиванию в продукцию характеристик, способствующих тому, чтобы она отвечала требованиям и возможностям рынка.
  • Качество благодаря поддержанию постоянного соответствия конструкции, реализации характеристик, заложенных в проект.
  • Качество благодаря техническому обслуживанию продукции в процессе ее эксплуатации.
  • Функциональные возможности . Данная характеристика описывает свойства программы в части полноты удовлетворения требований пользователя и в этом смысле является определяющей для потребительских свойств программного обеспечения.
  • Надежность. Специфика программного обеспечения заключается в том, что оно не подвержено старению и износу, а отказы проявляются из-за ошибок в требованиях, проекте, реализации.
  • Практичность. При оценке этой характеристики следует исходить из требований пользователя.
  • Эффективность. Оценка данной характеристики также критически зависит от требований пользователя. Программа может оказаться неэффективной не в силу плохого кодирования, а в силу противоречивости и нереальности исходных требований.
  • Сопровождаемость. Мобильность. Для этих двух характеристик следует учитывать, что в специфических российских условиях им часто не уделяется достаточно внимании со стороны пользователя. Эти характеристики связаны с долгосрочным планированием развития программного обеспечения. Сопровождаемость – набор атрибутов, относящихся к объекту работ, требуемых для проведения конкретных изменений (модификации). Мобильность – набор атрибутов, относящихся к способности программы быть перенесенной из одного окружения в другое.

Выводы

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

Вопросы для самопроверки

Литература

Конструкторы и деструкторы классов. Методы как интерфейс класса. Сигнатура методов: процедуры и функции. Перегрузка методов и конструкторов класса – разновидность полиморфизма. Наследование классов. Базовый класс, его конструкторы, свойства, методы. Ключевые слова MyBase, MyClass, Me. Полиморфизм. Переопределенные методы. Абстрактные классы, их назначение и устройство. Применение модификаторов доступа в базовых классах: Public, Private, Protected. Использование затенения (shadowing) методов базового класса

Читайте также: