Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Предпочитайте интерфейсы абстрактным классам.



 

 

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

 

 

80

 

Имеющийся класс несложно подогнать под реализацию нового интерфейса.

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

Интерфейсы идеально подходят для создания дополнений (mixin). Помимо своего "первоначального типа", класс может реализовать некий дополнительный тип (mixin), объявив о том, что в нем реализован дополнительный функционал. Например, Compaгable является дополнительным интерфейсом, который дает классу возможность декларировать, что его экземпляры упорядочены по отношению к другим, сравнимым с ними объектам. Такой интерфейс называется mixiп, поскольку позволяет к перво­начальным функциям некоего типа примешивать (mixed in) дополнительные функ­циональные возможности. Абстрактные классы нельзя использовать для создания дополнений по той же причине, по которой их невозможно встроить в уже имеющиеся классы: класс не может иметь более одного родителя, и в иерархии классов нет подхо­дящего места, куда можно поместить mixin.

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

public interface Singer {

AudioClip Sing(Song s);  }

publlic interface Songwriter {

  Song compose(boolean hit); }

 

В жизни некоторые певцы являются и авторами песен. Поскольку для определе­ния этих типов мы использовали не абстрактные классы, а интерфейсы, то одному классу никак не запрещается реализовывать оба интерфейса: Singer и Songwriter. В действительности мы можем определить третий интерфейс, который расширяет оба Интерфейса и добавляет новые методы, соответствующие сочетанию:

public interface SingerSongwriter extends Singer, Songwriter {

 AudioClip strum();

void actSensitive(); }

 

 

81

 

 

Такой уровень гибкости нужен не всегда. Если же он необходим, интерфейсы становятся спасительным средством. Альтернативой им является раздутая иерархия классов, которая содержит отдельный класс для каждой поддерживаемой ею комбинации атрибутов. Если в системе имеется п атрибутов, то существует 2 в степени n сочетаний, ко­торые, возможно, придется поддерживать. Это называется комбинаторным взрывом (combinatorial explosion). Раздутые иерархии классов могут привести к созданию раз­дутых классов, содержащих массу методов, отличающихся друг от друга лишь типом аргументов, поскольку в такой иерархии классов не будет типов, отражающих общий

функционал.

Интерфейсы позволяют безопасно и мощно наращивать функциональность,

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

Хотя в интерфейсе нельзя хранить реализацию методов, определение типов с по­мощью интерфейсов не мешает оказывать программистам помощь в реализации клас­са. Вы можете объединить преимущества интерфейсов и абстрактных классов, сопроводив каждый предоставляемый вами нетривиальный интерфейс. абстракт­ным классом с наброском (скелетом) реализации (skeletal implementation class). Интерфейс по-прежнему будет определять тип, а вся работа по его воплощению ляжет на скелетную реализацию.

По соглашению, скелетные реализации носят названия вида Abstract/пterface,

где iпterface - это имя реализуемого ими интерфейса. Например, в архитектуре Collections Framework представлены скелетные реализации для всех основных интер­фейсов коллекций: AbstractCollection, AbstractSet, AbstractList и AbstractMap.

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

// Адаптер интерфейса List для массива целых чисел (int)

static List intArrayAsList(final int[] а)

if (а == null)

throw new NullPointerException();

return new AbstractList() {

public Object get(int i) {

return new Integer(a[i]); }

public int size() {

return a.length; }

public Object set(int i, Object о) {

 int oldVal = a[i];

a[i] = «Integer)o).intValue();

return new Integer(oldVal);

}  

};

}

 

 

82

 

Если принять во внимание все, что делает реализация интерфейса List, то этот пример демонстрирует всю мощь скелетных реализаций. Кстати, пример является адаптером (Adapter) [Сатта95, стр. 139], который позволяет представить массив int в виде списка экземпляров Integer. Из-за всех этих преобразований из значений int в экземпляры Integer и обратно производительность метода не очень высока. Отметим, что здесь приведен лишь статический метод генерации, сам же класс являет­ся недоступным анонимным lUIассом (статья 18), спрятанным внутри статического метода генерации.

Достоинство скелетных реализаций заключается в том, что они оказывают по­мощь в реализации абстрактного класса, не налагая при этом строгих ограничений, как это имело бы место, если бы для определения типов использовались абстрактные классы. для большинства программистов, реализующих интерфейс, расширение ске­летной реализации - это очевидный, хотя и необязательный выбор. Если имеющийся класс нельзя заставить расширять скелетную реализацию, он всегда может реализо­вать представленный интерфейс сам. Более того, скелетная реализация помогает в решении стоящей перед разработчиком задачи. Класс, который реализует данный интерфейс, может переадресовывать вызов метода, указанного в интерфейсе, содер­жащемуся внутри его экземпляру закрытого класса, расширяющего скелетную реа­лизацию. Такой прием, известный как искусственное множественное наследование (simulated multiple inheritance), тесно связан с идиомой класса-оболочки (статья 14). Он обладает большинством преимуществ множественного наследования и при этом избегает его подводных камней.

Написание скелетной реализации - занятие относительно простое, хотя иногда и скучное. Во-первых, вы должны изучить интерфейс и решить, какие из методов Являются примитивами (primitive) в терминах, в которых можно было бы реализовать остальные методы интерфейса. Эти примитивы и будут абстрактными методами в ва­шей скелетной реализации. После этого вы должны предоставить конкретную реали­зацию всех остальных методов данного интерфейса. В качестве примера при в едем скелетную реализацию интерфейса Мар. Entry. В том виде, как это показано здесь, Класс не включен в библиотеки для платформы Java, хотя, вероятно, это следовало бы сделать.

// Скелетная реализация

public abstract class AbstractMapEntry implements Мар. Entry {

// Примитивы

public abstract Object getKey();

public abstract Object getValue();

 

 

83

 

 

// Элементы в изменяемых схемах должны переопределять этот метод

publiC Object setValue(Object value) {

throw пеw UnsupportedOperationException();

}

// Реализует основные соглашения для метода Мар. Entry . equals

public boolean equals(Object о) {

if (о == this)

return true;

if (!(o iпstапсеоf Map,Entry))

return false;

Map.Entry arg = (Мар. Entry)o;

return eq(getKey(), arg.getKey()) && eq(getValue(), arg.getValue());

private static boolean eq(Object 01, Object 02) {

return (01 == null ? 02 == null : 01.equals(02));

}

// Реализует основные соглашения для метода Мар . Entry.hashCode

 public int hashCode() {

return

(getKey() == пull ? 0 : getKey().hashCode())

­(getValue() == null ? 0 : getValue().hashCode());

}

}

 

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

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

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

 

84

 

новый метод добавить одновременно и· в скелетную реализацию, и в интерфейс, однако по-настоящему это не решит проблемы. Любая реализация интерфейса, не наследую­щая скелетную реализацию, все равно работать не будет.

Следовательно, открытые интерфейсы необходимо проектировать аккуратно. Как толрко интерфейс создан и повсюду реализован, поменять его почти невозможно. В действительности его нужно правильно строить с первого же раза. Если в Интер­фейсе есть незначительньый изъян, он уже всегда будет раздражать и вас, и пользо­вателей. Если же интерфейс имеет серьезные дефекты, он способен погубить АРI. Самое лучшее, что можно предпринять при создании нового интерфейса,- заставить как можно больше программистов реализовать этот интерфейс самыми разнообраз­ными способами, прежде чем он будет "заморожен". Это позволит вам найти все

ошибки, пока у вас еще есть возможность их исправить.                      

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

 

Используйте интерфейсы только для определения типов

 

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

Среди интерфейсов, которые не отвечают этому критерию, числится так назы­ваемый интерфейс констант (constant interface). Он не имеет методов и содер­жит исключительно поля static final, передающие константы. Классы, в которых эти константы используются, реализуют данный интерфейс для того, чтобы исклю­чить необходимость в добавлении к названию констант названия класса. Приведем пример:

// Шаблон интерфейса констант - не использоваты!

public interface РhуsiсаlСопstапts

// Число Авогадро (1/ моль )

static final double AVOGADROS NUMBER = 6.02214199е23;

 

 

85

 

// Постоянная Больцмана ( Дж / К )

static final double BOLTZMANN_CONSTANT=1.3806503е-23;

// Масса электрона ( кг )

static final double ELECTRON MASS=9.10938188е-31;

 

Шаблон интерфейса констант представляет собой неудачный вариант ис­пользования интерфейсов. Появление внутри класса каких-либо констант является деталью реализации. Реализация интерфейса констант приводит к утечке таких дета­лей во внешний АРI данного класса. То, что класс реализует интерфейс констант, для пользователей класса не представляет никакого интереса. На практике это может даже сбить их с толку. Хуже того, это является неким обязательством: если в будущих версиях класс поменяется так, что ему уже не будет нужды использовать данные кон­станты, он все равно должен будет реализовывать этот интерфейс для обеспечения совместимости на уровне двоичных кодов (binary compatibility). Если же интерфейс констант реализует неокончательный класс; константами из этого интерфейса будет засорено пространство имен всех его подклассов.

В библиотеках для платформы Java есть несколько интерфейсов с константами, например jауа.io.ObjectSt reamConstants. Подобные интерфейсы нужно восприни­мать как отклонение от нормы, и подражать им не следует.

Для передачи констант существует несколько разумных способов. Если констан­ты сильно связаны с имеющимся классом или интерфейсом, вы должны добавить их непосредственно в этот класс или интерфейс. Например, все классы-оболочки в биб­лиотеках платформы Java, связанные с числами, такие как Integer и Float, предостав­ляют константы МIN_VALUE и MAX_VALUE. Если же константы лучше рассматривать как члены перечисления, то передавать их нужно с помощью класса перечисления (статья 21). В остальных случаях вы должны передавать константы с помощью вспо­могательнoго класса (utility c!ass), не имеющего экземпляров (статья 3). Представим вариант вспомогательного класса для предыдущего примера PhysicalConstants:

// Вспомогательный класс для констант

public class PhysicalConstants {

private PhysicalConstants() { }

// Предотвращает появление экэемпляра

public static final double AVOGADROS_NUMBER  =6.02214199е23;

public static final doubleBOLTZMANN_CONSTANT  =1.3806503е-23;

 

       public static final double ELECTRON_MASS  =9.10938188е-31;  

}

 

 

 

Хотя представление PhysicalConstants в виде вспомогательного класса требует, чтобы клиенты связывали названия констант с именем класса, это не большая цена за получение осмысленного API. Возможно, что со временем язык Java позволит

 

 

86

 

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

 

рrivate static final double РI = Math. PI;

 

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

 


Поделиться:



Последнее изменение этой страницы: 2019-04-11; Просмотров: 254; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.049 с.)
Главная | Случайная страница | Обратная связь