ABAP ООП Ч.5. Полиморфизм

ABAP ООП

Полиморфизм

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

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

Статические и динамические типы

Статический тип объекта (ссылочная переменная) — это тип класса, который используется для определения объекта. Например,  ссылка oref определяется ссылкой на класс lcl_class. В этом случае статический тип объекта lo_oref — это класс lcl_class, поскольку он статически определен в коде.
CLASS lcl_class DEFINITION.
  PUBLIC SECTION.
    METHODS meth.
ENDCLASS.
CLASS lcl_class IMPLEMENTATION.
  METHOD meth.
  ENDMETHOD.
ENDCLASS.
DATA lo_oref TYPE REF TO lcl_class.
Иногда вам может понадобиться присвоить один ссылочный объект другому ссылочному объекту, который не имеет того же статического типа. Например, поскольку экземпляры классов в дереве наследования взаимозаменяемы, вы можете присвоить экземпляр подкласса экземпляру суперкласса, как показано ниже, с помощью оператора parent = child.
CLASS lcl_parent DEFINITION.
  PUBLIC SECTION.
    METHODS meth1.
    METHODS meth2.
ENDCLASS.
CLASS lcl_parent IMPLEMENTATION.
  METHOD meth1.
    WRITE /'1 Метод родителя'.
  ENDMETHOD.
  METHOD meth2.
    WRITE /'2 Метод родителя'.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_child DEFINITION INHERITING FROM lcl_parent.
  PUBLIC SECTION.
    METHODS meth2 REDEFINITION.
    METHODS meth3.
ENDCLASS.
CLASS lcl_child IMPLEMENTATION.
  METHOD meth2.
    WRITE /'2 метод в классе-наследнике'.
  ENDMETHOD.
  METHOD meth3.
    WRITE /'3 Метод в классе-наследнике'.
  ENDMETHOD.
ENDCLASS.
DATA lo_parent TYPE REF TO lcl_parent.
DATA lo_child TYPE REF TO lcl_child.

START-OF-SELECTION.
  CREATE OBJECT lo_parent.
  CREATE OBJECT lo_child.
  lo_parent->meth2( ).
  lo_parent = lo_child.
  lo_parent->meth2( ).
После создания экземпляра родительского объекта ссылки с помощью оператора CREATE OBJECT он устанавливает указатель (в памяти) родительского объекта ссылки на класс lcl_parent. Указатель, на который указывает ссылочный объект во время выполнения, называется динамическим типом объекта.

На данный момент статический и динамический типы родительского объекта совпадают. Оператор lo_parent = lo_child присваивает экземпляр дочернего объекта родительскому объекту. Когда один эталонный объект назначается другому эталонному объекту, сам объект не назначается; перемещается только указатель объекта. Другими словами, после присваивания lo_parent = lo_child родительский объект указывает на место в памяти дочернего объекта. После этого назначения динамический тип родительского объекта изменяется на класс lcl_child, а статический тип — на lcl_parent.

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

При первом вызове метода lo_parent->meth2( ) до присваивания lo_parent = lo_child (то есть до изменения его динамического типа) выполняется код метода meth2 суперкласса lcl_parent. Однако тот же оператор после присваивания lo_parent = lo_child будет выполнять код в методе meth2 подкласса lcl_child, эффективно отображая полиморфизм, поскольку один и тот же объект ведет себя по-разному во время выполнения программы.

Casting

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

В примере выше родительский объект lcl_parent является более общим, чем дочерний объект lcl_child (суперкласс всегда будет таким же или более общим, чем подкласс). Другими словами, родительский класс реализовал два метода, тогда как подкласс стал более конкретным, реализовав третий метод, недоступный в родительском классе. Таким образом, родительский класс является более общим, чем дочерний класс, и по этой причине мы можем присвоить дочерний объект родительскому объекту.

Возможны два разных типа операций приведения (Cast): Narrowing (Up) cast и Widening (Down) Cast
ООП ABAP


Narrowing (Up) cast

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

Например, скажем, у родительского класса есть два метода, meth1 и meth2, а у дочернего класса есть дополнительный метод, meth3, который не определен в родительском классе. После операции приведения ( cast ) вы можете получить доступ к методам meth1 и meth2 дочернего класса только с помощью ссылки на родительский объект. Если вы попытаетесь получить доступ к методу meth3, это приведет к синтаксической ошибке, как показано ниже. Однако вы можете получить доступ к методу meth3, используя ссылку на дочерний объект, потому что он все еще существует в программе.

  lo_parent = child."Назначает дочернюю ссылку родительской.
  lo_parent->meth1( )."Вызовите метод math 1 в дочернем элементе, если он определен.
  lo_parent->meth2( )."Вызовите метод math 2 в дочернем элементе, если он определен.
  lo_parent->meth3( )."Приводит к синтаксической ошибке.
  lo_child->meth3( )."Работает как ожидалось..
Узкое приведение также можно выполнить во время создания экземпляра, добавив TYPE к оператору CREATE OBJECT, как показано ниже:
  
DATA lo_parent TYPE REF TO lcl_parent.
CREATE OBJECT lo_parent TYPE lcl_child.

Widening (down) Cast 

Расширяющее приведение происходит, когда статический тип целевого ссылочного объекта является более конкретным, чем статический тип исходного ссылочного объекта. Поскольку это противоречит правилу приведения, которое указывает, что целевой объект всегда должен быть более общим, чем исходный объект, средство проверки синтаксиса будет жаловаться, что исходный объект не может быть преобразован в целевой объект, если вы попытаетесь присвоить объекты с помощью оператор = или используйте оператор MOVE. Чтобы обойти это ограничение и сообщить компилятору, что мы знаем, что делаем, мы используем оператор ?= для расширения приведения, как показано ниже:
  
lo_child ?= lo_parent.
Использование оператора ?= только статически обходит проверку во время компиляции и откладывает ее до времени выполнения. Это вызовет ошибку времени выполнения, если целевой объект не является более общим, чем исходный объект.

Например, код ниже пройдет проверку синтаксиса (статическую проверку), но приведет к ошибке времени выполнения, поскольку дочерний объект является более конкретным, чем родительский объект.
  
CLASS lcl_class DEFINITION.
  PUBLIC SECTION.
    METHODS meth.
ENDCLASS.
CLASS lcl_class IMPLEMENTATION.
  METHOD meth.
  ENDMETHOD.
ENDCLASS.
DATA lo_oref TYPE REF TO lcl_class.

CLASS lcl_parent DEFINITION.
  PUBLIC SECTION.
    METHODS meth1.
    METHODS meth2.
ENDCLASS.
CLASS lcl_parent IMPLEMENTATION.
  METHOD meth1.
    WRITE /'1 Метод родителя'.
  ENDMETHOD.
  METHOD meth2.
    WRITE /'2 Метод родителя'.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_child DEFINITION INHERITING FROM lcl_parent.
  PUBLIC SECTION.
    METHODS meth2 REDEFINITION.
    METHODS meth3.
ENDCLASS.
CLASS lcl_child IMPLEMENTATION.
  METHOD meth2.
    WRITE /'2 метод в классе-наследнике'.
  ENDMETHOD.
  METHOD meth3.
    WRITE /'3 Метод в классе-наследнике'.
  ENDMETHOD.
ENDCLASS.
DATA lo_parent TYPE REF TO lcl_parent.
DATA lo_child TYPE REF TO lcl_child.

START-OF-SELECTION.
  CREATE OBJECT lo_parent.
  CREATE OBJECT lo_child.
  lo_child ?= lo_parent."Выдает ошибку времени выполнения
Расширяющее приведение (down cast) следует использовать только в том случае, если динамический тип целевого объекта будет универсальным, чем исходный объект. Фактически это означает, что сужающее приведение должно выполняться перед расширяющим приведением, как ниже. Расширяющие приведения могут сбивать с толку и быть опасными, поэтому их следует использовать с осторожностью.
  
CLASS lcl_parent DEFINITION.
  PUBLIC SECTION.
    METHODS meth1.
    METHODS meth2.
ENDCLASS.
CLASS lcl_parent IMPLEMENTATION.
  METHOD meth1.
    WRITE '1 Метод родителя'.
  ENDMETHOD.

  METHOD meth2.
    WRITE '2 Метод родителя'.
  ENDMETHOD.
ENDCLASS.
CLASS lcl_child1 DEFINITION INHERITING FROM lcl_parent.
  PUBLIC SECTION.
    METHODS meth2 REDEFINITION.
    METHODS meth3.
ENDCLASS.
CLASS lcl_child1 IMPLEMENTATION.
  METHOD meth2.
    WRITE '2 метод в классе-наследнике 1'.
  ENDMETHOD.
  METHOD meth3.
    WRITE '3 метод в классе-наследнике 1'.
  ENDMETHOD.
ENDCLASS.
CLASS lcl_child2 DEFINITION INHERITING FROM lcl_child1.
  PUBLIC SECTION.
    METHODS meth2 REDEFINITION.
    METHODS meth3 REDEFINITION.
    METHODS meth4.
ENDCLASS.
CLASS lcl_child2 IMPLEMENTATION.
  METHOD meth2.
    WRITE '2 метод в классе-наследнике 2'.
  ENDMETHOD.
  METHOD meth3.
    WRITE '3 метод в классе-наследнике 2'.
  ENDMETHOD.
  METHOD meth4.
    WRITE '4 метод в классе-наследнике 2'.
  ENDMETHOD.
ENDCLASS.
DATA lo_parent TYPE REF TO lcl_parent.
DATA lo_child1 TYPE REF TO lcl_child1.
DATA lo_child2 TYPE REF TO lcl_child2.

START-OF-SELECTION.
  CREATE OBJECT lo_parent.
  CREATE OBJECT lo_child1.
  CREATE OBJECT lo_child2.
  lo_parent = lo_child2.
  lo_child1 ?= lo_parent.
  lo_child1->meth2( ).
В коде определены три класса: lcl_parent, lcl_child1 и lcl_child2. lcl_child1 наследуется от lcl_parent, а lcl_child2 наследуется от lcl_child1. Сначала выполняется узкое приведение( up cast ) путем присвоения объекта lo_child2 родительскому объекту, а затем выполняется расширяющее приведение( down cast ) путем присвоения родительского объекта дочернему lo_child1. Когда метод meth2 вызывается с использованием ссылки на объект lp_child1 после расширяющего приведения( down cast ), он выполняет код в методе meth2 класса cl_child2.

Динамическое связывание с СALL METHOD

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

Чтобы понять динамическое связывание с помощью CALL METHOD, давайте посмотрим на фрагмент кода ниже. Здесь мы определили lcl_student как абстрактный класс с tuition_fee и get_fee как абстрактные методы с общедоступной видимостью. В защищенном разделе определен атрибут mv_fee_paid, который можно установить, если плата оплачена студентом.

Два класса, lcl_commerce_student и lcl_science_student, наследуют класс lcl_student и поддерживают собственную реализацию абстрактных методов tuition_fee и get_fee. В методе tuition_fee атрибуту private mv_fee_paid присваивается значение abap_true, если студент уплатил взнос. Метод get_fee возвращает значение закрытого атрибута mv_fee_paid.

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

Метод set_student класса lcl_admission определен с параметром импорта для импорта объекта студента lcl_student. Если мы сохраняем статический тип этого параметра импорта как lcl_commerce_student, он может импортировать только объект студента-коммерции, а наш класс может обрабатывать только допуск для студентов-коммерции. Точно так же, если мы сохраняем статический тип параметра импорта как lcl_science_student, он может импортировать только объект студента-естественника, а наш класс может обрабатывать только допуск для студентов-естественников. Чтобы он работал для любой категории студентов, мы сохранили параметр импорта этого метода как статический тип абстрактного класса lcl_student, от которого наследуются классы lcl_science_student и lcl_commerce_student.
  
CLASS lcl_student DEFINITION ABSTRACT.
  PUBLIC SECTION.
    METHODS tuition_fee ABSTRACT.
    METHODS get_fee ABSTRACT RETURNING VALUE(rv_fee_paid) TYPE boolean.
  PROTECTED SECTION.
    DATA mv_fee_paid TYPE boolean.
ENDCLASS.

CLASS lcl_commerce_student DEFINITION INHERITING FROM lcl_student.
  PUBLIC SECTION.
    METHODS tuition_fee REDEFINITION.
    METHODS get_fee REDEFINITION.
ENDCLASS.
CLASS lcl_commerce_student IMPLEMENTATION.
  METHOD tuition_fee.
*логика для расчета платы за обучение для студентов,
*изучающих коммерцию,
    mv_fee_paid = abap_true.
  ENDMETHOD.
  METHOD get_fee.
    rv_fee_paid = me->mv_fee_paid.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_science_student DEFINITION INHERITING FROM lcl_student.
  PUBLIC SECTION.
    METHODS tuition_fee REDEFINITION.
    METHODS get_fee REDEFINITION.
ENDCLASS.
CLASS lcl_science_student IMPLEMENTATION.
  METHOD tuition_fee.
*логика для расчета платы за обучение для студентов,
*изучающих естественные науки
    mv_fee_paid = abap_true.
  ENDMETHOD.
  METHOD get_fee.
    rv_fee_paid = me->mv_fee_paid.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_admission DEFINITION.
  PUBLIC SECTION.
    METHODS set_student IMPORTING io_student TYPE REF TO lcl_student.
    METHODS enroll.
  PRIVATE SECTION.
    DATA mv_admit TYPE boolean.
ENDCLASS.
CLASS lcl_admission IMPLEMENTATION.
  METHOD set_student.
    IF io_student->get_fee( ) EQ abap_true.
      mv_admit = abap_true.
    ENDIF.
  ENDMETHOD.
  METHOD enroll.
    IF mv_admit EQ abap_true.
*Зачисления студента на курс
    ENDIF.
  ENDMETHOD.
ENDCLASS.
DATA : lo_commerce_student TYPE REF TO lcl_commerce_student,
       lo_science_student  TYPE REF TO lcl_science_student,
       lo_admission        TYPE REF TO lcl_admission.

START-OF-SELECTION.
  CREATE OBJECT: lo_commerce_student,
                 lo_science_student,
                 lo_admission.

  CALL METHOD lo_commerce_student->tuition_fee.
  CALL METHOD lo_admission->set_student( EXPORTING io_student = lo_commerce_student ).
  CALL METHOD lo_admission->enroll.

  CALL METHOD lo_science_student->tuition_fee.
  CALL METHOD lo_admission->set_student( EXPORTING io_student = lo_science_student ).
  CALL METHOD lo_admission->enroll.
Когда вызывается метод set_student класса lcl_admission, он позволяет нам передать любой объект io_student, унаследованный от lcl_student. В этом методе мы проверяем, оплатил ли студент плату, вызывая метод get_fee импортированного объекта и соответствующим образом обрабатывая допуск студента. Здесь, несмотря на то, что статический тип импортируемого объекта отличается от статического типа параметра импорта, система автоматически привязывает объект к правильному экземпляру.

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

Интерфейсы


Подобно Java, объекты ABAP поддерживают только одиночное наследование и не поддерживают множественное наследование, как в C++. Одиночное наследование означает, что класс может иметь несколько подклассов, но только один суперкласс. Многие подклассы могут использовать тот же класс, что и их суперкласс, но каждый подкласс может иметь только один суперкласс.

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

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

Синтаксис для определения интерфейса показан ниже, в котором интерфейс lif_student определяется с помощью оператора INTERFACE и заканчивается оператором ENDINTERFACE.
INTERFACE lif_student.
  DATA mv_course TYPE char10.
  METHODS meth1.
  EVENTS enrolled.
ENDINTERFACE.
CLASS lcl_science DEFINITION.
  PUBLIC SECTION.
    INTERFACES lif_student.
ENDCLASS.
CLASS lcl_science IMPLEMENTATION.
  METHOD lif_student~meth1.
  ENDMETHOD.
ENDCLASS.
Интерфейс может иметь все компоненты, поддерживаемые классом, такие как методы, атрибуты, события и т. д. Все компоненты по умолчанию находятся в ОБЩЕСТВЕННОМ РАЗДЕЛЕ, и вы не можете использовать какой-либо раздел видимости в определении интерфейса. Поскольку интерфейс обычно создается для использования в нескольких классах, имеет смысл использовать интерфейс для определения общедоступного интерфейса класса, при этом каждый класс, использующий интерфейс, реализует свои собственные частные или защищенные компоненты.

Класс lcl_science реализует интерфейс lif_student с помощью оператора INTERFACES. В реализации класса lcl_science реализован метод meth1, определенный в интерфейсе lif_student. Поскольку метод meth1 определен в интерфейсе, а не в самом классе, доступ к методу осуществляется с использованием синтаксиса имя_интерфейса~имя_компонента. Здесь символ тильды (~) — это селектор компонентов интерфейса, который следует использовать для доступа к компоненту интерфейса.

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

Иногда имя интерфейса может быть слишком длинным. Чтобы избежать обращения к компоненту интерфейса с использованием длинного имени интерфейса, мы можем использовать псевдонимы, чтобы упростить кодирование, как показано ниже.
INTERFACE lif_student.
  DATA mv_course TYPE char10.
  METHODS meth1.
  EVENTS enrolled.
ENDINTERFACE.
CLASS lcl_science DEFINITION.
  PUBLIC SECTION.
    INTERFACES lif_student.
    ALIASES m1 FOR lif_student~meth1.
ENDCLASS.
CLASS cl_science IMPLEMENTATION.
  METHOD m1.
  ENDMETHOD.
ENDCLASS.
DATA lo_science TYPE REF TO lcl_science.

START-OF-SELECTION.
  CREATE OBJECT lo_science.
  lo_science->m1( ).
Интерфейсы могут быть вложены с помощью оператора INTERFACES, чтобы включить интерфейс в интерфейс. В коде ниже интерфейс lif_student вложен внутрь lif_college. Псевдоним определен для метода meth1 lif_student в lif_college. Класс lcl_science реализует интерфейс lif_college. Доступ к методу meth1 интерфейса lif_student осуществляется внутри класса с использованием псевдонима, определенного для него в lif_college.
INTERFACE lif_student.
  DATA mv_course TYPE char10.
  METHODS meth1.
  EVENTS enrolled.
ENDINTERFACE.
INTERFACE lif_college.
  INTERFACES lif_student.
  ALIASES m1 FOR lif_student~meth1.
  METHODS meth1.
ENDINTERFACE.
CLASS lcl_science DEFINITION.
  PUBLIC SECTION.
    INTERFACES lif_college.
    ALIASES m1 FOR lif_college~m1.
    ALIASES m2 FOR lif_college~meth1.
ENDCLASS.
CLASS lcl_science IMPLEMENTATION.
  METHOD m1.
  ENDMETHOD.
  METHOD m2.
  ENDMETHOD.
ENDCLASS.
DATA lo_science TYPE REF TO lcl_science.

START-OF-SELECTION.
  CREATE OBJECT lo_science.
  lo_science->m1( ).

Комментарии