JavaRush/Java блог/Random UA/Кава-брейк #71. Рекомендації щодо аналізу Java-коду

Кава-брейк #71. Рекомендації щодо аналізу Java-коду

Стаття з групи Random UA
учасників
Джерело: DeepSource Наявність ще однієї пари очей для перевірки коду завжди корисна. Вам не потрібно бути експертом, щоб проаналізувати код. Рев'ю коду можна проводити навіть із невеликим досвідом роботи. Потрібно лише знати, як і що ви хочете перевірити.Кава-брейк #71.  Рекомендації з аналізу Java-коду - 1

1. Дотримуйтесь угоди щодо оформлення коду Java

Дотримання Java code conventions допомагає швидко переглянути код та зрозуміти його. Наприклад, всі імена пакетів у Java пишуться малими літерами, константи - тільки великими, імена змінних - в CamelCase і т. д. Повний список угод можна знайти тут . Деякі команди розробляють власні угоди про стиль, тому врахуйте це!

2. Замініть імперативний код лямбдами та потоками

Якщо ви використовуєте Java 8+, заміна циклів і надто докладних методів потоками та лямбда-виразами зробить код чистішим. Лямбди та потоки дозволяють писати функціональний код на Java. Наступний фрагмент коду фільтрує непарні числа традиційним імперативним способом:
List<Integer> oddNumbers = new ArrayList<>();
for (Integer number : Arrays.asList(1, 2, 3, 4, 5, 6)) {
	if (number % 2 != 0) {
	  oddNumbers.add(number);
  }
}
А це функціональний спосіб фільтрації непарних чисел:
List<Integer> oddNumbers = Stream.of(1, 2, 3, 4, 5, 6)
  .filter(number -> number % 2 != 0)
  .collect(Collectors.toList());

3. Остерігайтесь NullPointerException

При написанні нових методів намагайтеся уникати повернення значень null. Це може призвести до появи null pointer exception. У наведеному нижче фрагменті зовнішній метод повертає значення null, якщо списку немає цілих чисел.
class Items {
	private final List<Integer> items;
	public Items(List<Integer> items) {
	        this.items = items;
	}
	public Integer highest() {
	  if (items.isEmpty()) return null;
	  Integer highest = null;
	  for (Integer item : items) {
	      if (items.indexOf(item) == 0) highest = item;
	      else highest = highest > item ? highest : item;
	  }
	  return highest;
	}
}
Перед прямим викликом об'єкта я рекомендую перевірити наявність null, як показано нижче.
Items items = new Items(Collections.emptyList());
Integer item = items.highest();
boolean isEven = item % 2 == 0; // throws NullPointerException ❌
boolean isEven = item != null && item % 2 == 0  // ✅
Однак мати всюди перевірки на null може бути досить обтяжливо. Якщо ви використовуєте версію Java 8 або вище, розгляньте можливість використання класу Optional для надання значень без дійсних станів (valid states). Він дозволяє легко визначати альтернативну поведінку та корисний для зв'язування методів. У наведеному нижче фрагменті ми використовуємо Java Stream API, щоб знайти найбільше за допомогою методу, що повертає Optional . Зверніть увагу, що ми використовуємо Stream.reduce , який повертає значення Optional .
public Optional<Integer> highest() {
    return items
            .stream()
            .reduce((integer, integer2) >
							integer > integer2 ? integer : integer2);
}
Items items = new Items(Collections.emptyList());
items.highest().ifPresent(integer -> {             // ✅
    boolean isEven = integer % 2 == 0;
});
В якості альтернативи ви також можете використовувати анотації, такі як @Nullable або @NonNull , які попередять вас, якщо під час створення коду виникне конфлікт null. Наприклад, передача аргументу @Nullable методу, який приймає параметри @NonNull .

4. Пряме присвоєння поля посилань з коду клієнта

Посилання, надані кодом клієнта, можна змінювати, навіть якщо поле є остаточним. Давайте краще розберемося в цьому прикладі.
private final List<Integer> items;
public Items(List<Integer> items) {
        this.items = items;
}
У наведеному вище фрагменті ми безпосередньо призначаємо поле посилання з коду клієнта. Клієнт може легко змінювати вміст списку та керувати нашим кодом, як показано нижче.
List<Integer> numbers = new ArrayList<>();
Items items = new Items(numbers);
numbers.add(1); // This will change how items behaves as well
Натомість розгляньте можливість клонування посилання або створення нового посилання, а потім призначення його полю, як показано нижче:
private final List<Integer> items;
public Items(List<Integer> items) {
    this.items = new ArrayList<>(items);
}
Те саме правило застосовується при поверненні посилань. Ви повинні бути обережні, щоб не розкрити внутрішній стан, що змінюється.

5. Обережно звертайтеся до винятків

При перехопленні винятків, якщо у вас є кілька блоків перехоплення, переконайтеся, що послідовність блоків перехоплення — від конкретніших до менших. У наведеному нижче фрагменті виняток ніколи не буде перехоплений у другому блоці, оскільки клас Exception – головний.
try {
	stack.pop();
} catch (Exception exception) {
	// handle exception
} catch (StackEmptyException exception) {
	// handle exception
}
Якщо ситуація виправна і може бути оброблена клієнтом (користувачем вашої бібліотеки або коду), тоді краще використати перевірені винятки. Наприклад IOException . Воно змушує клієнта обробляти сценарій, і якщо клієнт вирішує повторно викинути виняток, це має бути свідомий заклик ігнорувати виняток.

6. Обміркуйте вибір структур даних

У Java колекції входять ArrayList , LinkedList , Vector , Stack , HashSet , HashMap , Hashtable . Важливо розуміти плюси та мінуси кожного з них, щоб використовувати їх у правильному контексті. Декілька порад, які допоможуть зробити правильний вибір:
  • Map : Корисний, якщо у вас є невпорядковані елементи, ключі, пари значень і потрібні ефективні операції вилучення, вставки та видалення. HashMap , Hashtable , LinkedHashMap - це варіанти реалізації інтерфейсу Map.
  • List : Дуже часто використовується для створення впорядкованого списку елементів. Цей список може містити дублікати. ArrayList - це реалізація інтерфейсу List . Список можна зробити безпечним, використовуючи Collections.synchronizedList . Таким чином, відпадає необхідність використання Vector . Ось ще трохи інформації про те, чому Vector насправді застарів.
  • Set : Аналогічний List , але не допускає дублікатів. Реалізує HashSet в інтерфейсі Set .

7. Подумайте двічі перед “розкриттям”

У Java є кілька модифікаторів доступу - public , protected , private . Якщо ви не бажаєте надавати метод клієнтському коду, можете залишити все private за замовчуванням. Після того, як ви відкриєте API, назад дороги вже не буде. Наприклад, у вас є клас Library , у якому є метод перевірки book by name:
public checkout(String bookName) {
	Book book = searchByTitle(availableBooks, bookName);
  availableBooks.remove(book);
  checkedOutBooks.add(book);
}

private searchByTitle(List<Book> availableBooks, String bookName) {
...
}
Якщо ви не збережете метод searchByTitle за умовчанням як private , то в кінцевому підсумку він стане доступним, і інші класи можуть почати використовувати його та будувати на його основі логіку, яку ви, можливо, хотіли б зробити частиною класу Library . Це може порушити інкапсуляцію класу Library і унеможливить відкат / зміну без порушення чужого коду.

8. Код для інтерфейсів

Якщо у вас є конкретні реалізації певних інтерфейсів (наприклад, ArrayList або LinkedList ), і якщо ви використовуєте їх у своєму коді, це може призвести до посилення зв'язку (high coupling). Використовуючи інтерфейс List , ви можете переключитися на реалізацію будь-якої миті в майбутньому, не порушуючи код.
public Bill(Printer printer) {
	this.printer = printer;
}

new Bill(new ConsolePrinter());
new Bill(new HTMLPrinter());
У наведеному вище фрагменті використання інтерфейсу Printer дозволяє розробнику перейти до іншого конкретного класу HTML Printer .

9. Не "насаджуйте" інтерфейси

Погляньте на наступний інтерфейс:
interface BookService {
		List<Book> fetchBooks();
    void saveBooks(List<Book> books);
    void order(OrderDetails orderDetails) throws BookNotFoundException, BookUnavailableException;
}

class BookServiceImpl implements BookService {
...
Чи є користь від створення такого інтерфейсу? Чи є можливість реалізації цього інтерфейсу у вигляді іншого класу? Чи достатньо універсальний цей інтерфейс для реалізації іншим класом? Якщо відповідь на всі ці питання негативна, то я рекомендую уникати цього непотрібного інтерфейсу, який вам доведеться підтримувати в майбутньому. Мартін Фаулер дуже добре пояснює це у своєму блозі . Що ж, тоді як успішно використовувати інтерфейси? Припустимо, у нас є клас Rectangle і клас Circle , які мають поведінку для обчислення периметра. Якщо є вимога, що периметр усіх форм — це варіант використання поліморфізму, наявність інтерфейсу матиме більше сенсу:
interface Shape {
		Double perimeter();
}

class Rectangle implements Shape {
//data members and constructors
    @Override
    public Double perimeter() {
        return 2 * (this.length + this.breadth);
    }
}

class Circle implements Shape {
//data members and constructors
    @Override
    public Double perimeter() {
        return 2 * Math.PI * (this.radius);
    }
}

public double totalPerimeter(List<Shape> shapes) {
	return shapes.stream()
               .map(Shape::perimeter)
               .reduce((a, b) -> Double.sum(a, b))
               .orElseGet(() -> (double) 0);
}

10. Замініть hashCode під час перевизначення Equals

Об'єкти, які дорівнюють своїм значенням, називаються об'єктами значень (value objects): наприклад, гроші, час. Такі класи повинні перевизначатися методом equals щоб повертати true, якщо значення збігаються. Метод яких зазвичай використовується іншими бібліотеками для порівняння та перевірки рівності; отже, перевизначення equals необхідно. Кожен об'єкт Java також має значення хеш-коду, який відрізняє його від іншого об'єкта.
class Coin {
    private final int value;

    Coin(int value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Coin coin = (Coin) o;
        return value == coin.value;
    }
}
У наведеному вище прикладі ми перевизначабо метод equals для Object .
HashMap<Coin, Integer> coinCount = new HashMap<Coin, Integer>() {{
  put(new Coin(1), 5);
  put(new Coin(5), 2);
}};

//update count for 1 rupee coin
coinCount.put(new Coin(1), 7);

coinCount.size(); // 3 🤯 why?
Ми очікуємо coinCount , щоб оновити кількість монет по одній рупії до семи, оскільки ми перевизначаємо рівність. Але HashMap внутрішньо перевіряє, чи дорівнює хеш-код для двох об'єктів, і лише потім переходить до перевірки рівності за допомогою методу equals . Два різні об'єкти можуть мати або не мати той самий хеш-код, але два рівних об'єкти завжди повинні мати той самий хеш-код, як визначено в контракті методу hashCode . Таким чином, перевірка хеш-коду в першу чергу є ранньою умовою виходу. Це означає, що, як і equals , методи hashCode повинні бути перевизначені, щоб виразити рівність.
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.