Вам бонус- начислено 1 монета за дневную активность. Сейчас у вас 1 монета

Интерфейсы в Объектно-ориентированном программировании

Лекция



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

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

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

Способ взаимодействия программных компонентов

  • Прикладной программный интерфейс (API) — набор стандартных библиотечных методов, которые программист может использовать для доступа к функциональности другой программы.
  • Удаленный вызов процедур
  • COM-интерфейс
  • Интерфейс объектно-ориентированного программирования — описание методов взаимодействия объектов приложения на уровне исходного кода
  • Запись голосовой команды в мобильном приложении или веб-браузере информационных систем; дополнение аудиозаписи идентификатором и прочими метаданными; передача в Интеграционную шину Ассистента речевого управления произвольным интерфейсом; получение от Интеграционной шины идентификатора распознанной команды и ее параметра; отправка и исполнение распознанной голосовой команды управления веб-интерфейсом на стороне информационной системы.
  • Через графический веб-интерфейс, имеющий картографическую основу и позволяющий визуализировать прием, обработку, регистрацию и передачу данных, обеспечивая предоставление цифровых услуг. Область применения: информационное обеспечение и взаимодействие судов и береговых систем мониторинга и управления. Функциональные возможности: сопряжение с сервисами e-Навигации; обеспечение интерактивной работы с сервисами e-Навигации; отображение данных на электронной навигационной карте; предоставление пользователю необходимых инструментов для работы с сервисами е-Навигации.

Интерфейс (в объектно-ориентированном программировании)

Интерфе́йс (англ. interface) — структура программы/синтаксиса, определяющая отношение с объектами, объединенными только некоторым поведением. При проектировании классов, разработка интерфейса тождественна разработке спецификации (множества методов, которые каждый класс, использующий интерфейс, должен реализовывать).

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

Интерфейс в ООП является строго формализованным элементом объектно-ориентированного языка и широко используется в исходном коде программ.

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

Описание и использование интерфейсов

Описание ООП-интерфейса, если отвлечься от деталей синтаксиса конкретных языков, состоит из двух частей: имени и методов интерфейса.

  • Имя интерфейса строится по тем же правилам, что и другие идентификаторы используемого языка программирования. Разные языки и среды разработки имеют различные соглашения по оформлению кода, в соответствии с которыми имена интерфейсов могут формироваться по некоторым правилам, которые помогают отличать имя интерфейса от имен других элементов программы. Например, в технологии COM и во всех поддерживающих ее языках действует соглашение, следуя которому, имя интерфейса строится по шаблону «I<Имя>», то есть состоит из написанного с заглавной буквы осмысленного имени, которому предшествует заглавная латинская буква I (IUnknown, IDispatch, IStringList и т. п.).
  • Методы интерфейса. В описании интерфейса определяются имена и сигнатуры входящих в него методов, то есть процедур или функций класса.

Использование интерфейсов возможно двумя способами:

  • Класс может реализовывать интерфейс. Реализация интерфейса заключается в том, что в описании класса данный интерфейс указывается как реализуемый, а в коде класса обязательно определяются все методы, которые описаны в интерфейсе, в полном соответствии с сигнатурами из описания этого интерфейса. То есть, если класс реализует интерфейс, для любого экземпляра этого класса существуют и могут быть вызваны все описанные в интерфейсе методы. Один класс может реализовать несколько интерфейсов одновременно.
  • Возможно объявление переменных и параметров методов как имеющих тип «интерфейс». В такую переменную или параметр может быть записан экземпляр любого класса, реализующего интерфейс. Если интерфейс объявлен как тип возвращаемого значения функции, это означает, что функция возвращает объект класса, реализующего данный интерфейс.

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

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

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

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

Что может и что не может содержать интерфейс

«Чистый» интерфейс содержит только названия функций, которые будут определены где-то в классе-потомке, без тел. Однако для удобства программиста языки программирования и требования к коду могут допускать в интерфейсах определенные виды данных и функций.

  • Функции-утилиты. Пример: для абстрактного потока данных функция writeIntelWord(unsigned short) переводит число в порядок байтов «младший первым», а затем пишет два байта, вызвав интерфейсную функцию write.
  • Эталонные реализации, в целом рабочие, но непригодные/неоптимальные для определенных потомков. Пример: для абстрактного потока данных функция remainder() возвращает size() - pos(). Она работает, например, для дискового файла или буфера памяти, и отлично документирует, что должна делать. Но существуют потоки, которые не имеют длины, но имеют остаток (например, забуферизированный ввод из порта).
  • В языке Си++ интерфейсу, как и любому виртуальному классу, поощряется иметь пустой виртуальный деструктор.
  • Статические (общие для всех экземпляров) данные, особенно неизменяемые.

В любом случае интерфейс не может содержать:

  • любые нестатические данные, кроме технических (указатель на таблицу виртуальных методов и т.д.);
  • конструктор сверх того, чтобы эти технические данные инициализировать;
  • незаконченные функции, неспособные работать «как написано» ни в одном потомке и требующие доработки;
  • private-функции, к которым потомки обращаться не могут.

Интерфейсы и абстрактные классы

Можно заметить, что интерфейс, с формальной точки зрения, — это просто чистый абстрактный класс, то есть класс, в котором не определено ничего, кроме абстрактных методов. Если язык программирования поддерживает множественное наследование и абстрактные методы (как, например, C++), то необходимости во введении в синтаксис языка отдельного понятия «интерфейс» не возникает. Данные сущности описываются с помощью абстрактных классов и наследуются классами для реализации абстрактных методов.

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

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

  • если объект может параллельно наследовать n классов, существует n независимых способов к нему обращаться, а значит должно существовать (n — 1) дополнительных указателей на него; с точки зрения автоматического управления памятью это будет означать, что возникают ссылки, указывающие в середину объекта;
  • поддержка виртуальных вызовов подразумевает, что в объекте хранится ссылка на его виртуальную таблицу, а в случае множественного наследования n ссылок; активное использовании множественного наследования сильно увеличит объем памяти, занимаемый каждым объектом (экземпляром).

Использование схемы с интерфейсами (вместо множественного наследования) позволяет отбросить эти проблемы, если не считать вопроса о вызове интерфейсных методов (то есть виртуальных вызовов методов при множественном наследовании, см. выше). Классическое решение состоит в том (например, в JVM для Java или CLR для C#), что интерфейсные методы вызываются менее эффективным способом, без помощи виртуальной таблицы: при каждом вызове сначала определяется конкретный класс объекта, а затем в нем ищется нужный метод (разумеется, с многочисленными оптимизациями).

Множественное наследование и реализация интерфейсов

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

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

  • Запрет. В одном классе просто запрещается реализовывать несколько интерфейсов, имеющих методы с одинаковыми сигнатурами. Если для какого-то класса требуется комбинация несовместимых интерфейсов, программист должен выбрать другой путь решения проблемы, например, выделить несколько классов, каждый из которых реализует один из необходимых интерфейсов, и использовать их экземпляры совместно.
  • Явное разрешение неоднозначности. В случае обнаружения компилятором коллизии от программиста требуется явно указать, метод какого из интерфейсов он реализует и вызывает. То есть одноименные методы реализуются раздельно, а при вызове указывается, какой из них вызывается. При вызове одноименных методов через переменную типа «интерфейс» неоднозначность не возникает, если использованный в качестве типа переменной интерфейс имеет только один метод с заданным именем. Вариантом этого решения является явное переименование для совпадающих по именам наследуемых или реализуемых методов, за счет чего в пределах реализующего класса нет одноименных методов, но при обращении через интерфейс всегда вызывается нужная реализация.
  • Общая реализация одноименных методов. Если наследуется или реализуется несколько методов с одной и той же сигнатурой, то они объединяются в интерфейсе-наследнике, а в классе-реализаторе получают одну общую реализацию. Это хорошо подходит для случаев, когда одноименные методы разных интерфейсов идентичны по предполагаемой функциональности, но может вызвать нежелательные эффекты, если поведение этих методов должно различаться.

Интерфейсы в конкретных языках и системах

Реализация интерфейсов во многом определяется исходными возможностями языка и целью, с которой интерфейсы введены в него. Очень показательны особенности использования интерфейсов в языках Java, Object Pascal системы Delphi и C++, поскольку они демонстрируют три принципиально разные ситуации: изначальная ориентация языка на использование концепции интерфейсов, их применение для совместимости и их эмуляция классами.

  • В Java интерфейсы изначально входят в язык, являясь неотъемлемой его частью.
  • В объектной подсистеме языка Object Pascal никаких интерфейсов не было, их поддержка была введена в Delphi 2 для обеспечения написания и использования COM-компонентов. Соответственно, механизм интерфейсов Delphi ориентирован, в первую очередь, на использование технологии COM.
  • В C++ интерфейсов, строго говоря, нет вообще. Механизм, аналогичный интерфейсам (и исторически предшествующий им) реализуется другими средствами чрезвычайно мощной объектной подсистемы этого языка.

Интерфейсы в Delphi

В Delphi интерфейсы были введены для поддержки технологии COM фирмы Microsoft. Однако при выпуске Kylix интерфейсы как элемент языка были «отвязаны» от технологии COM. Все интерфейсы наследуются от интерфейса IInterface , который на платформе win32 совпадает с IUnknown, стандартным одноименным COM-интерфейсом, подобно тому, как все классы в нем являются наследниками класса TObject. Явное использование в качестве предка IUnknown оставлено для кода, использующего технологию COM.

Пример объявления интерфейса:

  IMyInterface = interface
    procedure DoSomething;
  end;

Для того, чтобы объявить о реализации интерфейсов, в описании класса необходимо указать их имена в скобках после ключевого слова class, после имени класса-предка. Так как «интерфейс — это договор, который нужно выполнить», программа не компилируется, пока в реализующем классе не будет реализована procedure DoSomething;

Вышеупомянутая ориентированность интерфейсов Delphi на технологию COM привела к некоторым неудобствам. Дело в том, что интерфейс IInterface (от которого наследуются все остальные интерфейсы) уже содержит три обязательных для COM-интерфейсов метода: QueryInterface, _AddRef, _Release. Следовательно, любой класс, реализующий любой интерфейс, обязан реализовать эти методы, даже если по логике программы интерфейс и класс не имеют никакого отношения к COM. Необходимо заметить, что данные три метода также используются для контроля времени жизни объекта и реализации механизма запроса интерфейса через оператор «as».

Пример класса, реализующего интерфейс:

  TMyClass = class(TMyParentClass, IMyInterface)
    procedure DoSomething;
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;

implementation

Программист должен правильно реализовать методы QueryInterface, _AddRef, _Release. Чтобы избавиться от необходимости писать стандартные методы, предусмотрен библиотечный класс TInterfacedObject — он реализует три вышеупомянутых метода, и любой класс, наследуемый от него и его потомков, получает эту реализацию. Реализация этих методов в TInterfacedObject предполагает автоматический контроль за временем жизни объекта путем подсчета ссылок через методы _AddRef и _Release, которые вызываются автоматически при входе в область видимости и выходе из нее.

Пример класса — наследника TInterfacedObject:

  TMyClass = class(TInterfacedObject, IMyInterface)
    procedure DoSomething;
  end;

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

Пример произвольного класса без подсчета ссылок:

  TMyClass = class(TObject, IInterface, IMyInterface)
    //IInterface
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    //IMyInterface
    procedure DoSomething;
  end;
{ TMyClass }

function TMyClass.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TMyClass._AddRef: Integer;
begin
  Result := -1;
end;

function TMyClass._Release: Integer;
begin
  Result := -1;
end;

procedure TMyClass.DoSomething;
begin
  //Do something
end;

Интерфейсы в C++

C++ поддерживает множественное наследование и абстрактные классы, поэтому, как уже упоминалось выше, отдельная синтаксическая конструкция для интерфейсов в этом языке не нужна. Об этом говорит сайт https://intellect.icu . Интерфейсы определяются при помощи абстрактных классов, а реализация интерфейса производится путем наследования этих классов.

Пример определения интерфейса:

/**
*   interface.Openable.h
*
*/
#ifndef INTERFACE_OPENABLE_HPP
#define INTERFACE_OPENABLE_HPP
// Класс интерфейса iOpenable. Определяет возможность открытия/закрытия чего-либо.
class iOpenable
{
    public:
    virtual ~iOpenable(){}

    virtual void open()=0;
    virtual void close()=0;
};
#endif

Интерфейс реализуется через наследование (благодаря наличию множественного наследования возможно реализовать в одном классе несколько интерфейсов, если в этом есть необходимость; в примере ниже наследование не множественное):

/**
*   class.Door.h
*
*/
#include "interface.Openable.h"
#include 

class Door: public iOpenable
{
    public:
    Door(){std::cout << "Door object created" << std::endl;}
    virtual ~Door(){}

    //Конкретизация методов интерфейса iOpenable для класса Door
    void open() override {std::cout << "Door opened" << std::endl;}
    void close() override {std::cout << "Door closed" << std::endl;}

    //Специфические для класса Door свойства и методы
    std::string mMaterial;
    std::string mColor;
    //...
};
/**
*   class.Book.h
*
*/
#include "interface.Openable.h"
#include 

class Book: public iOpenable
{
    public:
    Book(){std::cout << "Book object created" << std::endl;}
    virtual ~Book(){}

    //Конкретизация методов интерфейса iOpenable для класса Book
    void open() override {std::cout << "Book opened" << std::endl;}
    void close() override {std::cout << "Book closed" << std::endl;}

    //Специфические для класса Book свойства и методы
    std::string mTitle;
    std::string mAuthor;
    //...
};

Тестируем все вместе:

/**
*   test.Openable.cpp
*
*/
#include "interface.Openable.h"
#include "class.Door.h"
#include "class.Book.h"

//Функция открытия/закрытия любых разнородных объектов, в которых реализован интерфейс iOpenable
void openAndCloseSomething(iOpenable& smth)
{
    smth.open();
    smth.close();
}

int main()
{
    Door myDoor;
    Book myBook;

    openAndCloseSomething(myDoor);
    openAndCloseSomething(myBook);
    system ("pause");
    return 0;
}

Интерфейсы в C#

В C# интерфейсы могут наследовать один или несколько других интерфейсов. Членами интерфейсов могут быть методы, свойства, события и индексаторы:

    interface I1 
    {
        void Method1();
    }
    interface I2 
    {
        void Method2();
    }

    interface I : I1, I2
    {
        void Method();
        int Count { get; }
        event EventHandler SomeEvent;
        string this[int index] { get; set; }
    }

При реализации интерфейса класс должен реализовать как методы самого интерфейса, так и его базовых интерфейсов:

    public class C : I
    {
        public void Method()
        {
        }

        public int Count
        {
            get { throw new NotImplementedException(); }
        }

        public event EventHandler SomeEvent;

        public string this[int index]
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public void Method1()
        {
        }

        public void Method2()
        {
        }
    }

Интерфейсы в UML

Интерфейсы в Объектно-ориентированном программировании

Изображение интерфейса и реализующего его класса в UML.

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

В UML интерфейсы изображаются как классы со стереотипом «interface», либо в виде кружочков (в этом случае содержащиеся в интерфейсе UML-операции не отображаются).

Интерфейсы в Java

В отличие от C++, Java не позволяет наследовать больше одного класса. В качестве альтернативы множественному наследованию существуют интерфейсы. Каждый класс в Java может реализовать любой набор интерфейсов. Порождать объекты от интерфейсов в Java нельзя.

"В узком смысле слова Java - это объектно-ориентированный язык, напоминающий C++, но более простой для освоения и использования. В более широком смысле Java - это целая технология программирования, изначально рассчитанная на интеграцию с Web-сервисом. ... Java-среда должна быть как можно более мобильной, в идеале полностью независимой от платформы." Java как центр архипелага. Александр Таранов, Владимир Цишевский

Приведенная цитата типична для статей, посвященных Java. Все сказанное в ней - правда. Но не вся.

Java - это не только "С++ без указателей" и не только "С++ для интернета". Java - это объектно-ориентированный язык нового поколения.

Осознание сего факта потребовало от меня пересмотра стереотипов, сложившихся во время программирования на С++. Этот процесс я планирую отобразить в серии статей "ООП на Java".

Объявление интерфейсов Java

Объявление интерфейсов очень похоже на упрощенное объявление классов.

Оно начинается с заголовка. Сначала указываются модификаторы. Интерфейс может быть объявлен как public, и тогда он будет доступен для общего использования, либо модификатор доступа может не указываться, в этом случае интерфейс доступен только для типов своего пакета. Модификатор abstract для интерфейса не требуется, поскольку все интерфейсы являются абстрактными классами. Его можно указать, но делать этого не рекомендуется, чтобы не загромождать код.

Далее записывается ключевое слово interface и имя интерфейса.

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

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

Затем в фигурных скобках записывается тело интерфейса.

Пример объявления интерфейса (Ошибка если Colorable и Resizable классы: The type Colorable and Resizable cannot be a superinterface of Drawable; a superinterface must be an interface):

public interface Drawable extends Colorable, Resizable {
}

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

public interface Directions {
  int RIGHT=1;
  int LEFT=2;
  int UP=3;
  int DOWN=4;
}

Все методы интерфейса являются public abstract, и эти модификаторы также необязательны.

public interface Moveable {
  void moveRight();
  void moveLeft();
  void moveUp();
  void moveDown();
}

Как видно, описание интерфейса гораздо проще, чем объявление класса.

Реализация интерфейса Java

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

interface I
{
   void interfaceMethod();
}

public class ImplementingInterface implements I
{
   void interfaceMethod()
   {
      System.out.println("Этот метод реализован из интерфейса I");
   }
}

public static void main(String[] args)
{
   ImplementingInterface temp = new ImplementingInterface();
   temp.interfaceMethod();
}

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

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

interface A {
  int getValue();
}

interface B {
  double getValue();
}

interface C {
  int getValue();
}

public class Correct implements A, C // класс правильно наследует методы с одинаковой сигнатурой
{
   int getValue()
   {
      return 5;
   }
}

class Wrong implements A, B // класс вызывает ошибку при компиляции
{
   int getValue()
   {
      return 5;
   }

   double getValue()
   {
      return 5.5;
   }
}

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

Предполагаемые очередные темы - множественное наследование, динамические типы, обобщение и классификация, отображение на РСУБД.

Статья в большей степени ориентирована на философию Java, чем на практические рекомендации по программированию. Предполагается, что читатель знаком с ООП на С++ или на Java.

Первый вопрос, возникший у меня при знакомстве с Java, - Зачем нужны интерфейсы? Казалось бы вполне достаточно обычных и абстрактных классов в стиле C++.

Для маскировки отсутствия множественного наследования? Смешно.

После некоторого количества размышлений и особенно после неоднократного прочтения упомянутой книги Брюса Эккеля вопрос трансформировался: Зачем в Java нужны классы?

Привычные варианты ответа: класс вводит новый тип, класс обобщает ....

Декларация типа Java

Суть вещи тогда поймешь, когда правильно назовешь ее.

Новый тип вводится спецификацией интерфейса.

В C++ класс неявно определяет интерфейс. И в силу этого одновременно объявляет тип. При этом единственный интерфейс связывается с единственной реализацией. Множественное наследование и абстрактные классы в C++ - это прежде всего попытка обойти жесткую детерминированность.

В Java подобного ограничения нет. Любой интерфейс (тип) может иметь много реализаций. Любой класс может реализовывать много интерфейсов.

В качестве примера попробуем объявить собственный тип "число". Для краткости ограничимся операциями сложения и умножения.



/* INumber.java ------------------------------------------------------ (8<
 *
 * Декларация типа INumber и фрагмент программы, использующий этот тип.
 *
 * -------------------------------------------------------------------- */

interface INumber
{
  public void setValue(String s);
  public INumber add(INumber n);
  public INumber mul(INumber n);
  public String toString();
}

class CalcNumber
{
  void calculation(INumber n1, INumber n2, INumber n3)
  {
    INumber xx;
    xx = n2;
// Если закомментировать предыдущую строку, то компилятор выдаст ошибку:
// variable xx might not have been initialized
    xx.setValue("5.3");
    System.out.println("xx="+xx.toString());

    n1.setValue("21");
    n2.setValue("37.6");
    System.out.println("n1="+n1.toString());
    System.out.println("n2="+n2.toString());
    System.out.println("n3="+n3.toString());

    System.out.println("(n1+n2)*n3=" + n1.add(n2).mul(n3).toString());
    n1.setValue("21");
    System.out.println("(n2+n1)*n3=" + n2.add(n1).mul(n3).toString());
    n2.setValue("37.6");
    System.out.println("n1*(n2+n3)=" + n1.mul(n2.add(n3)).toString());
    n1.setValue("21");
    n2.setValue("37.6");
    System.out.println("n3*(n1+n2)=" + n3.mul(n1.add(n2)).toString());
  }
} // (8<

Из приведенного примера видно:

  • Интерфейс позволяет объявить тип. В приведенном примере объявляются переменные и параметры типа INumber, описываются действия над ними. Компиляция выполняется без ошибок.
  • Реализация типа передается через объект. Объекты n1, n2 и n3 передаются через параметры. Тем самым компилятор информируется, что объекты проинициализированы где-то за пределами данного модуля. Этого достаточно. Классы пока не нужны.
  • Инициализировать объект в приведенном модуле мы не можем, т.к. для этого необходимо иметь реализацию.

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

Добавим два варианта реализации.



/* DblNumber.java ---------------------------------------------------- (8<
 *
 * Реализация типа INumber через double.
 * 
 * -------------------------------------------------------------------- */

class DblNumber implements INumber
{
  double d;
  public DblNumber(double ip)
  {
    d = ip;
  }

  public void setValue(String s)
  {
    d = (new Double(s)).doubleValue();
  }

  public INumber add(INumber n)
  {
    d += (new Double(n.toString())).doubleValue();
    return this;
  }

  public INumber mul(INumber n)
  {
    d *= (new Double(n.toString())).doubleValue();
    return this;
  }

  public String toString()
  {
    return (new Double(d)).toString();
  }
} // (8<


/* IntNumber.java ---------------------------------------------------- (8<
 *
 * Реализация типа INumber через int.
 *
 * -------------------------------------------------------------------- */

class IntNumber implements INumber
{
  int i;
  public IntNumber(int v)
  {
    i = v;
  }

  public void setValue(String s)
  {
    String sw=s;
    int l = sw.indexOf('.');
    if (l > 0)
      sw = sw.substring(0, l);
    i = (new Integer(sw)).intValue();
  }

  public INumber add(INumber n)
  {
    String sw = n.toString();
    int l = sw.indexOf('.');
    if (l > 0)
      sw = sw.substring(0, l);
    i += (new Integer(sw)).intValue();
    return this;
  }

  public INumber mul(INumber n)
  {
    String sw = n.toString();
    int l = sw.indexOf('.');
    if (l > 0)
      sw = sw.substring(0, l);
    i *= (new Integer(sw)).intValue();
    return this;
  }

  public String toString()
  {
    return (new Integer(i)).toString();
  }
} // (8<

Проверим результат:



/* TestNumber.java --------------------------------------------------- (8<
 *
 * Тестирование типа INumber.
 * 
 * -------------------------------------------------------------------- */

public class TestNumber
{
  public static void main(String[] args) {
    INumber i1 = new IntNumber(22);
    INumber i2 = new DblNumber(11.2);
    INumber i3 = new DblNumber(3.4);
    CalcNumber cn = new CalcNumber();
    cn.calculation(i1, i2, i3);
  }
} // (8<

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

xx=5.3
n1=21
n2=37.6
n3=3.4
(n1+n2)*n3=174
(n2+n1)*n3=199.24
n1*(n2+n3)=861
n3*(n1+n2)=197.2

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

Интересно отметить, что результат операции над INumber зависит от последовательности использования переменных. Эффект возникает потому, что в спецификации типа мы опустили важные для чисел свойства: точность и диапазон допустимых значений. В результате они неявно берутся из базового типа, использованного при реализации. В данном случае достаточно добавить метод
setFormat(maxValue, minValue, decimal).

Реализация типа в Java

- Нужно ли знать формулу аспирина, чтобы вылечить головную боль?
- Нет! Достаточно иметь деньги в кармане.

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

В следующем примере есть только один класс - для запуска приложения. Собственно логика приложения реализована без использования классов!



/* TestAnimal.java --------------------------------------------------- (8<
 *
 * Образец бесклассовой реализации
 *
 * -------------------------------------------------------------------- */

import java.util.ArrayList;

interface Animal
{
  void giveSignals();
  void goHome();
  String getTitle();
  String getNick();
}

interface Command
{
  void exeCommand(Animal an);
}

interface Ranch
{
  void add(Animal an);
  void visitAll(Command cmd);
}

public class TestAnimal
{
  public static void main(String[] args)
  {
     Ranch myRanch = new Ranch()
     {
       private ArrayList ranchAnimals = new ArrayList();
       public void add(Animal a)
       {
         ranchAnimals.add(a);
       }
      
       public void visitAll(Command cmd)
       {
         for(int i = 0; i < ranchAnimals.size(); i++)
           cmd.exeCommand((Animal)ranchAnimals.get(i));
       }
     };   // end of new Ranch()

     // add animals
     myRanch.add(new Animal() //dog
        {
          public void giveSignals()
          {
            System.out.println("Гав-гав");
          }
         
          public void goHome()
          {
            System.out.println("Бежит в будку");
          }
         
          public String getTitle()
          {
            return new String("собака");
          }
         
          public String getNick()
          {
            return new String("Блэк");
          }
        }); // end of add new Animal dog

     myRanch.add(new Animal() // sheep
        {
          public void giveSignals()
          {
            System.out.println("Бе-е");
          }
         
          public void goHome()
          {
            System.out.println("Идет в загон");
          }
         
          public String getTitle()
          {
            return new String("овца");
          }
         
          public String getNick()
          {
            return new String("");
          }
        }); // end of add new Animal sheep

     myRanch.add(new Animal() // another sheep
        {
          public void giveSignals()
          {
            System.out.println("Бе-е");
          }
         
          public void goHome()
          {
            System.out.println("Идет в загон");
          }
         
          public String getTitle()
          {
            return new String("овца");
          }
         
          public String getNick()
          {
            return new String("");
          }
        }); // end of add new Animal another sheep

     // gives signals
     System.out.println("\n<<<<<<< Все подали голос >>>>>>>>>\n");
     myRanch.visitAll(new Command()
        {
          public void exeCommand(Animal a)
          {
            System.out.print(a.getTitle()+" "+a.getNick() + " говорит: ");
            a.giveSignals();
          }
        });

     // go to Home
     System.out.println("\n<<<<<<< Все домой! >>>>>>>>>\n");
     myRanch.visitAll(new Command()
        {
          public void exeCommand(Animal a)
          {
            System.out.print(a.getTitle()+" "+a.getNick() + " идет домой: ");
            a.goHome();
          }
        });

  }
} // (8<

Использование класса Sheep позволило бы сократить текст программы. Никаких других преимуществ введение этого класса не дает. Для остальных объектов определение соответствующих классов не дает ничего.

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

<<<<<<< Все подали голос >>>>>>>>>

собака Блэк говорит: Гав-гав
овца  говорит: Бе-е
овца  говорит: Бе-е

<<<<<<< Все домой! >>>>>>>>>

собака Блэк идет домой: Бежит в будку
овца  идет домой: Идет в загон
овца  идет домой: Идет в загон

Кто-нибудь скажет, что в приведенном примере использованы анонимные классы и будет прав.

Но что такое анонимный класс? В спецификации Java сказано: декларация анонимного класса автоматически извлекается компилятором из выражения создания экземпляра класса. Т.е. авторы языка воспользовались принципом чайника и привели задачу создания "самоопределенного" объекта к уже решенной. Другими словами, обычно сначала декларируется класс, а затем порождается его экземпляр. С анонимным классом все наоборот - сначала описывается экземпляр, а потом под него подгоняется класс. Реинжиниринг называется. :)

Можно сказать, что анонимный класс нужен для того, чтобы узаконить существование созданного объекта.

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

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

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

Наследование типа Java

Назвался груздем - полезай в кузов.

Наследование типа и полиморфизм обеспечиваются наследованием интерфейса и ничем иным.

Простой пример:



/* TestShips.java ---------------------------------------------------- (8<
 *
 * Наследование интерфейсов и полиморфизм
 *
 * -------------------------------------------------------------------- */

import java.util.ArrayList;

interface Ship
{
  void runTo(String s);
}

interface WarShip extends Ship
{
  void bombard();
}

interface Transport extends Ship
{
  void loadTroops(int n);
  void landTroops();
}

public class TestShips
{
  public static void main(String[] args)
  {
     ArrayList ships = new ArrayList();

     for(int i = 0; i < 3; i++)
       ships.add(new Transport()
         {
           private int troopers;
           public void runTo(String s) { System.out.println("Транспорт направляется в "+s+"."); }
           public void loadTroops(int n) { troopers = n; }
           public void landTroops()
           { System.out.println((new Integer(troopers)).toString()+" отрядов десантировано."); }
         }
       );

     for(int i = 0; i < 2; i++)
       ships.add(new WarShip()
         {
           public void runTo(String s) { System.out.println("Корабль направляется в "+s+"."); }
           public void bombard() { System.out.println("Корабль бомбардирует цель."); }
         }
       );

     for(int i = 0; i < 3; i++)
      ((Transport)ships.get(i)).loadTroops(i+5);

     for(int i = 0; i < ships.size(); i++)
      ((Ship)ships.get(i)).runTo("Вражий Порт");

     for(int i = 0; i < 3; i++)
      ((Transport)ships.get(i)).landTroops();

     for(int i = 3; i < ships.size(); i++)
      ((WarShip)ships.get(i)).bombard();

// Run-time error: java.lang.ClassCastException
//      ((Transport)ships.get(4)).landTroops();

// Run-time error: java.lang.ClassCastException
//      ((WarShip)ships.get(1)).bombard();

// Compile-time error: cannot resolve symbol
//      ((Ship)ships.get(1)).landTroops();
//      ((Ship)ships.get(4)).bombard();
  }
} // (8<

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

Транспорт направляется в Вражий Порт.
Транспорт направляется в Вражий Порт.
Транспорт направляется в Вражий Порт.
Корабль направляется в Вражий Порт.
Корабль направляется в Вражий Порт.
5 отрядов десантировано.
6 отрядов десантировано.
7 отрядов десантировано.
Корабль бомбардирует цель.
Корабль бомбардирует цель.

Концепция интерфейсов добавляет полиморфизму второе измерение:

  • Иерархический полиморфизм в стиле C++, основанный на приведении к базовому типу классов и /или интерфейсов (см. TestShips);
  • Полиморфизм экземпляров, основанный на разных реализациях одного и того же интерфейса (см. INumber).

Наследование имеет два аспекта:

  • "быть похожим (внешне) на" - наследование типа, поведения;
  • "быть устроенным как" - наследование реализации.

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

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

Обратите внимание, что в DblNumber и IntNumber наследования реализации нет. Поэтому иерархия классов не используется.

Обобщение в Java

Что же осталось на долю класса? - Обобщение.
Точнее, обобщение реализации.
А если быть честным до конца - организация повторно используемого кода.

Возможно повторное использование:

  • исходного кода (атрибуты);
  • исполняемого кода (методы).

Классы обеспечивают два измерения повторного использования:

  • классификация - экземпляр (объект) использует реализацию класса;
  • обобщение - классы наследуют реализацию родительских классов.

Таким образом, истинное предназначение класса - упаковка повторно используемого кода в соответствии с принципами объектно-ориентированной технологии.

Вау!! 😲 Ты еще не читал? Это зря!

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

создано: 2014-10-09
обновлено: 2025-01-29
198



Рейтиг 9 of 10. count vote: 2
Вы довольны ?:


Поделиться:

Найди готовое или заработай

С нашими удобными сервисами без комиссии*

Как это работает? | Узнать цену?

Найти исполнителя
$0 / весь год.
  • У вас есть задание, но нет времени его делать
  • Вы хотите найти профессионала для выплнения задания
  • Возможно примерение функции гаранта на сделку
  • Приорететная поддержка
  • идеально подходит для студентов, у которых нет времени для решения заданий
Готовое решение
$0 / весь год.
  • Вы можите продать(исполнителем) или купить(заказчиком) готовое решение
  • Вам предоставят готовое решение
  • Будет предоставлено в минимальные сроки т.к. задание уже готовое
  • Вы получите базовую гарантию 8 дней
  • Вы можете заработать на материалах
  • подходит как для студентов так и для преподавателей
Я исполнитель
$0 / весь год.
  • Вы профессионал своего дела
  • У вас есть опыт и желание зарабатывать
  • Вы хотите помочь в решении задач или написании работ
  • Возможно примерение функции гаранта на сделку
  • подходит для опытных студентов так и для преподавателей
avatar
22.3.2020 13:27

чтобы еще лучше понять смысл классов интерфейсов и вообще ООП, рекомендуем прочитать UML диаграммы классов
https://intellect.icu/diagramma-klassov-class-diagram-4825

Отношения классов в UML
https://intellect.icu/otnosheniya-klassov-v-uml-4301


Комментарии


Оставить комментарий
Если у вас есть какое-либо предложение, идея, благодарность или комментарий, не стесняйтесь писать. Мы очень ценим отзывы и рады услышать ваше мнение.
To reply

Объектно-ориентированное программирование ООП

Термины: Объектно-ориентированное программирование ООП