JavaRush /Java блог /Random UA /Кава-брейк #85. Три уроки Java, які я засвоїв на власному...

Кава-брейк #85. Три уроки Java, які я засвоїв на власному гіркому досвіді. Як використовувати принципи SOLID у коді

Стаття з групи Random UA

Три уроки Java, які я засвоїв на власному гіркому досвіді

Джерело: Medium Вивчати Java складно. Я вчився на своїх помилках. Тепер і ви можете навчитися на моїх помилках та гіркому досвіді, отримувати який вам зовсім не обов'язково. Кава-брейк #85.  Три уроки Java, які я засвоїв на власному гіркому досвіді.  Як використовувати принципи SOLID у коді - 1

1. Лямбди можуть завдати неприємностей

Лямбди часто перевищують 4 рядки коду і бувають більшими, ніж передбачалося. Це ускладнює робочу пам'ять. Вам потрібно змінити змінну з лямбду? Ви не можете це зробити. Чому? Якщо лямбда може отримати доступ до змінного місця виклику, можуть виникнути проблеми з багатопоточністю. Тому ви не можете змінити змінні з лямбди. А ось Happy path у лямбді працює чудово. Після збою під час виконання ви отримаєте таку відповідь:
at [CLASS].lambda$null$2([CLASS].java:85)
at [CLASS]$$Lambda$64/730559617.accept(Unknown Source)
Слідкувати за трасуванням стека лямбда складно. Імена заплутані, їх важко відстежити та налагодити. Більше лямбд – більше трасування стека. Який найкращий спосіб налагодження лямбд? Використовуйте проміжні результати.
map(elem -> {
 int result = elem.getResult();
 return result;
});
Ще один хороший спосіб - використовувати просунуті прийоми налагодження IntelliJ. Використовуйте TAB для вибору коду, який ви хочете налагодити, і поєднайте це з проміжними результатами. Коли ми зупиняємося на рядку, що містить лямбда, якщо ми натискаємо F7 (step into), тоді IntelliJ виділяє фрагмент, який потрібно налагоджувати. Ми можемо переключити блок налагодження за допомогою Tab, і як тільки ми це вирішимо, знову натисніть F7”. Як отримати доступ до змінних місця дзвінка з лямбди? Ви можете отримати доступ до кінцевих або фактично кінцевих змінних. Вам потрібно створити оболонку (wrap) змінних місця дзвінка. Або за допомогою AtomicType або через свій type. Ви можете змінити створену змінну оболонку з lambda. Як вирішити проблеми з трасуванням стека? Використовуйте іменовані функції (named functions). Так ви зможете швидше знаходити відповідальний (responsible) код, перевіряти логіку та вирішувати проблеми. Використовуйте іменовані функції, щоб вирізати cryptic stack trace. Повторюється та сама лямбда? Помістіть її у іменовану функцію. Ви матимете єдину точку відліку. Кожна лямбда отримує згенеровану функцію, що ускладнює відстеження.
lambda$yourNamedFunction
lambda$0
Іменовані функції вирішують іншу проблему. Великі лямбди. Іменовані функції розбивають великі лямбди, створюють менші фрагменти коду і створюють функції, що підключаються.
.map(this::namedFunc1).filter(this::namedFilter1).map(this::namedFunc2)

2. Проблемами зі списками

Вам потрібно працювати зі списками ( Lists ). Вам потрібно HashMap для даних. Для ролей вам знадобиться TreeMap . Цей список можна продовжити. І вам не уникнути роботи з колекціями. Як скласти список? Який список вам потрібний? Він повинен бути незмінним чи зміненим? Всі ці відповіді впливають на майбутнє вашого коду. Виберіть правильний список заздалегідь, щоб потім не шкодувати. Arrays::asList створює "наскрізний" список. Що не можна робити із цим списком? Ви не можете змінити його розмір. Він незмінний. Що тут можна зробити? Встановіть елементи, сортування або інші операції, які не впливають на розмір. Використовуйте Arrays::asList обережно, оскільки його розмір незмінний, а вміст ні. new ArrayList() створює новий “змінний” список. Які операції підтримує список? Все, і це привід бути обережнішим. Створюйте змінені списки з незмінюваних за допомогою new ArrayList() . List::of створює "незмінну" колекцію. Її розмір та зміст незмінні за певних умов. Якщо вміст примітивні дані, наприклад, int , список незмінюємо. Погляньте на наступний приклад.
@Test
public void testListOfBuilders() {
  System.out.println("### TESTING listOF with mutable content ###");

  StringBuilder one = new StringBuilder();
  one.append("a");

  StringBuilder two = new StringBuilder();
  two.append("a");

  List<StringBuilder> asList = List.of(one, two);

  asList.get(0).append("123");

  System.out.println(asList.get(0).toString());
}
### TESTING listOF with mutable content ### a123
Вам потрібно створювати незмінні об'єкти та вставляти їх у List::of . List::of не дає жодних гарантій незмінності. List::of забезпечує незмінність, надійність та зручність читання. Знайте, коли використовувати структури, що змінюються, а коли незмінні. Список аргументів, які не повинні змінюватися, повинен бути в списку, що не змінюється. Список, що змінюється, може бути зміненим списком. Зрозумійте, яка колекція потрібна для створення надійного коду.

3. Анотації уповільнюють роботу

Ви користуєтесь інструкціями? Ви їх знаєте? Ви знаєте, що вони роблять? Якщо ви думаєте, що Logged інструкція підходить для кожного методу, то ви помиляєтеся. Я використовувався Logged для реєстрації аргументів методу. На мій подив, це не спрацювало.
@Transaction
@Method("GET")
@PathElement("time")
@PathElement("date")
@Autowired
@Secure("ROLE_ADMIN")
public void manage(@Qualifier('time')int time) {
...
}
Що не так у цьому коді? Тут багато дайджесту конфігурації. Ви стикатиметеся з цим багато разів. Конфігурація змішана із звичайним кодом. Непогано саме собою, але впадає у вічі. Інструкції потрібні, щоб зменшити шаблонний код. Вам не потрібно писати логіку журналів для кожної endpoint. Не потрібно налаштовувати транзакції, використовуйте @Transactional . Анотації скорочують шаблон з допомогою вилучення коду. Тут немає явного переможця, оскільки у грі беруть участь обидва. Я досі використовую XML та анотації. Коли ви виявите шаблон, що повторюється, краще перенести логіку в анотацію. Наприклад, логування хороший варіант анотації. Мораль: не зловживайте анотаціями та не забувайте XML.

Бонус: у вас можуть бути проблеми з Optional

Ви будете використовувати orElse з Optional . Небажана поведінка зустрічається, коли ви не передаєте константу orElse . Ви повинні знати про це, щоб запобігти проблемам у майбутньому. Подивимося кілька прикладів. Коли getValue(x) повертає значення, виконується getValue(y) . Метод в orElse виконується, якщо getValue(x) повертається непусте значення Optional .
getValue(x).orElse(getValue(y)
                  .orElseThrow(() -> new NotFoundException("value not present")));

public Optional<Value> getValue(Source s)
{
  System.out.println("Source: " + s.getName());

  // returns value from s source
}

// when getValue(x) is present system will output
Source: x
Source: y
Використовуйте orElseGet . Він не буде виконувати код для непустих Optionals .
getValue(x).orElseGet(() -> getValue(y)
                  .orElseThrow(() -> new NotFoundException("value not present")));

public Optional<Value> getValue(Source s)
{
  System.out.println("Source: " + s.getName());

  // returns value from s source
}

// when getValue(x) is present system will output
Source: x

Висновок

Вивчати Java складно. Ви не можете вивчити Java за 24 години. Відточуйте свою майстерність. Знайдіть час, навчайтеся і процвітайте в роботі.

Як використовувати принципи SOLID у коді

Джерело: Cleanthecode Для написання надійного коду потрібні принципи SOLID. Якоїсь миті нам усім довелося навчитися програмувати. І давайте будемо чесними. Ми були дурні. І наш код був таким самим. Слава Богу, ми маємо SOLID. Кава-брейк #85.  Три уроки Java, які я засвоїв на власному гіркому досвіді.  Як використовувати принципи SOLID у коді - 2

Принципи SOLID

Отже, як написати SOLID-код? Насправді, це просто. Вам просто потрібно дотримуватися цих п'яти правил:
  • Принцип єдиної відповідальності
  • Принцип відкритості-закритості
  • Принцип заміни Лісків
  • Принцип відділення інтерфейсу
  • Принцип інверсії залежностей
Не хвилюйтеся! Ці принципи набагато простіші, ніж здається!

Принцип єдиної відповідальності

У своїй книзі Роберт К. Мартін описує цей принцип так: “У класу має бути лише одна причина зміни”. Давайте разом розглянемо два приклади.

1. Чого не потрібно робити

У нас є клас з ім'ям User , який дозволяє користувачеві робити такі речі:
  • Зареєструвати обліковий запис
  • Авторизуватися
  • Отримувати сповіщення при першому вході до системи
Тепер цей клас має кілька обов'язків. Якщо процес реєстрації зміниться, клас User зміниться. Те саме станеться при зміні процесу входу в систему або в процесі сповіщення. Це означає, що клас перевантажений. У нього дуже багато обов'язків. Найпростіший спосіб виправити це – перенести відповідальність на свої класи, щоб клас User відповідав лише за об'єднання класів. Якщо процес зміниться, у вас буде один чіткий, окремий клас, який потрібно змінити.

2. Що потрібно робити

Уявіть клас, який повинен показувати повідомлення для нового користувача, FirstUseNotification . Він складатиметься з трьох функцій:
  • Перевірити, чи відображалося повідомлення.
  • Показати повідомлення
  • Позначити повідомлення як уже показане
Чи є у цього класу кілька причин зміни? Ні. Цей клас має одну зрозумілу функцію — відображення повідомлення для нового користувача. Це означає, що клас має одну причину для зміни. А саме якщо ця мета зміниться. Отже, цей клас не порушує принципу єдиної відповідальності. Звичайно, є кілька речей, які можуть змінитися: спосіб позначення повідомлень як прочитаних може змінитися або спосіб відображення повідомлення. Однак, оскільки мета класу ясна та елементарна, це нормально.

Принцип відкритості-закритості

Принцип відкритого-закритого придуманий Бертраном Мейєром: "Програмні об'єкти (класи, модулі, функції тощо) повинні бути відкриті для розширення, але закриті для модифікації". Цей принцип насправді дуже простий. Ви повинні написати свій код, щоб до нього можна було додавати нові функції без зміни вихідного коду. Це допомагає запобігти ситуації, коли вам потрібно змінити класи, які залежать від вашого зміненого класу. Проте реалізувати цей принцип набагато складніше. Мейєр запропонував використати успадкування. Але це призводить до сильного зв'язку. Це ми обговоримо в Принципах розподілу інтерфейсів та Принципах інверсії залежностей. Тому Мартін вигадав найкращий підхід: використати поліморфізм. Замість звичайного наслідування у цьому підході використовуються абстрактні базові класи. Таким чином, специфікації успадкування можна використовувати повторно, тоді як реалізація не є обов'язковою. Інтерфейс можна написати один раз, а потім закрити для внесення змін. Потім нові функції мають реалізувати цей інтерфейс та розширити його.

Принцип заміни Лісків

Цей принцип винайшла Барбара Лісков — лауреат премії Т'юрінга за внесок у розробку мов програмування та методологію програмного забезпечення. У статті вона визначила свій принцип так: “Об'єкти у програмі мають бути замінюваними на екземпляри їх підтипів без зміни правильності виконання програми”. Подивимося цей принцип як програміст. Уявіть, що маємо квадрат. Він може бути прямокутником, що звучить логічно, оскільки квадрат це особлива форма прямокутника. Тут на допомогу приходить принцип заміни Лисків. Скрізь, де ви очікуєте побачити прямокутник у своєму коді, поява квадрата також можлива. Тепер уявіть, що прямокутник має методи SetWidth і SetHeight . Це означає, що квадрату потрібні ці методи. На жаль, у цьому немає жодного сенсу. Це означає, що тут порушується принцип заміни Лисків.

Принцип відділення інтерфейсу

Як і всі інші принципи, принцип поділу інтерфейсу набагато простіше, ніж здається: "Багато інтерфейсів, спеціально призначених для клієнтів, краще ніж один інтерфейс загального призначення". Як і у випадку принципу єдиної відповідальності, мета полягає в тому, щоб зменшити побічні ефекти та кількість необхідних змін. Звісно, ​​спеціально такий код ніхто не пише. Але з ним легко зіткнутися. Пам'ятаєте квадрат із попереднього принципу? Тепер уявіть, що ми вирішабо реалізувати наш план: ми виготовляємо квадрат із прямокутника. Тепер ми змушуємо квадрат реалізувати setWidth та setHeight , які, ймовірно, нічого не роблять. Якби вони це зробабо, ми, мабуть, зламали б щось, бо ширина та висота не були б очікуваними. На щастя для нас, це означає, що ми більше не порушуємо принцип заміни Лисків, оскільки тепер дозволяємо використовувати квадрат скрізь, де ми використовуємо прямокутник. Однак це створює нову проблему: тепер ми порушуємо принцип відокремлення інтерфейсів. Ми змушуємо похідний клас реалізувати функціональність, яку він вважає за краще не використовувати.

Принцип інверсії залежностей

Останній принцип простий: високорівневі модулі мають бути повторно використані, і них повинні впливати зміни низькорівневих модулів.
  • A. Модулі високого рівня не повинні залежати від низьких модулів. Обидва мають залежати від абстракцій (наприклад, інтерфейсів).
  • Б. Абстракції не повинні залежати від деталей. Деталі (конкретні реалізації) мають залежати від абстракцій.
Цього можна досягти за рахунок реалізації абстракції, яка розділяє модулі високого та низького рівня. Назва принципу говорить про те, що напрямок залежності змінюється, але це не так. Воно лише поділяє залежність, вводячи абстракцію з-поміж них. У результаті ви отримаєте дві залежності:
  • Модуль високого рівня, залежно від абстракції
  • Модуль низького рівня, що залежить від тієї ж абстракції
Це може здатися складно, але насправді відбувається автоматично, якщо правильно застосувати принцип відкритості/закритості та принцип підстановки Лісків. От і все! Ви ознайомабось із п'ятьма основними принципами, що лежать в основі SOLID. З цими п'ятьма принципами ви зможете зробити свій код приголомшливим!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ