Главная страница

Effective Java tmprogramming Language GuideJ o s h u a b lo c h


Скачать 1.05 Mb.
НазваниеEffective Java tmprogramming Language GuideJ o s h u a b lo c h
Дата03.04.2018
Размер1.05 Mb.
Формат файлаpdf
Имя файлаBlokh_Dzh_-_Java_Effektivnoe_programmirovanie.pdf
ТипДокументы
#40178
страница15 из 25
1   ...   11   12   13   14   15   16   17   18   ...   25

Т щ а тел ь но проектируйте сигнатуру метода В этой статье приводятся советы по проектированию API, не удостоившиеся собственной статьи. Собранные вместе, они помогут сделать ваш АР не толь подверженным ошибкам, более удобными простым в изучении.
Тщательно выбирайте названия методов. Названия всегда должны соответствовать стандартным соглашениям по именованию (статья 38). Вашей главной целью должен быть выбор таких имен, которые будут понятны и согласуются с остальными названиями в том же пакете. Второй целью должен быть выбор имен, отвечающих более общим соглашениям, если таковые имеются. В случае сомнений смотрите руководство по API библиотек языка Jаvа. Несмотря на массу противоречий, которые неизбежны если учитывать размер и возможности библиотек, здесь также существует консенсус. Бесценным источником является "The Java Developers п" Патрика Чана (Patrick Chan)
[ChanOO], содержащий декларации всех без исключения методов в библиотеках платформы Jаvа, индексированные в алфавитном порядке, если, к примеру, вы сомневаетесь, назвать ли метод remove или delete, то, бегло просмотрев указатель в этой книге, поймете, что, без всяких сомнений, выбор remove лучше есть сотни методов, чьи названия начинаются со слова remove, и лишь жалкая горстка имен, которые начинаются со слова delete. Не заходите слишком далеко в погоне за удобством своих методов. Каждый метод должен выполнять собственную часть работы. Избыток методов делает класс сложным для изучения, использования, описания, тестирования и сопровождения. В отношении интерфейсов это верно вдвойне большое количество методов усложняет жизнь и разработчиками пользователям. для каждого действия, поддерживаемого вашим типом, создайте полнофункциональный метод. Сокращенный вариант операции рассматривайте лищь в том случае, если он будет использоваться часто. Если есть сомнения, забудьте об этом варианте. Избегайте длинного перечня параметров. Правило таково, что на практике три параметра нужно рассматривать как максимум, и чем параметров меньше, тем лучше. Большинство программистов неспособны помнить более длинные списки параметров. Если целый ряд методов превышает этот предел, вашим API невозможно будет пользоваться, не обращаясь беспрестанно к его описанию. Особенно вредны длинные последовательности параметров одного итого же типа. И это не только потому, что ваш пользователь не сможет запомнить порядок их следования. Если он по ошибке поменяет их местами, его программа все равно будет компилироваться и работать. Только вот делать она будет совсем не то, что хотел ее автор. для сокращения слишком длинных списков параметров можно использовать два приема. Первый заключается в разбиении метода на несколько методов, каждому ИЗ которых нужно лишь какое-то подмножество его параметров. Если делать это неаккуратно, может получиться слишком много методов, однако этот же прием помогает сократить количество методов путем увеличения их ортогональности. Например, рассмотрим интерфейс java.util.List. У него нет методов для поиска индекса первого и последнего элемента в подсписке, каждому из них потребовалось бы потри параметра. Вместо этого он предлагает метод subList, который принимает два параметра И возвращает представление подсписка. Для получения желаемого результата можно объединить метод subList с методами indexOf и lastlndexOf, принимающими по одному параметру. Более того, метод subList можно сочетать с любыми другими Методами экземпляра List, чтобы выполнять самые разные операции для подсписков. Полученный АР имеет высокое соотношение мощности и размера.
Второй прием сокращения чрезмерно длинных перечней параметров заключается в создании вспомогательных классов, обеспечивающих агрегирование параметров. Обычно эти вспомогательные классы являются статическими классами-членами (статья 18). Данный прием рекомендуется использовать, когда становится понятно, что часто возникающая последовательность параметров на самом деле представляет некую отдельную сущность. Предположим, что выпишите класс, реализующий карточную игру, и выясняется, что постоянно передается последовательность из двух параметров достоинство карты и ее масть. И ваш АР и содержимое вашего класса, вероятно, выиграют, если для представления карты вы создадите вспомогательный класс и каждую такую последовательность параметров замените одним параметром, соответствующим этому вспомогательному классу. Выбирая тип параметра, отдавайте предпочтение интерфейсу, а не классу. Если для декларации параметра имеется подходящий интерфейс, всегда используйте его, а не класс, который реализует этот интерфейс. Например, нет причин писать метод, принимающий параметр типа Hashtable, лучше использовать Мар. Это позволит вам передавать этому методу Hashtable, HashMap, ТгееМар, подмножество ТгееМар, и вообще любую, пока еще ненаписанную реализацию интерфейса Мар. Применяя же вместо интерфейса класс, вы навязываете вашему клиенту конкретную реализацию и вынуждаете выполнять ненужное и потенциально трудоемкое копирование в том случае, если входные данные будут представлены в какой-либо иной форме.
Объекты-функции (статья 22) применяйте с осторожностью. Некоторые языки, в частности
Smalltalk и различные диалекты Lisp, поощряют стиль программирования, изобилующий объектами, представляющими функции, которые можно применять к другим объектам. Программисты, имеющие опыт работы с такими языками, могут поддаться соблазну и использовать тот же стиль для Java, но это не слишком хороший выбор. Проще всего создавать объект-функцию с помощью анонимного класса статья 18), однако даже это приводит к некоторому загромождению синтаксиса и ограничивает мощность и производительность в сравнении со встроенными управляющими конструкциями (inline control construct). Более того, стиль программирования, когда вы постоянно создаете объекты-функции и передаете их из одного метода в другой, расходится с господствующей тенденцией, а потому, если выбудете придерживаться этого стиля, другим программистам будет трудно разобраться в вашем коде. Это не означает, что объекты-функции не имеют права на использование. Напротив, они важны для многих мощных шаблонов, таких как Strategy [Саmmа95, стр. 315] и Visitor [Саmmа95, стр. 331]. Точнее говоря, объекты-функции следует применять только при наличии веских причин. Перегружая методы, соблюдайте осторожность bПриведем пример попытки классифицировать коллекции по признаку - б, список или другой вид коллекций- предпринятой из лучших побуждений

120
// Ошибка неверное использование перезагрузки class CollectionClassifier { public static String classify(Set s) {
return "Set"; }
public static String classify(List 1) { return "List"; } public static String classify(Collection с) { return "Unknown Collection"; }
public static void main(String[] args) {
Collection[] tests = new Collection[] { new HashSet(),
// Набор new ArrayList(),
// Список new HashMap().values()
// Не набор и не список
} ; for (int i = о i < tests.length; i++)
System.out.println(classify(tests[i]));
}
} Возможно, вы ожидаете, что эта программа напечатает сначала "Set", затем "List" и наконец "Unknown Collection". Ничего подобного Программа напечатает "Unknown Collection" три раза. Почему это происходит Потому что метод classify перезагружается (overload), и выбор варианта перезагрузки осуществляется на стадии компиляции. Для всех трех проходов цикла параметр на стадии компиляции имеет один и тот же тип Collection. И хотя вовремя выполнения программы при каждом проходе используется другой тип, это уже не влияет на выбор варианта перезагрузки. Поскольку вовремя компиляции параметр имел тип Collection, может применяться только третий вариант перезагрузки classify(Collection). И именно этот перезагруженный метод вызывается при каждом проходе цикла. Поведение этой программы такое странное потому, что выбор перезагруженных методов является статическим, тогда как выбор переопределенных методов динамическим. Правильный вариант переопределенного метода выбирается при выполнении программы, исходя из того, какой тип в этот момент имеет объект, для которого был вызван метод. Напомним, что переопределение (overrid) метода осуществляется тогда, когда подкласс имеет декларацию метода с точно такой же сигнатурой, что и у декларации метода предка. Если в подклассе метод был переопределен и затем данный метод был вызван для экземпляра этого подкласса, то выполняться
будет уже переопределенный метод независимо оттого, какой тип экземпляр подкласса имел на стадии компиляции. Для пояснения рассмотрим маленькую программу class A{
String name() { return "A"; } class ВВС СВ С) }; for (int i
= 0;
i
<
tests.length; i++)
System.out.print(tests[i].name());
}
} Метод пате декларируется в классе Д и переопределяется в классах В и С. Как и ожидалось, эта программа печатает "ABC", хотя на стадии компиляции при каждом проходе в цикле экземпляр имеет тип Д. Тип объекта на стадии компиляции не влияет на то, какой из методов будет исполняться, когда поступит запрос на вызов переопределенного метода всегда выполняется "самый точный" переопределяющий метод. Сравните это с перезагрузкой, когда тип объекта на стадии выполнения уже не влияет на то, какой вариант перезагрузки будет использоваться выбор осуществляется на стадии компиляции и всецело основывается на том, какой тип имеют параметры на стадии компиляции. В примере с ColleetionClassi fier программа должна была определять тип параметра, автоматически переключаясь на соответствующий перезагруженный метод на основании того, какой тип имеет параметр на стадии выполнения. Именно это делает метод name в примере "ABC". Перезагрузка метода не имеет такой возможности. Исправить программу можно, заменив все три варианта перезагрузки метода elassify единым методом, который выполняет явную проверку instaneeOf: public static String classify(Collection c) { return (c instanceof Set ? "Set" :
(c instancepf List ? "List" : "Unknown Collection")); }
Поскольку переопределение является нормой, а перезагрузка - исключением, именно переопределение задает, что люди ожидают увидеть при вызове метода. Как показал пример
CollcetionClassifier, перезагрузка может не оправдать эти ожидания. Не следует писать код, поведение которого не очевидно для среднего программиста. Особенно это касается интерфейсов API. Если рядовой пользователь АР не знает, какой из перезагруженных методов будет вызван для указанного набора параметров, то работа с таким API, вероятно, будет сопровождаться ошибками. Причем ошибки эти проявятся скорее всего только на этапе выполнения в виде некорректного поведения программы, и многие программисты не смогут их диагностировать. Поэтому необходимо избегать запутанных вариантов перезагрузки. Стоит обсудить, что же именно сбивает людей столку при использовании перезагрузки. Безопасная, умеренная политика предписывает никогда не предоставлять два варианта перезагрузки с одними тем же числом параметров. Если вы придерживаетесь этого ограничения, у программистов никогда не возникнет сомнений по поводу того, какой именно вариант перезагрузки соответствует тому или иному набору параметров. Это ограничение не слишком обременительно, поскольку вместо того чтобы использовать перезагрузку, вы всегда можете дать методам различные названия. Например, рассмотрим класс ObjectOutputStream. Он содержит варианты методов write для каждого простого типа и нескольких ссылочных типов. Вместо того чтобы перезагружать метод write, они применяют такие сигнатуры, как writeBoolean(boolean), writelnt(int) и writeLong(long). Дополнительное преимущество такой схемы именования по сравнению с перезагрузкой заключается в том, что можно создать методы read с соответствующими названиями, например readBoolean(), readlnt() и readLong(). И действительно, в классе ObjectlnputStream есть методы чтения с такими названиями. В случае с конструкторами у вас нет возможности использовать различные названия, несколько конструкторов в классе всегда подлежат перезагрузке. Правда, в отдельных ситуациях вы можете вместо конструктора предоставлять статический метод генерации (статья 1), но это не всегда возможно. Однако, с другой стороны, при применении конструкторов вам ненужно беспокоиться о взаимосвязи между перезагрузкой и переопределением, так как конструкторы нельзя переопределять. Поскольку вам, вероятно, придется предоставлять несколько конструкторов с одними тем же количеством параметров, полезно знать, в каких случаях это безопасно. Предоставление нескольких перезагруженных методов с одними тем же количеством параметров вряд ли запутает программистов, если всегда понятно, какой вариант перезагрузки соответствует заданному набору реальных параметров. Это как раз тот случай, когда у каждой пары вариантов перезагрузки есть хотя бы один формальный параметр с совершенно непохожим типом. Два типа считаются совершенно непохожими, если экземпляр одного из этих типов невозможно привести к другому типу. В этих условиях выбор варианта перезагрузки для данного набора реальных параметров полностью диктуется тем, какой тип имеют параметры в момент выполнения программы, и никак не связан сих типом на стадии компиляции. Следовательно, исчезает главный источник путаницы.
Например, класс ArrayList имеет конструктор, принимающий параметр int, и конструктор, принимающий параметр типа Collection. Трудно представить себе условия, когда возникнет путаница с вызовом двух этих конструкторов, поскольку простой тип и ссылочный тип совершенно непохожи. Аналогично, у класса BigInteger есть конструктор, принимающий массив типа byte, и конструктор, принимающий String. Это также не создает путаницы. Типы массивов и классы совершенно непохожи, за исключением Object. Совершенно непохожи также типы массивов и интерфейсы (за исключением
Serializable и Cloneable). Наконец, в версии 1.4 класс Throwable имеет конструктор, принимающий параметр String, и конструктор, принимающий параметр Throwable. Классы String и Throwable не родственные, иначе говоря, ни один из этих классов не является потомком другого. Ни один объект, не может быть экземпляром двух неродственных классов, а потому неродственные классы совершенно непохожи. Можно привести еще несколько примеров, когда для двух типов невозможно выполнить преобразование нив ту, нив другую сторону []LS, 5.1.7]. Однако в сложных случаях среднему программисту трудно определить, который из вариантов перезагрузки, если таковой имеется, применим к набору реальных параметров. Спецификация, определяющая, какой из вариантов перезагрузки должен использоваться, довольно сложна, и все ее тонкости понимают лишь немногие из программистов [JLS, 15.12.1-3] .. Иногда, подгоняя существующие классы под реализацию новых интерфейсов, вам приходится нарушать вышеприведенные рекомендации. Например, многие типы значений в библиотеках для платформы Jаvа до появления интерфейса Comparable имели методы соmраrеТо с типизацией (self- tуреd). Представим декларацию исходного метода соmраrеТо с типизацией для класса String: public int compareTo(String s); С появлением интерфейса Сора все эти классы были перестроены под реализацию данного интерфейса, содержащую новый, более общий вариант метода соmраrеТо со следующей декларацией public int соmраrеТо( Object о Полученный таким образом вариант перезагрузки явно нарушает изложенные выше требования, но он не наносит ущерба, поскольку оба перезагруженных метода, будучи вызваны с одинаковыми параметрами, неизменно выполняют одно и тоже. Программист может не знать, какой из вариантов перезагрузки будет задействован, но это не имеет значения, пока оба метода возвращают один и тот же результат. Стандартный прием, обеспечивающий опиеанную схему перезагрузки, заключается в том, чтобы ставить более общий вариант перезагрузки перед более частным public int compareTo(Object о) { return compareTo((String) о }
Аналогичная идиома иногда используется и для методов equals: public boolean equals(Object о) { return о instanceof String & & equals((String)o); Эта идиома безопасна и может повысить производительность, если на стадии компиляции тип параметра будет соответствовать параметру в более частном варианте перезагрузки (статья 37). Хотя библиотеки для платформы Java в основном следуют приведенным здесь советам, все же можно найти несколько мест, где они нарушаются. Например, класс String передает два перезагруженных статических метода генерации valueOf(char[]) и valueOf(Object), которые, получив ссылку на один и тот же объект, выполняют совершенно разную работу. Этому нет четкого объяснения, и относиться к данным методам следует как к аномалии, способной вызвать настоящую неразбериху. Подведем итоги. То, что вы можете осуществлять перезагрузку методов, еще не означает, что вы должны это делать. Обычно лучше воздерживаться от перезагрузки методов, которые имеют несколько сигнатур с одинаковым количеством параметров. Но иногда, особенно при наличии вызова конструкторов, невозможно следовать этому совету. Тогда постарайтесь избежать ситуации, при которой благодаря приведению типов один и тоже набор параметров может использоваться разными вариантами перезагрузки. Если такой ситуации избежать нельзя, например, из-за того, что вы переделываете уже имеющийся класс под реализацию нового интерфейса, удостоверьтесь в том, что все варианты перезагрузки, получая одни и те же параметры, будут вести себя одинаковым образом. Если же вы этого не сделаете, программисты не смогут эффективно использовать перезагруженный метод или конструктор и не смогут понять, почему он не работает. Возвращайте массив нулевой длины, а не Нередко встречаются методы, имеющие следующий вид private List cheesesInStock = ... ;
/**
* @return массив, содержащий все сыры, имеющиеся в магазине,
* или null, если сыров для продажи нет.
*/
public Cheese[] getCheeses() { if (cheesesInStock.size() == 0) return null;
}
Нет причин рассматривать как особый случай ситуацию, когда в продаже нет сыра. Это требует от клиента написания дополнительного кода для обработки возвращаемого методом значения п, например
Cheese[] cheeses = shop.getCheeses(); if (cheeses ! = п &&
Аrrауs.аsList(shор.gеtСhееsеs()).соntаins(Сhееsе.SТILТON))
Sуstеm.оut.рrintln("Jоllу good, just the thing."); вместо простого if (Аrrаys.аsList(shор.gеtСhееsеs()).соntаins(Сhееsе.SТILTON))
Sуstеm.оut.рrintln("Jоllу good, just the thing."); Такого рода многоречивость необходима почти при каждом вызове метода, который BM

CTO массива нулевой длины возвращает null. Это чревато ошибками, так как разработчик клиента мог и не написать специальный код для обработки результата null. Ошибка может оставаться незамеченной годами, поскольку подобные методы, как правило, возвращают один или несколько объектов. Следует еще упомянуть о том, что возврат null вместо массива приводит к усложнению самого метода, возвращающего массив. Иногда можно услышать возражения, что возврат значения null предпочтительнее возврата массива нулевой длины потому, что это позволяет избежать расходов на размещение массива в памяти. Этот аргумент несостоятелен по двум причинам. Во-первых, на этом уровне нет смысла беспокоиться о производительности, если только профилирование программы не покажет, что именно этот метод является основной причиной падения производительности (статья 37). Во-вторых, при каждом вызове метода, который не возвращает записей, клиенту можно передавать один и тот же массив нулевой длины, поскольку любой массив нулевой длины неизменяем, а неизменяемые объекты доступны для совместного использования (статья 13). На самом деле, именно это и происходит, когда вы применяете стандартную идиому для выгрузки элементов из коллекции в массив с контролем типа private List сhееsеsInStосk = ... , private пае @геtuгп массие, содержащий все сыры, имеющиеся в магазине
1   ...   11   12   13   14   15   16   17   18   ...   25


написать администратору сайта