JavaRush /Java блог /Архив info.javarush /Руководство по общему стилю программирования
pandaFromMinsk
39 уровень
Минск

Руководство по общему стилю программирования

Статья из группы Архив info.javarush
Статья является частью академического курса "Advanced Java" ("Java для совершенствующихся") Данный курс создан, чтобы помочь вам научиться эффективно использовать особенности Java. Материал охватывает "продвинутые" темы, как создание объектов, конкуренцию, сериализацию, рефлексию и пр. Курс научит эффективно владеть приемами Java. Подробности тут.
Содержание
1. Введение 2. Область видимости переменных 3. Поля класса и локальные переменные 4. Аргументы метода и локальные переменные 5. Упаковка и распаковка 6. Интерфейсы 7. Строки 8. Соглашения об именах 9. Стандартные библиотеки 10. Неизменяемость 11. Тестирование 12. Далее... 13. Загрузка исходного кода
1. Введение
В этой части руководства продолжим обсуждение общих принципов хорошего стиля программирования и гибкого дизайна в Java. Некоторые из этих принципов мы уже видели в предыдущих главах руководства, однако много практических советов будут даны с целью улучшения квалификации Java-разработчика.
2. Область видимости переменных
В третьей части ("Как проектировать классы и интерфейсы") мы обсудили как видимость и доступность могут быть применены к членам классов и интерфейсов, учитывая ограничения в области видимости. Однако, мы пока не обсудили локальные переменные, которые используются в реализации методов. В языке Java, каждая локальная переменная, однажды объявленная, имеет область видимости. Данная переменная становится видимой из места, где она объявлена, до места завершения исполнения метода (или блока кода). Как правило, необходимо следовать единственному правилу: объявлять локальную переменную как можно ближе к месту, где она будет использоваться. Предлагаю взглянуть на типичный пример: for( final Locale locale: Locale.getAvailableLocales() ) { // блок кода } try( final InputStream in = new FileInputStream( "file.txt" ) ) { // блока кода } В обоих фрагментах кода область видимости переменных ограничена блоками выполнения, где эти переменные объявлены. По завершению блока область видимости заканчивается и переменная становится невидимой. Это выглядит более понятно, однако с выпуском Java 8 и введением в лямбды, множество известных идиом языка с использованием локальных переменных становятся устаревшими. Приведу пример из предыщущего примера с использованием лямбд вместо цикла: Arrays.stream( Locale.getAvailableLocales() ).forEach( ( locale ) -> { // блок кода } ); Видно, что локальная переменная стала аргументом функции, переданной, в свою очередь, в качестве аргумента методу forEach.
3. Поля класса и локальные переменные
Каждый метод в Java принадлежит определенному классу (или, в случае Java8, интерфейсу, где данный метод объявлен в качестве метода по умолчанию). Между локальными переменными являющимися полями класса или использующихся в реализации методов, существует как таковая вероятность конфликта имен. Компилятор Java умеет выбирать правильную переменную из ряда имеющихся, несмотря на то, что эту переменную намеревается использовать не один разработчик. Современные Java IDE выполняют огромную работу, чтобы подсказать разработчику, когда такие конфликты имеют место произойти, путем предупреждений компилятора, подсветкой переменных. Но все же лучше подумать о подобных вещах во время написания кода. Предлагаю посмотреть на пример: public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += value; return value; } } Пример выглядит вполне легким, однако это ловушка. Метод calculateValue вводит локальную переменную value и оперируя ей, прячет поле класса с таким же именем. В строке value += value; должна быть сумма значения поля класса и локальной переменной, однако вместо этого, выполняется что-то другое. Правильная реализация будет выглядеть вот так (с использованием ключевого слова this): public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += this.value; return value; } } В некотором роде данный пример наивен, но тем не менее он демонстрирует важный момент, при котором в некоторых случаях могут потребоваться часы на отладку и исправление.
4. Аргументы метода и локальные переменные
Другая ловушка, куда часто попадают неопытные Java-разработчики, это использование аргументов метода в качестве локальных переменных. Java позволяет переприсвоить значения аргументам не являющимися константами (однако, это не оказывает никакого эффекта на первоначальное значение): public String sanitize( String str ) { if( !str.isEmpty() ) { str = str.trim(); } str = str.toLowerCase(); return str; } Фрагмент кода выше не является элегантным, но он вполне хорош для раскрытия проблемы: аргументу str присвоено другое значение (и в основном используется как локальная переменная). Во всех случаях (без всякого исключения) можно и следует обойтись без этого примера (например, объявляя аргументы как константы). Например: public String sanitize( final String str ) { String sanitized = str; if( !str.isEmpty() ) { sanitized = str.trim(); } sanitized = sanitized.toLowerCase(); return sanitized; } Следуя этому простому правилу, данный код легче отслеживать и находить в нем источник проблемы, даже при введении локальных переменных.
5. Упаковка и распаковка
Упаковка и распаковка - название техники используемой в Java для конвертации примитивных типов (int, long, double и пр.) к соответствующим оберткам типов (Integer, Long, Double и пр.). В 4ой части руководства "Как и когда использовать дженерики", вы уже встречали это в действии, когда я рассказывал об обертках примитивных типов в качестве параметров типа дженериков. Несмотря, что компилятор Java пытается сделать все возможное в сокрытии подобных конвертаций выполнением автоупаковки, иногда это получается хуже ожидаемого и приводит к неожиданным результатам. Взглянем на пример: public static void calculate( final long value ) { // блок кода } final Long value = null; calculate( value ); Фрагмент кода выше отлично скомпилируется. Однако, он выбросит исключение NullPointerException на строке // блок где будет выполняться конвертация между Long и long. Совет для подобного случая - желательно использовать примитивные типы (однако, мы уже знаем, что это не всегда возможно).
6. Интерфейсы
В третьей части руководства "Как проектировать классы и интерфейсы", мы обсудили интерфейсы и программирование по контракту, заостряя внимание на то, что интерфейсы должны предпочтены конкретным классам там, где возможно. Цель данного раздела - убедить снова вас принимать во внимание сначала интерфейсы, демонстрируя это на реальных примерах. Интерфейсы не привязаны к определенной имплементации (за исключением методов по умолчанию). Они только контракты и, как пример, они обеспечивают много свободы и гибкости в способе, где контракты могли быть выполнены. Эта гибкость становится более важной, когда имплементация влечет за собой внешние системы или сервисы. Посмотрим на пример простого интерфейса и его возможной реализации: public interface TimezoneService { TimeZone getTimeZone( final double lat, final double lon ) throws IOException; } public class TimezoneServiceImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { final URL url = new URL( String.format( "http://api.geonames.org/timezone?lat=%.2f&lng=%.2f&username=demo", lat, lon ) ); final HttpURLConnection connection = ( HttpURLConnection )url.openConnection(); connection.setRequestMethod( "GET" ); connection.setConnectTimeout( 1000 ); connection.setReadTimeout( 1000 ); connection.connect(); int status = connection.getResponseCode(); if (status == 200) { // Do something here } return TimeZone.getDefault(); } } Фрагмент кода сверху демонстрирует типичный шаблон интерфейса и его реализации. Данная реализация использует внешний HTTP-сервис (http://api.geonames.org/) для извлечения временной зоны определенной местности. Однако, т.к. контракт зависит от интерфейса, очень легко ввести еще одну реализацию интерфейса, применяя, например, базу данных или даже обычный плоский файл. С ними интерфейсы очень хорошо помогут спроектировать тестируемый код. Например, не всегда практично вызвать внешние сервисы на каждый тест, что вместо этого имеет смысл выполнить альтернативную, простейшую реализацию (например, заглушку): public class TimezoneServiceTestImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { return TimeZone.getDefault(); } } Эта реализация может быть использована в любом месте, где интерфейс TimezoneService обязателен, изолируя сценарий теста от зависимости от внешних компонентов. Много отличных примеров эффективного использования таких интерфейсов инкапсулированы внутри стандартной библиотеки Java. Коллекции, списки, множества - эти интерфейсы имеют несколько реализаций, которые могут быть плавно заменены и могут быть взаимозаменяемы, когда контракты пользуются преимуществом. Например: public static< T > void print( final Collection< T > collection ) { for( final T element: collection ) { System.out.println( element ); } } print( new HashSet< Object >( /* ... */ ) ); print( new ArrayList< Integer >( /* ... */ ) ); print( new TreeSet< String >( /* ... */ ) );
7. Строки
Строки одни из самых используемых типов как в Java, так и в других языках программирования. Язык Java упрощает множество рутинных операций со строками, поддерживая операции конкатенации и сравнения "прямо из коробки". Вдобавок, стандартная библиотека содержит множество классов делающих операции со строками эффективными. Это как раз то, что собираемся обсудить в этой секции. В Java строки являются неизменяемыми объектами, представленными в кодировке UTF-16. Каждый раз, когда вы объединяете строки (или выполняете любую операцию, которая изменяет исходную строку), создается новый экземпляр класса String. Из-за этого, операция объединения может стать очень неэффективной, вызывая создание многих промежуточных экземпляров класса String (создает мусор, в общем говоря). Но стандартная библиотека Java содержит два очень полезных класса, цель которых - сделать удобными манипуляции со строками. Это - StringBuilder и StringBuffer (единственная разница между ними, что StringBuffer является потокобезопасным, когда StringBuilder наоборот). Взглянем на пару примеров используемых один из этих классов: final StringBuilder sb = new StringBuilder(); for( int i = 1; i <= 10; ++i ) { sb.append( " " ); sb.append( i ); } sb.deleteCharAt( 0 ); sb.insert( 0, "[" ); sb.replace( sb.length() - 3, sb.length(), "]" ); Несмотря на то, что использование StringBuilder / StringBuffer - рекомендованный способ для манипуляции строк, он может выглядеть излишним в простейшем сценарии объединения двух или трех строк, так, что вместо этого может использоваться обычный оператор сложения ("+"), например: String userId = "user:" + new Random().nextInt( 100 ); Часто лучшая альтернатива для упрощения объединения - использование форматирования строк а также стандартной библиотеки Java, чтобы помочь обеспечить статический метод хэлпера String.format. Это поддерживает богатый набор спецификаторов формата, включая числа, символы, дату/время и пр. (для получения полной информации обратитесь к справочной документации) String.format( "%04d", 1 ); -> 0001 String.format( "%.2f", 12.324234d ); -> 12.32 String.format( "%tR", new Date() ); -> 21:11 String.format( "%tF", new Date() ); -> 2014-11-11 String.format( "%d%%", 12 ); -> 12% Метод String.format обеспечивает чистый и легкий подход к образованию строк из различных типов данных. Стоит заметить, что современные Java IDE могут анализировать спецификацию формата из аргументов переданных методу String.format и предупреждать разработчиков в случае обнаруженных несовпадений.
8. Соглашения об именах
Java - язык, который не заставляет разработчиков строго следовать любому соглашению об именах, однако сообщество разработало набор простых правил, которые делают облик Java-кода ровным как в стандартной библиотеке, так и любых других проектах Java:
  • имена пакетов указываются в нижнем регистре: org.junit, com.fasterxml.jackson, javax.json
  • имена классов, перечислений, интерфейсов, аннотаций пишутся с прописной буквы: StringBuilder, Runnable, @Override
  • имена полей или методов (за исключением static final) указываются в верблюжьей нотации: isEmpty, format, addAll
  • статические финальные поля или имена констант перечеслений указаны в верхнем регистре, разделенными знаком нижнего подчеркивания ("_"): LOG, MIN_RADIX, INSTANCE.
  • локальные переменные или аргументы методов набраны в верблюжьей нотации: str, newLength, minimumCapacity
  • имена типов параметров у дженериков представлены одной буквой в верхнем регистре: T, U, E
Следуя этим простым соглашениям, код, который вы пишете, будет выглядеть кратким и неразличимым по своему стилю от кода другой библиотеки или фреймворка, и будет внушать ощущение, что был разработан одним и тем же человеком (один из тех редких моментов, когда соглашения действительно работают).
9. Стандартные библиотеки
Неважно, над каким видом Java-проекта вы работаете, стандартные библиотеки Java - ваши лучшие друзья. Да, сложно не согласиться, что у них есть кое-какие шероховатости и странные решения по дизайну, тем не менее, в 99% это высококачественный код написанный экспертами. Это стоит того, чтобы изучить. Каждый Java-релиз несет много новых особенностей в существующие библиотеки (с некоторыми возможно спорными моментами со старыми особенностями), а также, добавляет много новых библиотек. Java 5 принес новую concurrency библиотеку в составе пакета java.util.concurrent. Java 6 предоставил (этот момент менее известен) поддержку написания скриптов (javax.script пакет) и API Java-компилятора (в составе пакета javax.tools). Java 7 принес много улучшений в java.util.concurrent, ввел новую библиотеку ввода-вывода в пакете java.nio.file и поддержку динамических языков в java.lang.invoke. И наконец, в Java 8 добавили долгожданный date/time в пакете java.time. Java как платформа развивается и ей очень важно прогрессировать наряду с вышеуказанными изменениями. Всякий раз, когда вы учитываете включение сторонней библиотеки или фреймворка в ваш проект, убедитесь, что требуемая функциональность еще не содержится в стандартных библиотеках Java (конечно, есть много специализированных и высокопроизводительных реализаций алгоритмов которые опережают алгоритмы из стандартных библиотек, но в большинстве случаев действительно они не нужны).
10. Неизменяемость
Неизменяемость во всем руковостве и в этой части остается как напоминание: пожалуйста, отнеситесь к ней со всей серьезностью. Если класс, который вы проектируете или метод, который вы реализуете, может обеспечить гарантию неизменности, он может быть использован в большинстве случаев везде без страха одновременной модификации. Это облегчит вашу жизнь разработчика (и, надеюсь, жизни коллег из вашей команды).
11. Тестирование
Практика разработки через тестирование ("Test-driven development" - TDD) сверхпопулярна в Java-сообществе, поднимая планку качества кода. Со всеми выгодами, которые предоставляет TDD, грустно видеть, что стандартная библиотека Java на сегодня не включает никакого фреймворка для проведения тестов или вспомогательных средств. Тем не менее, тестирование стало необходимой частью современной Java-разработки и в этом разделе мы взглянем на несколько базовых приемов с использованием фреймфорка JUnit. В JUnit, существенно, каждый тест - набор утверждений об ожидаемом состоянии или поведении объекта. Секрете написания отличных тестов - писать их простыми и короткими, тестируя одну вещь за раз. Как упражнение, давайте напишем набор тестов, чтобы проверить что String.format - функция из раздела строк, возвращающая желаемый результат. package com.javacodegeeks.advanced.generic; import static org.junit.Assert.assertThat; import static org.hamcrest.CoreMatchers.equalTo; import org.junit.Test; public class StringFormatTestCase { @Test public void testNumberFormattingWithLeadingZeros() { final String formatted = String.format( "%04d", 1 ); assertThat( formatted, equalTo( "0001" ) ); } @Test public void testDoubleFormattingWithTwoDecimalPoints() { final String formatted = String.format( "%.2f", 12.324234d ); assertThat( formatted, equalTo( "12.32" ) ); } } Оба теста выглядят очень читаемыми и их выполнение - это экземпляры. На сегодняшний день средний Java-проект содержит сотни тест-кейсов, дающих разработчику быстрый фидбэк в процессе разработки по регрессиям или особенностям.
12. Далее
Эта часть руководства завершает серию обсуждений связанных с практикой программирования на Java и руководствами к данному языку программированию. В следующий раз мы вернемся к особенностям языка исследуя мир Java по части исключений, их типов, как и когда использовать их.
13. Загрузка исходного кода
Это был урок по общим принципам разработки из курса Advanced Java. Исходный код к занятию можно загрузить здесь.
Комментарии (7)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Joysi Уровень 41
2 июня 2016
Рекомендую всем в данном топике просмотреть также
google.github.io/styleguide/javaguide.html
fatfaggy Уровень 26
2 июня 2016
лично я предпочитаю не использовать сокращения в составных названиях. так как через какое-то время может быть трудно вспомнить вот так с ходу что я имел ввиду год назад под тем же fis например. в общем, лучше название подлиннее, но информативнее. в названиях стараюсь выражать не детали реализации (alFruits для ArrayList), а давать название исходя из сути предмета, которое это название будет описывать (fruitsList), как-то так)
или вот еще пример:
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
BufferedReader fileReader = new BufferedReader(new FileReader(fileName));

тогда в коде если мне надо читать с консоли — я пишу со… и выскакивает нужный мне ридер, а если с файла — то fi…
если говорить о примере со множеством FileInputStream — то тут я тоже называл бы их в зависимости от того, за что этот поток будет отвечать:
FileInputStream settingsFileStream, userDataFileStream;

как-то так, в общем)

еще я знаю вроде ГОСТ какой-то был для наименования продукции, где на первое место ставится имя существительное, а потом уже все остальное. например, если у нас есть список из более 100 позиций, которые названы привычным языком:
БольшоеКрасноеЯблоко
БольшойЖелтыйЖираф
БольшойСерыйСлон

и нам из такого списка надо выбрать например только яблоки (без разницы какие) — то это уже гемор) а вот если существительное будет стоять на первом месте — тогда в списке все яблоки будут сгруппированы вместе:

ЯблокоЗеленоеБольшое
ЯблокоКрасноеБольшое
ЯблокоКрасноеМалое

не знаю насколько этот пример подходит для программирования, но вот для всяких там программ учета этот момент бывает очень важным.
тут наверное стоит придерживаться правила, что на первое место ставить то слово, которое описывает критерий, по которому будет чаще всего проводиться поиск
Joysi Уровень 41
14 февраля 2016
Хорошая работа.
Вопрос по именованию переменных (8. Соглашения об именах), кто как именует?
Многие из вас используют prefix (или как правильно в терминах) именование для непримитивов? Когда в начале имени переменной стоят первые буквы слов образующих класс в строчном варианте:

FileInputStream     fisF = new FileInputStream( r.readLine());
ArrayList           alFruits = new ArrayList();
Sygurny Уровень 26
13 февраля 2016
Первый на Автор пиши исчо и спасибо за ссылку!