Чем сокрытие функций отличается от переопределения при наследовании

Обновлено: 16.06.2024

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

Задачи

Оглавление

Методы класса являются функциями (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) методов базового класса

Переопределение метода – это случай, когда подкласс и суперкласс содержат методы, которые имеют одинаковое имя и сигнатуру типов (рисунок 1). Переопределение метода возникает в случаях, когда классы образовывают иерархию наследования.

Java. Наследование. Пример переопределения метода суперкласса A в подклассе B

Рисунок 1. Пример переопределения метода method() суперкласса A в подклассе B

На рисунке 1 класс B наследует (расширяет) класс A . Класс A есть суперклассом для класса B . В подклассе B объявляется метод с таким же именем как и в суперклассе A . Итак, в данном случае, метод с именем method() суперкласса A есть переопределенным в подклассе B .

2. Какое отличие между переопределением и перегрузкой метода?

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

  • при переопределении методы суперкласса и подкласса носят одинаковое имя и одинаковую сигнатуру типов параметров (см. рис. 1 );
  • при перегрузке методы суперкласса и подкласса носят одинаковое имя но разные сигнатуры типов параметров. На рисунке 2 продемонстрирована перегрузка метода в иерархии наследования.

Рисунок 2. Пример перегрузки метода с именем method() в классах A , B , C которые образовывают иерархию

На рисунке 2 изображены 3 класса с именами A , B , C которые образуют иерархию наследования. Класс B наследует класс A . Класс C наследует класс B . Во всех классах реализованы разные методы, которые имеют одинаковое имя method() . Параметры метода в каждом классе отличаются. Это означает, что метод method() есть перегруженым.

3. Правило взаимодействия между методами суперкласса и подкласса, которые имеют одинаковые имена и сигнатуру. Пример

Если в суперклассе и подклассе реализованы методы, которые носят одинаковое имя и сигнатуру типов параметров, то действует следующее правило:

  • метод из подкласса переопределяет метод суперкласса.

На рисунке 3 схематически изображено правило взаимодействия между одноименными методами суперкласса и подкласса.

Java. Наследование. Переопределение метода суперкласса в методе подкласса

Рисунок 3. Демонстрация переопределения метода суперкласса в методе подкласса

На рисунке 3 изображен пример переопределения метода суперкласса в подклассе. Объявляется 3 класса с именами A , B , C . В каждом классе реализован метод с именем method() без параметров. Этот метод имеет одинаковую сигнатуру во всех классах.

Каждый объект (экземпляр) класса вызывает метод класса на который он был объявлен. Из экземпляра objB вызывается метод method() который реализован в классе B . Из экземпляра objC вызывается метод method() , который реализован в классе C .

Ниже приведен текст программы демонстрирующей переопределение методов, изображенных на рисунке 3.

Результат выполнения программы

4. Получение доступа из метода подкласса к методу суперкласса в случае, когда совпадают имена методов и сигнатуры их параметров. Ключевое слово super . Пример

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

На рисунке 4 изображен доступ к методу суперкласса A из подкласса B . В подклассе B есть метод с таким же именем и списком параметров, поэтому, этот метод переопределяет метод суперкласса. Чтобы доступиться к методу суперкласса A в методе подкласса B используется ключевое слово super .

Java. Наследование. Вызов метода суперкласса из метода подкласса с помощью ключевого слова super

Рисунок 4. Вызов метода суперкласса из метода подкласса с помощью ключевого слова super

Текст программы, которая демонстрирует рисунок 4 следующий

Результат выполнения программы

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

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

Метод считается перегруженным (не переопределенным) в случае, если:

  • в разных классах существует метод с таким же именем;
  • сигнатура параметров метода в каждом классе отличается.

Таким образом, методы с разными сигнатурами считаются перегруженными а не переопределенными.

На рисунке 5 изображен пример перегрузки метода с именем method() . Объявляются три класса с именами A , B , C которые образовывают иерархию. В классе A метод с именем method() реализован без параметров. В классе B метод с именем method() реализован с одним параметром типа int . В классе C метод с именем method() реализован с одним параметром типа double .

Java. Наследование. Перегрузка метода в классах, которые образовывают иерархию

Рисунок 5. Перегрузка метода с именем method() в классах, которые образовывают иерархию

Текст программы, которая демонстрирует рисунок 5 следующий

Результат работы программы

6. Что такое динамическая диспетчеризация методов? Пример реализации полиморфизма в Java

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

Динамическая диспетчеризация методов – это специальный механизм, который позволяет вызвать переопределенный метод в процессе выполнения программы а не во время компиляции. Динамическая диспетчеризация методов важна при реализации полиморфизма.

Рисунок 6 демонстрирует динамическую диспетчеризацию методов. Реализованы 3 класса с именами A , B , C которые образуют иерархию. В каждом классе реализован метод с именем method() . Сигнатура метода во всех классах одинаковая.

Java. Наследование. Динамическая диспетчеризация методов для трех классов

Рисунок 6. Демонстрация динамической диспетчеризации методов для трех классов, которые образуют иерархию

В начале кода демонстрации (рисунок 6) объявляется ссылка refA на базовый класс A

Теперь этой ссылке можно присваивать значение ссылки на подкласс класса A .

Таким образом, с помощью ссылки refA можно вызвать методы экземпляров классов A , B , C которые образуют иерархию. Если refA ссылается на экземпляр класса A , то строка

будет вызывать метод с именем method() класса A .

Аналогично, если ссылка refA ссылается на экземпляр (объект) класса B , то

будет вызывать соответствующий метод класса B .

Вышесказанное касается и класса C .

Как видно из рисунка 6, соответствующий вариант метода method() определяется типом объекта, на который ссылается ссылка refA , а не типом этой ссылки в момент ее объявления.

Ниже приведен программный код, который демонстрирует динамическую диспетчеризацию методов, изображенную на рисунке 6.

Результат выполнения программы

7. Пример, демонстрирующий способы реализации полиморфизма (динамической диспетчеризации методов)

Полиморфизм может быть реализован в программах двумя способами:

  • при присваивании ( = ) ссылке на базовый класс экземпляра любого производного класса. В этом случае, с помощью ссылки (через символ ‘ . ‘) вызывается переопределенный метод того экземпляра, на который в данный момент указывает эта ссылка;
  • при реализации некоторого метода, который получает параметром ссылку на базовый класс. В этом случае, в теле метода вызывается переопределенный метод по ссылке (метод, имеет общее имя и сигнатуру во всех классах иерархии). От того, на экземпляр какого класса указывает ссылка, зависит какой именно переопределенный метод будет вызван.

Нижеследующий пример демонстрирует эти два случая на примере иерархии из трех классов A , B , C . Все классы имеют метод Print() без параметров.

Для успокоения совести следует разрешить видимый парадокс, отмеченный выше (обнаружение того, что MB не является подмножеством MA ), так как мы хотим, чтобы некоторое отношение подмножества имело место между наследником и родителем. И это отношение реально существует, парадокс лишь показывает, что декартово произведение атрибутов не является подходящей моделью для моделирования класса. Рассмотрим класс:

Мы не должны выбирать в качестве математической модели C' - множества экземпляров C - декартово произведение T'1 _ T'2 _ T'3, где знак штрих ' указывает на рекурсивное использование модели множеств , приводящее к парадоксу (наряду с другими недостатками).

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

T

  • A1 Функция определена для c1 , c2 и c3 .
  • A2 Множество VALUE (множество цели для функции) является супермножеством .
  • A3 Значения функции для c1 лежат в T'1 и так далее.

Тогда, если вспомнить, что функция является специальным случаем отношения и что отношение является множеством пар (например, в ранее упоминаемом случае экземпляр класса A может быть промоделирован функцией <> , а экземпляр класса B - ), мы получаем ожидаемое свойство - B' является подмножеством A' . Заметьте, здесь уже элементы обоих множеств являются парами и первая функция задает все возможные отображения второго атрибута.

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

Это обсуждение дает только схему математической модели. С деталями использования частичных функций для моделирования кортежей и общими математическими основами можно ознакомиться в [M 1990].

Наследование вариаций

Определение: Наследование вариаций типа и функций

Наследование вариаций применяется, если B переопределяет некоторые компоненты A ; A и B являются оба либо отложенными, либо эффективными. Класс B не должен вводить никаких новых компонентов за исключением тех, что непосредственно необходимы переопределяемым компонентам. Здесь рассматриваются два случая:

  • Наследование вариаций функций: переопределения действуют на тела компонентов, но не на их сигнатуры.
  • Наследование вариаций типа: все переопределения являются переопределениями сигнатур.

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

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

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

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

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

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

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

Наследование функциональной вариации является прямым приложением принципа Открыт-Закрыт: мы хотим адаптировать существующий класс, не затрагивая оригинал (к коду которого мы можем и не иметь доступа) и его клиентов. Это может стать предметом злоупотреблений, некоей формой хакерства, перекручивая существующий класс, приспосабливая его для других целей. Во всяком случае это будет организованное хакерство, позволяющее избежать угроз модификации существующего ПО. Но если есть доступ к оригиналу, то предпочтительной может оказаться реорганизация иерархии наследования путем введения абстрактного класса , для которого как уже существующий класс A, так и новичок B будут его потомками или подходящими наследниками с равным статусом.

Отмена эффективизации

Определение: Наследование с Отменой эффективизации

Наследование с отменой эффективизации применимо, если B переопределяет некоторые из эффективных компонентов A , преобразуя их в отложенные компоненты.

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

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

Для этой категории наследования класс B будет отложенным; A обычно эффективным, но может быть частично отложенным.

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

Перейдем теперь к третьей и последней группе категорий - программному наследованию.

Определение: Наследование с конкретизацией

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

Примером, используемым многократно в предыдущих лекциях, является отложенный класс TABLE , описывающий таблицы самой общей природы. Конкретизация ведет к потомкам SEQUENTIAL_ TABLE и HASH_TABLE , все еще отложенным. Заключительная конкретизация SEQUENTIAL_TABLE приводит к эффективным классам ARRAYED_TABLE , LINKED_TABLE , FILE_TABLE .

Термин "конкретизация" (reification), введенный Георгом Лукасом, происходит от латинского слова, означающего "превращение в вещь". Он используется в спецификациях и методе разработки VDM .

Структурное наследование

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

Структурное наследование применяется, если A , отложенный класс, представляет общее структурное свойство, а B , который может быть отложенным или эффективным, представляет некоторый тип объектов, обладающих этим свойством.

Наследование таким способом от нескольких родителей является обычным приемом. Например, класс INTEGER в библиотеке Kernel наследует от COMPARABLE и класса NUMERIC (с такими компонентами, как infix "+" и infix "*" ), задающего арифметические свойства. (Класс NUMERIC более точно представляет математическое понятие кольца.)

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

Валден и Нерсон заметили, что новички иногда верят, что они используют подобную форму наследования, подменяя фактически имеющее место отношение "is" вариантом схемы " car -owner" ( AIRPLANE наследуется от VENTILATION_SYSTEM ). Они указывают, что этой ошибки просто избежать благодаря абсолютному критерию, не оставляющему места для сомнений или двусмысленности:

При схеме наследования, хотя наследуемые свойства являются вторичными, они все же являются свойствами всего объекта, описываемого классом. Если мы делаем AIRPLANE наследником COMPARABLE , то отношение порядка применимо к каждому самолету как к целому, но свойства VENTILATION_SYSTEM не таковы. Компонент stop VENTILATION_SYSTEM не прекращает полет самолета.

Заключение в этом примере очевидно: AIRPLANE должен быть клиентом, а не наследником класса VENTILATION_SYSTEM .

  1. Функция foo() внутри дочернего объекта скрывает foo() родителя.
  2. Функциональная bar() внутри дочернего объекта скрывает bar() родителя.
  3. Функция foobar() внутри дочернего объекта скрывает bar() родительского()

Во-первых, 1 и 3 одинаковы, компилятор /IDE (Visual Studio) автоматически предполагает, что вы должны делать номер 3, если вы делаете номер 1. Вы заметите, что он помещает зеленую строку под именем foo() и сообщает вам используйте new ключевое слово. Таким образом, на самом деле существует только два типа метода наследования, основанного на наследовании:

  1. Метод переопределения
  2. Скрытие метода

Переопределение метода – это когда класс объявляет свой метод виртуальным, что по сути говорит, что “любой класс, который наследуется от меня, может переопределить это”. Это означает, что независимо от того, что, если дочерний класс переопределяет этот метод, вызывается переопределенная версия.

С другой стороны, скрытие метода – это ребенок, который бунтует и говорит: “Пока я существую как дочерний класс, я проигнорирую свою родительскую реализацию и сделаю это вместо этого”. Трюк здесь в том, что в тот момент, когда дочерний класс рассматривается как его родитель, вместо него вызывается родительская реализация.

Вот пример, предполагая, что используется ваша настройка класса:

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

Теперь попробуйте добавить этот класс в микс:

И запустить аналогичный тест:

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

Это понятие известно как полиморфизм.

Чтобы увидеть разницу, попробуйте следующий код:

В этом случае у вас есть переменная типа Parent которая указывает на экземпляр класса Parent . Здесь нет ничего скрытого или переопределенного. Вызывается методы родительского класса. Теперь создайте переменную типа Child , которая указывает на экземпляр Child :

В этом случае вызывается все методы дочернего класса. И ничего интересного. Теперь создайте переменную типа Parent , которая указывает на экземпляр Child :

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

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

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

Child.foo() ‘скрывает унаследованный элемент’ Parent.foo() ‘. Используйте новое ключевое слово, если нужно было скрывать

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

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

Для успокоения совести следует разрешить видимый парадокс, отмеченный выше (обнаружение того, что MB не является подмножеством MA ), так как мы хотим, чтобы некоторое отношение подмножества имело место между наследником и родителем. И это отношение реально существует, парадокс лишь показывает, что декартово произведение атрибутов не является подходящей моделью для моделирования класса. Рассмотрим класс:

Мы не должны выбирать в качестве математической модели C' - множества экземпляров C - декартово произведение T'1 _ T'2 _ T'3, где знак штрих ' указывает на рекурсивное использование модели множеств , приводящее к парадоксу (наряду с другими недостатками).

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

T

  • A1 Функция определена для c1 , c2 и c3 .
  • A2 Множество VALUE (множество цели для функции) является супермножеством .
  • A3 Значения функции для c1 лежат в T'1 и так далее.

Тогда, если вспомнить, что функция является специальным случаем отношения и что отношение является множеством пар (например, в ранее упоминаемом случае экземпляр класса A может быть промоделирован функцией <> , а экземпляр класса B - ), мы получаем ожидаемое свойство - B' является подмножеством A' . Заметьте, здесь уже элементы обоих множеств являются парами и первая функция задает все возможные отображения второго атрибута.

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

Это обсуждение дает только схему математической модели. С деталями использования частичных функций для моделирования кортежей и общими математическими основами можно ознакомиться в [M 1990].

Наследование вариаций

Определение: Наследование вариаций типа и функций

Наследование вариаций применяется, если B переопределяет некоторые компоненты A ; A и B являются оба либо отложенными, либо эффективными. Класс B не должен вводить никаких новых компонентов за исключением тех, что непосредственно необходимы переопределяемым компонентам. Здесь рассматриваются два случая:

  • Наследование вариаций функций: переопределения действуют на тела компонентов, но не на их сигнатуры.
  • Наследование вариаций типа: все переопределения являются переопределениями сигнатур.

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

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

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

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

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

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

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

Наследование функциональной вариации является прямым приложением принципа Открыт-Закрыт: мы хотим адаптировать существующий класс, не затрагивая оригинал (к коду которого мы можем и не иметь доступа) и его клиентов. Это может стать предметом злоупотреблений, некоей формой хакерства, перекручивая существующий класс, приспосабливая его для других целей. Во всяком случае это будет организованное хакерство, позволяющее избежать угроз модификации существующего ПО. Но если есть доступ к оригиналу, то предпочтительной может оказаться реорганизация иерархии наследования путем введения абстрактного класса , для которого как уже существующий класс A, так и новичок B будут его потомками или подходящими наследниками с равным статусом.

Отмена эффективизации

Определение: Наследование с Отменой эффективизации

Наследование с отменой эффективизации применимо, если B переопределяет некоторые из эффективных компонентов A , преобразуя их в отложенные компоненты.

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

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

Для этой категории наследования класс B будет отложенным; A обычно эффективным, но может быть частично отложенным.

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

Перейдем теперь к третьей и последней группе категорий - программному наследованию.

Определение: Наследование с конкретизацией

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

Примером, используемым многократно в предыдущих лекциях, является отложенный класс TABLE , описывающий таблицы самой общей природы. Конкретизация ведет к потомкам SEQUENTIAL_ TABLE и HASH_TABLE , все еще отложенным. Заключительная конкретизация SEQUENTIAL_TABLE приводит к эффективным классам ARRAYED_TABLE , LINKED_TABLE , FILE_TABLE .

Термин "конкретизация" (reification), введенный Георгом Лукасом, происходит от латинского слова, означающего "превращение в вещь". Он используется в спецификациях и методе разработки VDM .

Структурное наследование

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

Структурное наследование применяется, если A , отложенный класс, представляет общее структурное свойство, а B , который может быть отложенным или эффективным, представляет некоторый тип объектов, обладающих этим свойством.

Наследование таким способом от нескольких родителей является обычным приемом. Например, класс INTEGER в библиотеке Kernel наследует от COMPARABLE и класса NUMERIC (с такими компонентами, как infix "+" и infix "*" ), задающего арифметические свойства. (Класс NUMERIC более точно представляет математическое понятие кольца.)

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

Валден и Нерсон заметили, что новички иногда верят, что они используют подобную форму наследования, подменяя фактически имеющее место отношение "is" вариантом схемы " car -owner" ( AIRPLANE наследуется от VENTILATION_SYSTEM ). Они указывают, что этой ошибки просто избежать благодаря абсолютному критерию, не оставляющему места для сомнений или двусмысленности:

При схеме наследования, хотя наследуемые свойства являются вторичными, они все же являются свойствами всего объекта, описываемого классом. Если мы делаем AIRPLANE наследником COMPARABLE , то отношение порядка применимо к каждому самолету как к целому, но свойства VENTILATION_SYSTEM не таковы. Компонент stop VENTILATION_SYSTEM не прекращает полет самолета.

Заключение в этом примере очевидно: AIRPLANE должен быть клиентом, а не наследником класса VENTILATION_SYSTEM .

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