JavaRush /Java блог /Random /Кофе-брейк #85. Три урока Java, которые я усвоил на собст...

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

Статья из группы Random

Три урока 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. С этими пятью принципами вы сможете сделать свой код потрясающим!
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ