Содержание


Язык программирования Ceylon: Часть 6. Наследование и интерфейсы, анонимные классы

Comments

Этот цикл статей посвящён новому языку программирования Ceylon, представленному компанией Red Hat, Inc. В данной статье рассматриваются различные аспекты наследования, интерфейсы как один из инструментов наследования, а также анонимные классы.

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

1. Наследование и "уточнение"

В Ceylon применяются общепринятые термины иерархии классов: во главе (или в корне) находится базовый класс (superclass), от него наследуются производные классы (subclass), они же - производные типы (subtype). Сразу же следует отметить, что, так же как и в Java, все новые создаваемые базовые классы в неявной форме являются производными классами или подтипами класса Object. Тем не менее, замещение (override) метода базового класса методом производного класса обозначено особым термином refinement (уточнение, усовершенствование). Дело в том, что в производных классах замещать можно не только методы, но и атрибуты. В большинстве случаев атрибуты действительно не заменяются полностью, а дополняются, то есть, на самом деле, уточняются. Да и методы, замещаемые в производном классе, поддерживают тенденцию "от общего к частному". Таким образом, уточнение (refinement) обладает несколько более широким смыслом, чем замещение (override).

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

Листинг 1. Базовый класс с атрибутом, который может быть уточнён (замещён)
"Библиотечная единица хранения"
class LibrItem( Integer inv_num ) {
  "Описание по умолчанию - может быть уточнено (замещено) в производных классах"
  shared default String description => "(#``inv_num``)";
}

Для производных классов имя базового класса указывается после ключевого слова extends, далее следует список аргументов, которые передаются в базовый класс для инициализации его параметров. У программистов на C++ может возникнуть вопрос: почему именно extends, а не лаконичное и ставшее уже привычным двоеточие? В ответ на этот вопрос разработчики Ceylon ещё раз напоминают о своём стремлении сделать язык как можно более понятным и недвусмысленным, иногда за счёт некоторой многословности. В данном конкретном случае отмечается, что extends - не единственное ключевое слово, применяемое при наследовании классов, есть ещё satisfies, abstracts и т.д., поэтому в подобных случаях, кроме двоеточия, пришлось бы изобретать дополнительные комбинации символов типа :>, :< и многие другие, которые только запутают пользователя. В конце концов, добавляют авторы языка, использовать "символьные ребусы" или понятные осмысленные ключевые слова - это дело вкуса.

В листинге 2 показан производный класс для книг, являющийся подтипом для базового типа "библиотечная единица хранения".

Листинг 2. Создание производного класса путём наследования от базового класса
"Библиотечная книга, как одна из единиц хранения"
class LibrBook( String author, String title )
      extends LibrItem( inv_num ) {
  "Описание книги - уточняет описание по умолчанию"
  shared actual String description => super.description + " - " + author + title;
}

Уточнение (замещение) атрибута или метода базового класса в производном классе обозначается ключевым словом actual. Для обращения к членам базового класса из производного используется префикс super.

2. Более краткий синтаксис уточнения (замещения)

В Ceylon допускается сокращённая, более компактная запись уточнения (замещения) члена базового класса, определённого с ключевым словом default. Пример такой записи продемонстрирован в листинге 3.

Листинг 3. Уточнение (замещение) атрибута базового класса в сокращённой форме
"Журнал, как одна из единиц хранения"
class LibrMagazine( String title, Integer year, Integer number )
      extends LibrItem( inv_num ) {
  description => super.description + " - " + title + ", ``year``, №``number``";
}

Оператор => можно заменить простым оператором присваивания, как показано в листинге 4.

Листинг 4. Ещё один вариант уточнения (замещения) атрибута базового класса в сокращённой форме
"Кинофильм, как одна из единиц хранения"
class LibrMovie( String title, Integer year, String director, String media_type )
      extends LibrItem( inv_num ) {
  description = "(#``inv_num``) - ``title``, ``year``, ``director`` - (``media_type``)";
}

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

3. Абстрактные классы

Абстрактные классы, как и в Java, объявляются с помощью ключевого слова abstract. Реальные экземпляры абстрактных классов создавать, разумеется, нельзя. Члены абстрактного класса обозначаются ключевым словом formal. Отдельное ключевое слово требуется потому, что внутри абстрактного класса могут быть определены вложенные классы, которые, в свою очередь, могут быть абстрактными (с ключевым словом abstract) или формальными (с ключевым словом formal), и их необходимо различать. Для formal-класса-члена экземпляр может быть создан, для абстрактного класса-члена, как уже было отмечено выше, нет.

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

Листинг 5. Определение абстрактного базового класса
"Абстракция геометрической точки, независимой от конкретной системы координат"
abstract class Point() {
  shared formal Polar polar;
  shared formal Cartesian cartesian;

  shared formal Point rotate( Float rotation );
  shared formal Point dilate( Float dilation );
}

Здесь следует обратить особое внимание на то, что Ceylon не инициализирует автоматически атрибуты класса ни нулём, ни null-значением, ни каким-либо другим, поэтому атрибуты, которые не инициализируются в явной форме, всегда будут считаться формальными.

Реализацию наследуемого типа (класса) можно определить двумя способами. Первый способ использует сокращённый синтаксис уточнения (замещения), рассмотренный в предыдущем разделе (см. листинг 6).

Листинг 6. Сокращённая синтаксическая запись реализации абстрактного базового класса
"Система полярных координат для точки"
class Polar( Float angle, Float radius ) extends Point() {
  polar => this;
  cartesian => Cartesian( radius * cos( angle ), radius * sin( angle ) );

  rotate( Float rotation ) => Polar( angle+rotation, radius );
  dilate( Float dilation ) => Polar( angle, radius*dilation );
}

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

Листинг 7. Полная синтаксическая запись реализации абстрактного базового класса
import ceylon.math.float { sin, cos }

"Система полярных координат"
class Polar( Float angle, Float radius ) extends Point() {
  shared actual Polar polar => this;
  shared actual Cartesian cartesian => Cartesian( radius * cos( angle ), radius * sin( angle ) );

  shared actual Polar rotate( Float rotation ) => Polar( angle+rotation, radius );
  shared actual Polar dilate( Float dilation ) => Polar( angle, radius*dilation );
}

Ceylon, как и Java, поддерживает ковариантное уточнение (замещение) типов членов (ковариантное означает "сохраняющее иерархию наследования исходных типов в производных типах в том же порядке"). То есть, можно было бы уточнить тип возвращаемого значения методов rotate() и dilate(), заменив исходный более общий тип, объявленный базовым классом Point, на более "узкий" (уточнённый) производный тип Polar. Но контрвариантное уточнение (замещение) в текущей версии Ceylon не поддерживается (контрвариантное - обращающее иерархию исходных типов на противоположную в производных типах). Поэтому нельзя уточнить (заместить) метод и при этом "расширить" тип его параметра. Разумеется, нельзя также уточнить (заместить) метод и "расширить" тип возвращаемого им значения или заменить его на другой произвольный тип, поскольку в этом случае производный класс перестаёт быть подтипом базового типа. Если требуется уточнение типа возвращаемого значения, то заменой может послужить только подтип. В листинге 8 показано, как класс Cartesian выполняет ковариантное уточнение типов методов rotate() и dilate() с замещением типа возвращаемого значения.

Листинг 8. Реализация класса Cartesian с ковариантным уточнением (замещением) типов возвращаемых значений методов
import ceylon.math.float { atan }

"Система декартовых координат"
class Cartesian( Float x, Float y ) extends Point() {
  shared actual Polar polar => Polar( (x^2 + y^2)^0.5, atan(y/x) );
  shared actual Cartesian cartesian => this;
  shared actual Cartesian rotate( Float rotattion ) => polar.rotate(rotation).cartesian;
  shared actual Cartesian dilate( Float dilation  ) => 
          Cartesian( x*dilation, y*dilation );
}

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

4. Интерфейсы и "смешанное" наследование

При использовании объектно-ориентированного языка программирования разработчик рано или поздно сталкивается с проблемой множественного наследования, то есть, с необходимостью наследовать функциональность от нескольких базовых типов. В C++ поддерживается прямая реализация множественного наследования. Инструмент мощный, но, при всех его достоинствах, часто служит причиной появления в программах ошибок, источники которых чрезвычайно трудно обнаружить. В Java прямое множественное наследование не поддерживается, а в качестве альтернативы используются интерфейсы. Интерфейс фактически представляет собой абстрактный класс, в котором все существующие методы абстрактные. Но, в отличие от обычного абстрактного класса, интерфейс можно включить в класс, который уже является элементом некоторой иерархической системы наследования, и таких включаемых интерфейсов может быть несколько. Абстрактные методы, перечисленные в интерфейсе, обязательно должны быть реализованы в классе, включающем в себя данный интерфейс. Но интерфейсы в Ceylon предоставляют разработчику дополнительные возможности, что делает их несколько более гибкими и расширяет диапазон их применимости. В интерфейсе Ceylon допускается определение конкретных (не абстрактных) методов, а также get/set-методов для атрибутов, но при этом в интерфейсах запрещено определять ссылки или какую-либо логику инициализации значений, таким образом, интерфейс не имеет никакого конкретного состояния (stateless). Очевидно, что интерфейс не может содержать ссылки на другие объекты.

В отличие от обычного класса, интерфейс объявляется с помощью ключевого слова interface. В листинге 9 приведён пример определения универсального, то есть, предоставляющего возможность многократного его использования интерфейса Writer, а также определение класса, выполняющего реализацию этого интерфейса для вывода в консоли.

Листинг 9. Пример определения интерфейса Writer и его конкретной реализации
interface Writer {
  shared formal Formatter formatter;

  shared formal void write( String string );

  shared void writeLine( String string ) {
    write( string );
    write( "\n" );
  }

  shared void writeFormattedLine( String frmt, Object* args ) {
    writeLine( formatter.format( frmt, args ) );
  }
}

class ConsoleWriter() satisfies Writer {
  formatter = StringFormatter();
  write( String string ) => print( string );
}

Следует ещё раз обратить внимание на то, что для формального атрибута formatter не определено конкретное значение, так как интерфейс не может содержать ссылки на другой объект, как уже было сказано выше.

При определении класса реализации используется ключевое слово satisfies. Вообще говоря, оно может использоваться не только для обозначения реализации интерфейса данным классом (как в рассматриваемом примере), но и для обозначения наследования интерфейса от другого интерфейса. Но, в отличие от объявления с ключевым словом extends, объявление с ключевым словом satisfies не предполагает наличия каких-либо аргументов, поскольку интерфейсы по определению не имеют ни параметров, ни логики инициализации членов. Кроме того, объявление с ключевым словом satisfies может содержать более одного интерфейса, тем самым обеспечивая "смешанное" (оно же множественное) наследование (mixing inheritance).

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

5. Анонимные классы

Если у класса нет параметров, то Ceylon позволяет использовать сокращённую форму записи, которая определяет именованный экземпляр (объект) некоторого класса, но без указания имени самого этого класса (типа). В таком случае говорят, что применяется анонимный класс (anonymous class). В теории постулируется одно из главных требований к анонимному классу: он должен наследовать существующий класс или реализовывать существующий интерфейс. На практике - в Ceylon - выполняется это требование: анонимный класс наиболее удобен и полезен как раз при расширении (наследовании) абстрактного класса или при реализации интерфейса. В листинге 10 показан простой случай применения анонимного класса для расширения абстрактного класса, определённого в одном из предыдущих разделов.

Листинг 10. Использование анонимного класса при наследовании от существующего класса
doc "Начало координат"
object origin extends Polar( 0.0, 0.0 ) {
  description => "origin";
}

В листинге 11 демонстрируется анонимный класс как реализация интерфейса, определённого в предыдущем разделе.

Листинг 11. Использование анонимного класса при реализации интерфейса
shared object consoleWriter satisfies Writer {
  formatter = StringFormatter();
  write( String string ) => process.write( string );
}

Объявление экземпляра (объекта) анонимного класса выполняется с помощью ключевого слова object. У объекта анонимного класса есть один недостаток: в коде невозможно сослаться на тип (класс) такого объекта, только на сам этот именованный объект.

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

Листинг 12. Определение вложенного объекта анонимного класса
interface Subscription {
  shared formal void cancel();
}

Subscription register( Subscriber s ) {
  subscribers.append( s );
  object subscription satisfies Subscription {
    shared actual void cancel() => subscribers.remove( s );
  }
  return subscription;
}

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

Итак, главным различием между class и object можно считать то, что объявление с ключевым словом class определяет тип, на который можно ссылаться в других частях программы, а объявление с ключевым словом object определяет не тип, а отдельный конкретный объект. Более простым для запоминания различия является мнемоника: class - это object с параметрами. Более того, Ceylon позволяет продолжить и по аналогии распространить эту мнемонику: метод - это атрибут с параметрами.

Заключение

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


Ресурсы для скачивания


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=978207
ArticleTitle=Язык программирования Ceylon: Часть 6. Наследование и интерфейсы, анонимные классы
publish-date=07182014