JavaRush /Java блог /Random UA /Посібник із загального стилю програмування
pandaFromMinsk
39 рівень
Минск

Посібник із загального стилю програмування

Стаття з групи Random UA
Стаття є частиною академічного курсу "Advanced Java" ("Java для удосконалюваних") Цей курс створений, щоб допомогти вам навчитися ефективно використовувати особливості Java. Матеріал охоплює "просунуті" теми, як створення об'єктів, конкуренцію, серіалізацію, рефлексію та ін. Курс навчить ефективно володіти прийомами Java. Подробиці тут .
Зміст
1. Вступ 2. Область видимості змінних 3. Поля класу та локальні змінні 4. Аргументи методу та локальні змінні 5. Упаковка та розпакування 6. Інтерфейси 7. Рядки 8. Угоди про імена 9. Стандартні бібліотеки 10. Незмінність 1. Далі... 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. Вихідний код до заняття можна завантажити тут .
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ