JavaRush /Java блог /Random UA /Кава-брейк #56. Короткий довідник з найкращих практик в J...

Кава-брейк #56. Короткий довідник з найкращих практик в Java

Стаття з групи Random UA
Джерело: DZone Цей посібник включає в себе кращі практичні та довідкові матеріали по Java для підвищення читабельності та надійності вашого коду. Розробники несуть велику відповідальність за те, щоб щодня приймати правильні рішення, і найкраще, що може допомогти їм у прийнятті правильних рішень, — це досвід. І хоча не всі з них мають великий досвід розробки програмного забезпечення, кожен може використовувати чужий досвід. Я підготував вам кілька рекомендацій, які отримав завдяки своєму досвіду роботи з Java. Я сподіваюся, що вони допоможуть вам покращити читання та надійність вашого коду Java.Кава-брейк #56.  Короткий довідник з найкращих практик в Java - 1

Принципи програмування

Не пишіть код, який працює лише . Прагніть написати код, який можна підтримувати — не лише вами, а й кимось, хто зможе в кінцевому підсумку працювати над цим програмним забезпеченням у майбутньому. 80% свого часу розробник читає код, а 20% - пише та тестує код. Значить, зосередьтеся на написанні коду, що читається. Ваш код не повинен потребувати коментарів, щоб хтось міг зрозуміти, що він робить. Щоб писати хороший код, існує безліч принципів програмування, які ми можемо використовувати як керівні вказівки. Нижче я перерахую найважливіші.
  • • KISS - Розшифровується як "Будь простіше, дурень" (Keep It Simple, Stupid). Ви можете помітити, що на початку свого шляху розробники намагаються реалізувати складний, неоднозначний дизайн.
  • • DRY — "Не повторюйся" (Don't Repeat Yourself). Намагайтеся уникати будь-яких дублікатів, натомість поміщайте їх у єдину частину системи чи методу.
  • YAGNI — "Тобі це не знадобиться" (You Ain't Gonna Need It). Якщо ви раптом почнете себе запитувати: «А як щодо додавання додаткових (функцій, коду тощо)?», тоді вам, мабуть, потрібно подумати, чи варто їх додавати.
  • Чистий код замість розумного — Говорячи простіше, залиште його за дверима і забудьте про написання розумного коду. Вам потрібний чистий код, а не розумний.
  • Уникайте передчасної оптимізації — Проблема з передчасною оптимізацією полягає в тому, що ви ніколи не дізнаєтесь, де в програмі будуть вузькі місця, доки вони себе не виявлять.
  • Єдина відповідальність — кожен клас або модуль у програмі повинен піклуватися лише про надання одного біта певної функціональності.
  • Композиція замість успадкування реалізацій — Об'єкти зі складною поведінкою повинні містити екземпляри об'єктів з індивідуальною поведінкою, а не успадковувати клас та додавати нові поведінки.
  • Об'єктна гімнастика – це вправи з програмування, оформлені у вигляді набору з 9 правил .
  • Швидка невдача, швидка зупинка — Цей принцип означає зупинення поточної операції у разі виникнення будь-якої непередбаченої помилки. Дотримання цього принципу призводить до стабільнішої роботи.

Пакети

  1. Віддавайте перевагу структуруванню пакетів за предметними областями , а не за технічними рівнями.
  2. Віддавайте перевагу макетам, які сприяють інкапсуляції та приховування інформації для захисту від неправильного використання, а не організації класів з технічних причин.
  3. Переглядайте пакети, як вони мають незмінний API, - не розкривайте внутрішні механізми (класи), призначені лише внутрішньої обробки.
  4. Не відкривайте доступ до класів, які можна використовувати лише всередині пакета.

Класи

Статичні

  1. Не дозволяйте створення статичного класу. Завжди створюйте приватний конструктор.
  2. Статичні класи повинні залишатися незмінними, не допускайте створення підкласів та багатопотокових класів.
  3. Статичні класи повинні бути захищені від зміни орієнтації та повинні надаватися у вигляді утиліт, таких як фільтрація списку.

успадкування

  1. Вибирайте композицію, а не успадкування.
  2. Не виставляйте захищені поля. Натомість вкажіть захищений метод доступу.
  3. Якщо змінну класу можна помітити як остаточну , зробіть це.
  4. Якщо наслідування не очікується, зробіть клас остаточним .
  5. Позначте метод як остаточний , якщо не очікується, що підкласи буде дозволено його перевизначити.
  6. Якщо конструктор не потрібний, не створюйте конструктор за умовчанням без логіки реалізації. Java автоматично надасть конструктор за замовчуванням, якщо він не вказаний.

Інтерфейси

  1. Не використовуйте шаблон константного інтерфейсу (an interface of constants), оскільки він дозволяє класам реалізовувати та забруднювати API. Натомість використовуйте статичний клас. Це дає додаткову перевагу, дозволяючи вам виконувати більш складну ініціалізацію об'єкта у статичному блоці (наприклад, заповнення колекції).
  2. Уникайте надмірного використання інтерфейсу .
  3. Наявність одного і лише одного класу, що реалізує інтерфейс, швидше за все, призведе до надмірного використання інтерфейсів і принесе більше шкоди, ніж користі.
  4. "Програма для інтерфейсу, а не для реалізації" не означає, що ви повинні об'єднати кожен з ваших класів домену з більш менш ідентичним інтерфейсом, роблячи це, ви порушуєте YAGNI .
  5. Завжди робіть інтерфейси невеликими та конкретними, щоб клієнти знали лише про методи, які їм цікаві. Перевірте ISP від ​​SOLID.

Фіналізатори

  1. Об'єкт # finalize () слід використовувати розумно і лише як засіб захисту від збоїв під час очищення ресурсів (наприклад, закриття файлу). Завжди надавайте явний спосіб очищення (наприклад, close() ).
  2. В ієрархії спадкування завжди викликайте батьківський finalize() у блоці try . Очищення класу має бути в блоці finally .
  3. Якщо явний метод очищення не був викликаний та фіналізатор закрив ресурси, запишіть цю помилку.
  4. Якщо засіб ведення журналу недоступний, використовуйте обробник виключень потоку (який зрештою передає стандартну помилку, яка фіксується в журналах).

Загальні правила

Твердження

Твердження, зазвичай, у формі перевірки попередньої умови, забезпечує виконання договору типу «швидка невдача, швидка зупинка». Їх слід використовувати широко, щоб виявляти помилки програмування якомога ближче до причини. Стан об'єкту:
  • • Об'єкт ніколи не повинен створюватися або переходити до неприпустимого стану.
  • • У конструкторах та методах завжди описуйте та застосовуйте контракт за допомогою перевірок.
  • • Слід уникати використання ключового Java-терміну assert , оскільки він може бути вимкнений і зазвичай є крихкою конструкцією.
  • • Використовуйте службовий клас Assertions , щоб уникнути докладних умов if-else для перевірки попередньої умови.

Дженеріки

Повне, надзвичайно докладне пояснення доступне у FAQ Java Generics . Нижче наведено поширені сценарії, про які слід знати розробникам.
  1. По можливості краще використовувати висновок типу, а не повертати базовий клас/інтерфейс:

    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. Якщо тип не можна визначити автоматично, встановіть його.

    public class MySpecialObject extends MyObject {
     public MySpecialObject() {
      super(Collections.emptyList());   // This is ugly, as we loose type
      super(Collections.EMPTY_LIST();    // This is just dumb
      // But this is beauty
      super(new ArrayList());
      super(Collections.emptyList());
     }
    }

  3. Підстановочні знаки:

    Використовуйте розширений знак підстановки, коли ви отримуєте тільки значення зі структури, використовуйте знак підстановки super , коли ви поміщаєте тільки значення в структуру, і не використовуйте знак підстановки, коли ви робите і те, і інше.

    1. Всі люблять PECS ! ( Producer-extends, Consumer-super )
    2. Використовуйте Foo для виробника T.
    3. Використовуйте Foo для споживача T.

Сінглтони

Сінглтон ніколи не слід писати в класичному стилі шаблонів проектування , який цілком допустимий для C++, але не підходить для Java. Незважаючи на те, що він правильно орієнтований на багатопоточність, ніколи не реалізуйте наступне (це було б вузьким місцем у продуктивності!):
public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
Якщо відкладена ініціалізація справді бажана, то комбінація цих двох підходів спрацює.
public final class MySingleton {
  private MySingleton() {
   // singleton
  }
  private static final class MySingletonHolder {
    static final MySingleton instance = new MySingleton();
  }
  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}
Spring: за замовчуванням bean-компонент реєструється з одноелементною областю видимості, що означає, що лише один екземпляр буде створений контейнером та підключений до всіх споживачів. Це забезпечує ту саму семантику, як і звичайний синглтон, без обмежень продуктивності чи зв'язування.

Винятки

  1. Використовуйте зазначені винятки для виправних умов та виключення часу виконання помилок програмування. Приклад: одержання цілого числа з рядка.

    Погано: NumberFormatException розширює RuntimeException, тому воно призначене для позначення помилок програмування.

  2. Не робіть такого:

    // String str = input string
    Integer value = null;
    try {
       value = Integer.valueOf(str);
    } catch (NumberFormatException e) {
    // non-numeric string
    }
    if (value == null) {
    // handle bad string
    } else {
    // business logic
    }

    Правильне використання:

    // String str = input string
    // Numeric string with at least one digit and optional leading negative sign
    if ( (str != null) && str.matches("-?\\d++") ) {
       Integer value = Integer.valueOf(str);
      // business logic
    } else {
      // handle bad string
    }
  3. Ви повинні обробляти винятки у потрібному місці, у потрібному місці на рівні домену.

    НЕПРАВИЛЬНИЙ СПОСІБ — шар об'єкта даних не знає, що робити у разі виключення бази даних.

    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    РЕКОМЕНДУЄМИЙ СПОСІБ — рівень даних просто повинен повторно викликати виняток та передати відповідальність за обробку виключення або не на правильний рівень.

    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. Винятки зазвичай НЕ повинні реєструватися в момент їх видачі, а скоріше в момент їх фактичної обробки. Винятки журналу, коли вони генеруються або повторно генеруються, мають тенденцію заповнювати файли журналу шумом. Також зверніть увагу, що трасування стека винятків все одно фіксує, де було згенеровано виняток.

  5. Підтримуйте стандартні винятки.

  6. Використовуйте винятки, а не коди повернення.

Equals та HashCode

При написанні належних методів еквівалентності об'єктів та хеш-коду необхідно враховувати низку проблем. Щоб спростити використання, використовуйте java.util.Objects' equals та hash .
public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {
   if (this == o) {
     return true;
   } else if (!(o instanceof User)) {
     return false;
   }
   User user = (User) o;
   return Objects.equals(getFirstName(), user.getFirstName()) &&
    Objects.equals(getLastName(),user.getLastName()) &&
    Objects.equals(getAge(), user.getAge());
 }
 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}

Управління ресурсами

Способи безпечного вивільнення ресурсів: Оператор try-with-resources гарантує, що кожен ресурс буде закрито наприкінці оператора. Будь-який об'єкт, який реалізує java.lang.AutoCloseable, який включає всі об'єкти, що реалізують java.io .Closeable, може бути використаний як ресурс.
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

Застосовуйте Shutdown Hooks

Застосовуйте shutdown hook , який викликається, якщо JVM буде коректно завершено. (Але він не зможе обробити раптові переривання, наприклад, через відключення електроенергії) Це рекомендована альтернатива замість оголошення методу finalize() , який запускатиметься тільки в тому випадку, якщо System.runFinalizersOnExit() має значення true (за замовчуванням — false) .
public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {
   Runtime
     .getRuntime()
     .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }
 /** Code may have acquired lock across servers */
 ...
 /** Safely releases the distributed lock. */
 private static final class LockShutdown implements Runnable {
   private final ExpiringGeneralLock distributedLock;
   public LockShutdown(ExpiringGeneralLock distributedLock) {
     if (distributedLock == null) {
       throw new IllegalArgumentException("ExpiringGeneralLock is null");
     }
     this.distributedLock = distributedLock;
   }
   public void run() {
     if (isLockAlive()) {
       distributedLock.release();
     }
   }
   /** @return True if the lock is acquired and has not expired yet. */
   private boolean isLockAlive() {
     return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
   }
 }
}
Дозвольте ресурсам стати завершеними (а також відновлюваними) розподіливши їх між серверами. (Це дозволить відновитись після раптового переривання, такого як відключення електроенергії). Приклад коду вище, в якому використовується ExpiringGeneralLock (блокування, загальне для всіх систем).

Date-Time

Java 8 представляє новий API Date-Time у пакеті java.time. У Java 8 представлений новий API Date-Time для усунення наступних недоліків старого API Date-Time: не багатопотоковість, поганий дизайн, складна обробка часових поясів і т.д.

Паралелізм

Загальні правила

  1. Стережіться наступні бібліотеки, які не орієнтовані на багатопоточність. Завжди виконуйте синхронізацію з об'єктами, якщо вони використовуються кількома потоками.
  2. Date ( не незмінна ) – використовуйте новий API Date-Time, який є потокобезпечним.
  3. SimpleDateFormat — використовуйте новий API Date-Time, який є безпечним.
  4. Вважайте за краще використовувати класи java.util.concurrent.atomic , а не робіть змінні volatile .
  5. Поведінка атомарних класів більш очевидна для середнього розробника, тоді як volatile вимагає розуміння моделі пам'яті Java.
  6. Атомарні класи містять змінні volatile в більш зручний інтерфейс.
  7. Розберіть приклади використання, в яких підходить volatile . (див. статтю )
  8. Використовуйте Callable , коли потрібен перевірений виняток, але немає типу, що повертається. Оскільки Void не може бути створений, він повідомляє про намір і може безпечно повернути null .

Потоки

  1. java.lang.Thread слід вважати застарілим. Хоча офіційно це не так, майже у всіх випадках пакет java.util.concurrent надає чіткіше вирішення проблеми.
  2. Розширення java.lang.Thread вважається поганою практикою - натомість реалізуйте Runnable і створіть новий потік з екземпляром у конструкторі (правило композиції важливіше за спадкування).
  3. Віддайте перевагу виконавцям і потокам, коли потрібна паралельна обробка.
  4. Завжди рекомендується вказати власну фабрику потоків, що настроюються для управління конфігурацією створюваних потоків (тут докладніше ).
  5. Використовуйте DaemonThreadFactory в Executors для некритичних потоків, щоб пул потоків міг бути вимкнений негайно після завершення роботи сервера (здесь докладніше ).
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. Синхронізація Java вже не така повільна (55-110 нс). Не уникайте її, використовуючи хитрощі, такі як double-checked locking .
  2. Віддайте перевагу синхронізації з внутрішнім об'єктом, а не з класом, оскільки користувачі можуть синхронізуватися з вашим класом/екземпляром.
  3. Завжди синхронізуйте кілька об'єктів в одному порядку, щоб уникнути взаємоблокування.
  4. Синхронізація з класом насправді не блокує доступ до його внутрішніх об'єктів. Завжди використовуйте одні й самі блокування при доступі до ресурсу.
  5. Пам'ятайте, що ключове слово synchronized не вважається частиною методу сигнатури і, отже, не буде успадковано.
  6. Уникайте надмірної синхронізації, це може призвести до зниження продуктивності та тупикової ситуації. Використовуйте ключове слово synchronized строго для частини коду, яка вимагає синхронізації.

Колекції

  1. По можливості використовуйте паралельні колекції Java-5 у багатопотоковому коді. Вони безпечні і мають чудові характеристики.
  2. За потреби використовуйте CopyOnWriteArrayList замість synchronizedList.
  3. Використовуйте Collections.unmodifiable list (…) або скопіюйте колекцію при її отриманні як параметр new ArrayList (list) . Уникайте змін локальних колекцій ззовні вашого класу.
  4. Завжди повертайте копію своєї колекції, уникаючи змін вашого списку ззовні new ArrayList (list) .
  5. Кожна колекція має бути загорнута в окремий клас, тому тепер у поведінки, пов'язаної з колекцією, є будинок (наприклад, методи фільтрації, застосування правила до кожного елемента).

Різне

  1. Вибирайте лямбди, а чи не анонімні класи.
  2. Вибирайте посилання на методи, а не на лямбди.
  3. Використовуйте enums замість констант типу int.
  4. Уникайте використання float та double, якщо потрібні точні відповіді, натомість використовуйте BigDecimal, наприклад Money.
  5. Вибирайте примітивні типи, а не boxed примітиви.
  6. Слід уникати використання магічних чисел у коді. Використовувати константи.
  7. Чи не повертайте Null. Спілкуйтесь зі своїм клієнтом методу за допомогою `Optional`. Те саме для колекцій — повертайте порожні масиви чи колекції, а не nulls.
  8. Уникайте створення непотрібних об'єктів, повторно використовуйте об'єкти та уникайте непотрібного очищення GC.

Відкладена ініціалізація

Лінива ініціалізація - це оптимізація продуктивності. Вона використовується, коли дані з якоїсь причини вважаються «дорогими». У Java 8 ми повинні використовувати функціональний інтерфейс постачальника.
== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
На цьому поки що все, сподіваюся, це було корисно!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ