JavaRush /Java Blog /Random EN /Coffee break #85. Three Java lessons I learned the hard w...

Coffee break #85. Three Java lessons I learned the hard way. How to use SOLID principles in code

Published in the Random EN group

Three Java Lessons I Learned the Hard Way

Source: Medium Learning Java is difficult. I learned from my mistakes. Now you too can learn from my mistakes and bitter experiences, which you don’t necessarily need to have. Coffee break #85.  Three Java lessons I learned the hard way.  How to use SOLID principles in code - 1

1. Lambdas can cause trouble.

Lambdas often exceed 4 lines of code and are larger than expected. This burdens working memory. Do you need to change a variable from lambda? You can't do that. Why? If the lambda can access call site variables, threading issues may arise. Therefore you cannot change variables from lambda. But Happy path in lambda works fine. After a runtime failure, you will receive this response:
at [CLASS].lambda$null$2([CLASS].java:85)
at [CLASS]$$Lambda$64/730559617.accept(Unknown Source)
It's difficult to follow the lambda stack trace. The names are confusing and difficult to track down and debug. More lambdas = more stack traces. What's the best way to debug lambdas? Use intermediate results.
map(elem -> {
 int result = elem.getResult();
 return result;
});
Another good way is to use advanced IntelliJ debugging techniques. Use TAB to select the code you want to debug and combine it with the intermediate results. “When we stop at a line containing a lambda, if we press F7 (step into), then IntelliJ highlights the fragment that needs to be debugged. We can switch the block to debug using Tab, and once we decide that, press F7 again.” How to access call site variables from lambda? You can only access final or actually final variables. You need to create a wrapper around the call location variables. Either using AtomicType or using your own type. You can change the created variable wrapper with lambda. How to solve stack trace problems? Use named functions. This way, you can quickly find responsible code, check logic, and solve problems. Use named functions to strip the cryptic stack trace. Is the same lambda repeated? Place it in a named function. You will have a single point of reference. Each lambda gets a generated function, which makes it difficult to keep track of.
lambda$yourNamedFunction
lambda$0
Named functions solve a different problem. Big lambdas. Named functions break up large lambdas, create smaller pieces of code, and create pluggable functions.
.map(this::namedFunc1).filter(this::namedFilter1).map(this::namedFunc2)

2. Problems with lists

You need to work with lists ( Lists ). You need a HashMap for the data. For roles you will need a TreeMap . The list goes on. And there is no way you can avoid working with collections. How to make a list? What kind of list do you need? Should it be immutable or mutable? All of these answers affect the future of your code. Choose the right list in advance so you don’t regret it later. Arrays::asList creates a “end-to-end” list. What can't you do with this list? You cannot resize it. He is unchangeable. What can you do here? Specify elements, sorting, or other operations that do not affect size. Use Arrays::asList carefully because its size is immutable but its contents are not. new ArrayList() creates a new “mutable” list. What operations does the created list support? That's it, and this is a reason to be careful. Create mutable lists from immutable ones using new ArrayList() . List::of creates an “immutable” collection. Its size and content are unchanged under certain conditions. If the content is primitive data, such as int , the list is immutable. Take a look at the following example.
@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
You need to create immutable objects and insert them into List::of . However, List::of does not provide any guarantee of immutability. List::of provides immutability, reliability, and readability. Know when to use mutable and when to use immutable structures. The list of arguments that should not change should be in the immutable list. A mutable list can be a mutable list. Understand what collection you need to create reliable code.

3. Annotations slow you down

Do you use annotations? Do you understand them? Do you know what they do? If you think that Logged annotation is suitable for every method, then you are wrong. I used Logged to log method arguments. To my surprise, it didn't work.
@Transaction
@Method("GET")
@PathElement("time")
@PathElement("date")
@Autowired
@Secure("ROLE_ADMIN")
public void manage(@Qualifier('time')int time) {
...
}
What's wrong with this code? There's a lot of configuration digest here. You will encounter this many times. The configuration is mixed with regular code. Not bad in itself, but it catches your eye. Annotations are needed to reduce boilerplate code. You don't need to write logging logic for each endpoint. No need to set up transactions, use @Transactional . Annotations reduce the pattern by extracting code. There is no clear winner here since both are in the game. I still use XML and annotations. When you find a repeating pattern, it's best to move the logic into the annotation. For example, logging is a good annotation option. Moral: don't overuse annotations and don't forget XML.

Bonus: You may have problems with Optional

You will use orElse from Optional . Undesirable behavior occurs when you don't pass the orElse constant . You should be aware of this to prevent problems in the future. Let's look at a few examples. When getValue(x) returns a value, getValue(y) is executed . The method in orElse is executed if getValue(x) returns a non-empty Optional value .
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
Use orElseGet . It will not execute code for non-empty 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

Conclusion

Learning Java is difficult. You can't learn Java in 24 hours. Hone your skills. Take time, learn and excel at your job.

How to use SOLID principles in code

Source: Cleanthecode Writing reliable code requires SOLID principles. At some point we all had to learn how to program. And let's be honest. We were STUPID. And our code was the same. Thank God we have SOLID. Coffee break #85.  Three Java lessons I learned the hard way.  How to Use SOLID Principles in Code - 2

SOLID principles

So how do you write SOLID code? It's actually simple. You just need to follow these five rules:
  • Single Responsibility Principle
  • Open-closed principle
  • Liskov replacement principle
  • Interface separation principle
  • Dependency Inversion Principle
Don't worry! These principles are much simpler than they seem!

Single Responsibility Principle

In his book, Robert C. Martin describes this principle as follows: “A class should have only one reason for changing.” Let's look at two examples together.

1. What not to do

We have a class called User that allows the user to do the following things:
  • Register an account
  • Login
  • Receive a notification the first time you log in
This class now has several responsibilities. If the registration process changes, the User class will change. The same will happen if the login process or notification process changes. This means that the class is overloaded. He has too many responsibilities. The easiest way to fix this is to move the responsibility to your classes so that the User class is only responsible for combining classes. If the process then changes, you have one clear, separate class that needs to change.

2. What to do

Imagine a class that should show a notification to a new user, FirstUseNotification . It will consist of three functions:
  • Check if a notification has already been displayed
  • Show notification
  • Mark notification as already shown
Does this class have multiple reasons to change? No. This class has one clear function - displaying a notification for a new user. This means that the class has one reason to change. Namely, if this goal changes. So, this class does not violate the single responsibility principle. Of course, there are a few things that might change: the way notifications are marked as read might change, or the way the notification appears. However, since the purpose of the class is clear and basic, this is fine.

Open-closed principle

The open-closed principle was coined by Bertrand Meyer: “Software objects (classes, modules, functions, etc.) should be open for extension, but closed for modification.” This principle is actually very simple. You must write your code so that new features can be added to it without changing the source code. This helps prevent a situation where you need to change classes that depend on your modified class. However, this principle is much more difficult to implement. Meyer suggested using inheritance. But it leads to a strong connection. We will discuss this in the Principles of Interface Separation and the Principles of Dependency Inversion. So Martin came up with a better approach: use polymorphism. Instead of conventional inheritance, this approach uses abstract base classes. In this way, inheritance specifications can be reused while implementation is not required. The interface can be written once and then closed to make changes. New functions must then implement this interface and extend it.

Liskov replacement principle

This principle was invented by Barbara Liskov, a Turing Award winner for her contributions to programming languages ​​and software methodology. In her article, she defined her principle as follows: “Objects in a program should be replaceable with instances of their subtypes without affecting the correct execution of the program.” Let's look at this principle as a programmer. Imagine we have a square. It could be a rectangle, which sounds logical since a square is a special shape of a rectangle. This is where the Liskov replacement principle comes to the rescue. Wherever you would expect to see a rectangle in your code, it is also possible for a square to appear. Now imagine your rectangle has SetWidth and SetHeight methods . This means that the square also needs these methods. Unfortunately, this doesn't make any sense. This means that the Liskov replacement principle is violated here.

Interface separation principle

Like all principles, the principle of interface separation is much simpler than it seems: “Many client-specific interfaces are better than one general-purpose interface.” As with the single responsibility principle, the goal is to reduce side effects and the number of changes required. Of course, no one writes such code on purpose. But it's easy to encounter. Remember the square from the previous principle? Now imagine that we decide to implement our plan: we produce a square from a rectangle. Now we're forcing the square to implement setWidth and setHeight , which probably don't do anything. If they did that, we would probably break something because the width and height wouldn't be what we expected. Luckily for us, this means that we are no longer violating the Liskov substitution principle, since we now allow the use of a square wherever we use a rectangle. However, this creates a new problem: we are now violating the principle of separating interfaces. We force the derived class to implement functionality that it chooses not to use.

Dependency Inversion Principle

The last principle is simple: high-level modules should be reusable and should not be affected by changes to low-level modules.
  • A. High level modules should not depend on low level modules. Both must depend on abstractions (such as interfaces).
  • B. Abstractions should not depend on details. The details (concrete implementations) must depend on the abstractions.
This can be achieved by implementing an abstraction that separates high- and low-level modules. The name of the principle suggests that the direction of the dependence changes, but this is not the case. It only separates the dependency by introducing an abstraction between them. As a result, you will get two dependencies:
  • High level module, depending on abstraction
  • Low level module depending on the same abstraction
This may seem difficult, but in fact it happens automatically if you correctly apply the open/closed principle and the Liskov substitution principle. That's all! You've now learned the five core principles that underpin SOLID. With these five principles, you can make your code amazing!
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION